简体   繁体   English

PInvoke x64与.Net 4.0崩溃

[英]PInvoke x64 crash with .Net 4.0

I've been tasked with getting some C# code working in x64 that calls a native x64 dll called Detagger that is used for converting HTML into Text while maintaining the basic stucture of the HTML. 我的任务是让一些C#代码在x64中运行,调用一个名为Detagger的本机x64 dll,用于将HTML转换为Text,同时保持HTML的基本结构。

This code has worked for years when running with platform target x86 for the C# code and an x86 build of the dll, but it's crashing when setting the platform target to x64 and using an x64 build of the dll. 这个代码在使用C#代码的平台目标x86和dll的x86版本运行多年,但是当将平台目标设置为x64并使用d64的x64版本时,它会崩溃。 In fact, x64 works fine if the C# app is built with the .Net framework 3.5 or below. 事实上,如果C#app是使用.Net framework 3.5或更低版本构建的,那么x64可以正常工作。 It crashes when built with 4.0 or above. 使用4.0或更高版本构建时崩溃。

The dll in question has the following header: 有问题的dll有以下标题:

#ifdef WIN32
    #ifdef USE_DLL
    #ifdef DLL_EXPORTS
        #define DLL_DECLARE __declspec(dllexport) long __stdcall
    #else
        #define DLL_DECLARE __declspec(dllimport) long __stdcall
    #endif
    #else
    #define DLL_DECLARE long
    #endif
#else
    #define DLL_DECLARE long
#endif

...

DLL_DECLARE CONVERTER_Allocate ();  // returns non-zero Handle if succeeds

...

DLL_DECLARE CONVERTER_ResetPolicies (long Handle);

And so the API requires calling the CONVERT_Allocate() function to get a "handle" (which I think is actually a memory address) and then passing that "handle" into all of the other methods. 因此,API需要调用CONVERT_Allocate()函数来获取“句柄”(我认为它实际上是一个内存地址),然后将“句柄”传递给所有其他方法。 I presume this is for making the calls thread safe. 我认为这是为了使调用线程安全。

I'm trying to focus on the CONVERTER_ResetPolicies() function for now, because that is one of the most basic ones that takes just a single parameter (the "handle"). 我现在正在尝试关注CONVERTER_ResetPolicies()函数,因为这是仅使用一个参数(“句柄”)的最基本的函数之一。 None of the functions in the entire API are complicated, all taking basic types or pointers to such as parameters (no structs). 整个API中的所有函数都不复杂,所有函数都采用基本类型或指针(如参数(无结构))。

From the C++ header, the calling convention is supposedly stdcall, and each of the exported functions in the dll returns a long (which should be 4 bytes in both x86 and x64). 从C ++头文件中,调用约定应该是stdcall,并且dll中的每个导出函数都返回一个long(在x86和x64中都应该是4个字节)。 My understanding of x64 is that its calling convention is basically always a variant of fastcall, so I'm curious about the stdcall, but it works in .Net 3.5 and below so that's a question for another day. 我对x64的理解是它的调用约定基本上总是fastcall的变体,所以我对stdcall很好奇,但是它在.Net 3.5及以下版本中工作,这是另一天的问题。

The PInvoke signatures provided by the vendor for the dll are: 供应商为dll提供的PInvoke签名是:

// DLL_DECLARE CONVERTER_Allocate();
[DllImport(_dll, EntryPoint = "CONVERTER_Allocate")]
public static extern IntPtr Allocate();

// DLL_DECLARE CONVERTER_ResetPolicies(long Handle);
[DllImport(_dll, EntryPoint = "CONVERTER_ResetPolicies")]
public static extern APIResult ResetPolicies(IntPtr handle);

Given the following C# code: 给出以下C#代码:

IntPtr handle = DetaggerAPI.Allocate();
var result = DetaggerAPI.ResetPolicies();

This crashes in the call to CONVERTER_ResetPolicies(). 这在CONVERTER_ResetPolicies()调用中崩溃。 Stepping in the debugger reveals the following: 单步调试器会显示以下内容:

In C#: handle = 0x00000000e82d0080 在C#中:handle = 0x00000000e82d0080

In disassembly after stepping into the DLL: 在进入DLL后的反汇编中:

registers and flags: 寄存器和标志:

RAX = 000000018001B490 RBX = 0000000FCC66EB68 RCX = 00000000E82D0080
RDX = 0000000FCC66EC80 RSI = 0000000FCF8B44A8 RDI = 0000000FCC66E980 
R8  = 00001EB6102A86D4 R9  = 0000000FE84C4001 R10 = 00007FF9497961F0
R11 = 0000000000000000 R12 = 0000000000000000 R13 = 0000000FCC66EAF0
R14 = 0000000FCC66EB68 R15 = 0000000000000004 RIP = 000000018001B490 
RSP = 0000000FCC66E848 RBP = 0000000FCC66E850 EFL = 00000246 

