PInvoke x64与.Net 4.0一起崩溃

PInvoke x64 crash with .Net 4.0

本文关键字:一起 崩溃 Net x64 PInvoke      更新时间:2023-10-16

我的任务是让一些C#代码在x64中工作,它调用一个名为Detagger的本地x64 dll,该dll用于将HTML转换为文本,同时维护HTML的基本结构。

当使用C#代码的平台目标x86和dll的x86版本运行时,此代码已经运行了多年,但当将平台目标设置为x64并使用dll的x64版本时,它会崩溃。事实上,如果C#应用程序是使用.Net framework 3.5或更低版本构建的,x64运行良好。使用4.0或更高版本构建时会崩溃。

有问题的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);

因此API需要调用CONVERT_Allocate((函数来获得一个"句柄"(我认为它实际上是一个内存地址(,然后将该"句柄"传递到所有其他方法中。我想这是为了使调用线程安全。

我现在试着把重点放在CONVERTER_ResetPolicies((函数上,因为它是最基本的函数之一,只需要一个参数("handle"(。整个API中没有一个函数是复杂的,都采用了基本类型或指向参数等的指针(没有结构(。

从C++标头来看,调用约定应该是stdcall,dll中的每个导出函数都返回一个长函数(在x86和x64中都应该是4字节(。我对x64的理解是,它的调用约定基本上总是fastcall的变体,所以我对stdcall很好奇,但它在.Net 3.5及以下版本中有效,所以这是一个有待解决的问题。

供应商为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);

给定以下C#代码:

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

这在对CONVERTER_ResetPolicies((的调用中崩溃。进入调试器会显示以下内容:

在C#中:句柄=0x00000000e82d0080

在进入DLL后的反汇编中:

寄存器和标志:

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 

请注意,句柄的值以RCX(e82d0080(为单位。

以下是disassembly(我添加了一些评论(:

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

因此,看起来"句柄"正在RCX中传递,然后是

movsxd  rbx,ecx

指令将这个句柄复制到RBX中,但基本上也会破坏它,因为它看起来是一个内存地址,而不仅仅是数组索引之类的不透明句柄。然后两条指令之后,我从指令中得到了访问违规

cmp dword ptr [rbx],4D2h

因为这是在试图取消引用指向垃圾的RBX。

根据https://msdn.microsoft.com/en-us/library/ee941656(v=vs.100(.aspx#core,在Platform Invoke下,它说3.5 SP1和4.0之间的区别是:

为了提高与非托管代码的互操作性的性能,平台调用中不正确的调用约定现在会导致应用程序失败。在早期版本中,封送处理层解决了这些错误。

这有点模糊,但由于我在这里唯一的选择是stdcall(不支持fastcall(,我认为这是正确的,而不是问题所在。

我要尝试的一些东西:

  1. 调试在.Net 3.5上运行,并尝试查看有什么不同
  2. 为dll创建一个C++/cli包装器,而不是使用PInvoke

如果有人能发现这里发生了什么,或者给我任何想法,那就太好了。

正如您所提到的,程序集显然是作为指针访问句柄的。这意味着它应该是一个指针,但由于Windows上的long总是32位的,所以它不起作用。

这可能是一个错误,C++代码不应该使用long。这可能是为linux编写的代码,因为long在linux上是64位的(依赖编译器定义的大小仍然是一个错误(。

我建议您用intptr_t(在<cstdint>/<stdint.h>中为linux和Windows定义(替换所有句柄出现的类型,以获得[可能]的预期行为。实际上,用intptr_t替换所有long可能是个好主意,因为错误可能无处不在。

EDIT:由于代码最初使用纯整数类型,intptr_t可能更安全,但理想的解决方案是对void*使用typedef,这将在任何地方都有效,而且更有意义。如果您发现使用void*不会发现任何问题,请使用它(仅用于句柄(。

如果我正确解释了反汇编,则此DLL的x64版本存在导致此问题的致命缺陷。它似乎试图将64位作为指针作为32位单整数(long(进行传递。

这是基于以下对拆卸的分析:

  1. 您传入句柄值e82d0080
  2. DLL获取该句柄并将其转换为64位值
  3. DLL然后获取该64位值并从该内存地址中读取

它似乎对以下代码做了什么:

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

一旦Handle>0x7FFFFFFF,此代码就会失败,因为在movsxd rbx,ecx行的转换中存在符号扩展。

只要Handle被分配到0x7FFFFFFF之下,这个代码就可以工作。这可以解释为什么它能在.Net 3.5中工作,但不能在4.0中工作,以及为什么这些代码可能已经通过了测试。您可以通过查看在3.5下运行时Handle的值来确认这一点。

这也让我想起了这篇博客文章,其中解释了在Windows 7和8之间分配的内存发生变化会导致Windows 8上的内存分配超过4GB。因此,这可能是导致此代码仅在某些环境中失败的另一个因素。

供应商提供的PInvoke签名看起来错误:在x64模式下,long是4字节,但在x64模式中,IntPtr是8字节。我建议将它们更改为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);

这可能也不应该在.NET3.5下工作,而且它只是运气好。此外,我不知道APIResult是什么,所以我没有研究那个部分。