CS = 0033 DS = 0000 ES = 0000 SS = 002B FS = 0000 GS = 0000 

OV = 0 UP = 0 EI = 1 PL = 0 ZR = 1 AC = 0 PE = 1 CY = 0 

Note that the value for handle is in RCX (e82d0080). 请注意,handle的值在RCX中(e82d0080)。

Here is the dissassembly (some comments added by me): 这是反汇编(我添加的一些评论):

000000018001B490  sub         rsp,28h                   ; subtract 40 from stack pointer, sets up stack frame
000000018001B494  call        000000018001B090  

    000000018001B090  push        rbx  
    000000018001B092  sub         rsp,20h               ; subtract 32 from stack pointer, sets up stack frame
    000000018001B096  test        ecx,ecx               ; check if ecx is 0
    000000018001B098  movsxd      rbx,ecx               ; move value in ecx (the handle passed in) to rbx and sign-extend it to qword
                                                        ; rbx changes from 0000000FCC66EB68 to FFFFFFFFE82D0080
    000000018001B09B  je          000000018001B0C6      ; if ecx is 0, probably jump to a function that returns an error
->  000000018001B09D  cmp         dword ptr [rbx],4D2h  ; compare value pointed to by rbx (as a dword) to 042d (1234),
                                                        ; but rbx points to FFFFFFFFE82D0080, which is probably an invalid memory location,
                                                        ; so !!this is the line that crashes !!
    000000018001B0A3  jne         000000018001B0C6      ; jump if not equal

    000000018001B0A5  mov         ecx,dword ptr [1801122C0h]  
    000000018001B0AB  mov         dword ptr [rbx+2F0B0h],ecx  
    000000018001B0B1  lea         rcx,[rbx+2F0B8h]  
    000000018001B0B8  call        00000001800A7C40  
    000000018001B0BD  mov         rax,rbx  
    000000018001B0C0  add         rsp,20h  
    000000018001B0C4  pop         rbx  
    000000018001B0C5  ret  

000000018001B499  test        rax,rax  
000000018001B49C  jne         000000018001B4BC  
000000018001B49E  cmp         dword ptr [1801122C0h],eax  
000000018001B4A4  je          000000018001B4B2  
000000018001B4A6  lea         rcx,[1800D7B70h]  
000000018001B4AD  call        000000018001B290  
000000018001B4B2  mov         eax,2                     ; if we got here, return 2 in eax, meaning APIResult.Invalid.  Note that this is 32bits.
000000018001B4B7  add         rsp,28h                   ; clean up stack frame
000000018001B4BB  ret                                   ; return

So, looks like the "handle" is being passed in RCX, and then subsequently the 所以,看起来像“句柄”正在RCX中传递,然后随后

movsxd  rbx,ecx

instruction is copying this handle into RBX but also basically destroying it since it appears to be a memory address rather than just some opaque handle that is an array index or something similar. 指令是将此句柄复制到RBX中,但也基本上将其破坏,因为它似乎是一个内存地址,而不仅仅是一些不透明的句柄,它是一个数组索引或类似的东西。 Then two instructions later I get an access violation from the instruction 然后两条指令后来我从指令中获得了访问冲突

cmp dword ptr [rbx],4D2h

because this is trying to dereference RBX, which points to garbage. 因为这是试图取消引用RBX,这指向垃圾。

According to https://msdn.microsoft.com/en-us/library/ee941656(v=vs.100).aspx#core , under Platform Invoke, it says the difference between 3.5 SP1 and 4.0 is: 根据https://msdn.microsoft.com/en-us/library/ee941656(v=vs.100).aspx#core ,在Platform Invoke下,它说3.5 SP1和4.0之间的区别是:

To improve performance in interoperability with unmanaged code, incorrect calling conventions in a platform invoke now cause the application to fail. 为了提高与非托管代码的互操作性能,平台调用中的错误调用约定现在导致应用程序失败。 In previous versions, the marshaling layer resolved these errors up the stack. 在以前的版本中,编组层在堆栈中解决了这些错误。

That is kind of vague, but since my only option here is stdcall (fastcall is not supported), I presume that is correct and not the issue. 这有点模糊,但由于我唯一的选择是stdcall(不支持fastcall),我认为这是正确的而不是问题。

Some things I'm going to try: 我要尝试的一些事情:

  1. Debugging running against .Net 3.5 and try to see what's different. 调试针对.Net 3.5运行并尝试查看有什么不同。
  2. Create a C++/cli wrapper for the dll instead of using PInvoke. 为dll创建C ++ / cli包装器而不是使用PInvoke。

If anyone can spot what's going on here or give me any ideas, that'd be great. 如果有人能够发现这里发生的事情或给我任何想法,那就太好了。

As you mentioned, the assembly is clearly accessing the handle as a pointer. 正如您所提到的,程序集显然是作为指针访问句柄。 This means it is supposed to be a pointer, but since long on Windows is always 32-bit, it doesn't work. 这意味着它应该是一个指针,但由于long在Windows上始终是32位,这是行不通的。

It is probably a mistake, the C++ code shouldn't use long . 这可能是一个错误,C ++代码不应该使用long It was probably a code that was written for linux, since long is 64-bit on linux (still a mistake to rely on compiler defined size). 它可能是为linux编写的代码,因为linux上的long是64位(依赖于编译器定义的大小仍然是错误的)。

I suggest that you replace the type of all the occurrence of handles by intptr_t (defined for linux and Windows in <cstdint> / <stdint.h> ), to get the [probable] intended behavior. 我建议你用intptr_t (在<cstdint> / <stdint.h>为linux和Windows定义)替换所有句柄出现的类型,以获得[可能的]预期行为。 Actually, it is probably a good idea to replace all the long by intptr_t , since the mistake is probably everywhere. 实际上,用intptr_t替换所有long可能是一个好主意,因为错误可能在任何地方。

EDIT: Since the code initially use a plain integer type, intptr_t is probably safer, but the ideal solution would be to use a typedef to void* , that would work everywhere and make more sense. 编辑:由于代码最初使用普通整数类型, intptr_t可能更安全,但理想的解决方案是使用typedef来void* ,这将在任何地方工作并且更有意义。 If you see that using void* doesn't reveal any problem, use that instead (only for handles). 如果您发现使用void*没有显示任何问题,请改用它(仅用于句柄)。

If I'm interpreting the disassembly correctly then the x64 build of this DLL has a fatal flaw that is causing this issue. 如果我正确解释反汇编,那么这个DLL的x64版本有一个导致此问题的致命缺陷。 It appears to be trying to passing a 64 bit as pointer as a 32 bit singed integer ( long ). 它似乎试图将64位作为指针传递为32位烧结整数( long整数)。

That's based on the following analysis of the disassembly: 这是基于以下对反汇编的分析:

  1. You pass in the handle value e82d0080 传入句柄值e82d0080
  2. The DLL takes that handle and converts it to a 64 bit value DLL获取该句柄并将其转换为64位值
  3. The DLL then takes that 64 bit value and reads from that memory address. 然后DLL获取该64位值并从该存储器地址读取。

It appears to be doing something to the following code: 它似乎对以下代码做了一些事情:

DLL_DECLARE CONVERTER_ResetPolicies (long Handle) {
    int* ptr = (int*)Handle;
    if (*ptr == 0x4D2h) 
         ...
}

This code will fail as soon as Handle > 0x7FFFFFFF because of the sign extension in the conversion at the line movsxd rbx,ecx . Handle > 0x7FFFFFFF由于转换符号扩展名为movsxd rbx,ecx此代码将失败。

This code could work as long as Handle was allocated below 0x7FFFFFFF . 只要Handle分配在0x7FFFFFFF以下,此代码就可以工作。 That could explain why it works in .Net 3.5 but not 4.0 and why this code may have made it through testing. 这可以解释为什么它可以在.Net 3.5中工作但不能在4.0中工作,以及为什么这段代码可能通过测试。 You could confirm this by looking at the value of Handle when running under 3.5. 您可以通过在3.5下运行时查看Handle的值来确认这一点。

This also reminds me of this blog post which explains that memory allocated changed between Windows 7 and 8 causes memory to be allocated above 4GB on Windows 8. So this could be another factor that would cause this code to only fail in certain environments. 这也让我想起了这篇博文 ,它解释了在Windows 7和8之间分配的内存变化导致在Windows 8上将内存分配到4GB以上。因此,这可能是导致此代码仅在某些环境中失败的另一个因素。

The PInvoke signatures provided by the vendor look wrong: long is 4-bytes in x64 mode, but IntPtr is 8-bytes in x64 mode . 供应商提供的PInvoke签名看起来是错误的:x64模式下长4个字节,但x64模式下IntPtr是8个字节 I suggest changing them to UInt32. 我建议将它们更改为UInt32。

// DLL_DECLARE CONVERTER_Allocate();
[DllImport(_dll, EntryPoint = "CONVERTER_Allocate")]
public static extern UInt32 Allocate();

// DLL_DECLARE CONVERTER_ResetPolicies(long Handle);
[DllImport(_dll, EntryPoint = "CONVERTER_ResetPolicies")]
public static extern APIResult ResetPolicies(UInt32 handle);

This probably should not have worked under .NET 3.5 either, and it is just working by luck. 这可能不应该在.NET 3.5下工作,它只是运气好。 Also, I have no idea what APIResult is so I didn't look into that part. 另外,我不知道APIResult是什么,所以我没有考虑那部分。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM