【问题标题】:PInvoke x64 crash with .Net 4.0PInvoke x64 与 .Net 4.0 崩溃
【发布时间】:2015-12-04 07:10:29
【问题描述】:

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

此代码在 C# 代码的平台目标 x86 和 dll 的 x86 构建下运行多年,但在将平台目标设置为 x64 并使用 dll 的 x64 构建时它会崩溃。事实上,如果 C# 应用程序是使用 .Net 框架 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() 函数,因为它是最基本的函数之一,只需要一个参数(“句柄”)。整个 API 中没有一个函数很复杂,都是采用基本类型或指针等参数(没有结构体)。

从 C++ 标头来看,调用约定应该是 stdcall,并且 dll 中的每个导出函数都返回一个 long(在 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) 中。

这是反汇编(我添加的一些cmets):

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。

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

【问题讨论】:

  • 你看过这个问题吗? stackoverflow.com/questions/10852634/…
  • 你看过这个问题吗? stackoverflow.com/questions/10852634/…
  • 谢谢@Hans。这是有道理的,除了它是分配此句柄/指针事物而不是 .Net 的本机 dll。我们只是把它传回给我们的东西。无论我们从哪个 .Net 版本调用它,它都是针对同一个运行时库编译的。分配一个大于 4GB 的指针显然是一个问题,并且这个“句柄”的 dll 中使用的长数据类型存在问题,但我仍然不明白为什么它在 .Net 3.5 及更低版本中完全有效。

标签: c# c++


【解决方案1】:

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

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

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

编辑:由于代码最初使用纯整数类型,intptr_t 可能更安全,但理想的解决方案是对void* 使用 typedef,这样可以在任何地方工作并且更有意义。如果您发现使用 void* 没有发现任何问题,请改用它(仅用于句柄)。

【讨论】:

    【解决方案2】:

    如果我正确解释了反汇编,则此 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 的值来确认这一点。

    这也让我想起了这个blog post,它解释了在 Windows 7 和 8 之间更改的内存分配导致在 Windows 8 上分配的内存超过 4GB。所以这可能是导致此代码仅在某些情况下失败的另一个因素环境。

    【讨论】:

    • 我们尝试在 Windows 8 桌面(我的一位同事)和 Windows 10 桌面(我)上对此进行调试。鉴于 dll 在本机代码中分配此句柄,我仍然不明白为什么 .Net 3.5 及更低版本可以正常工作,但您所说的一切都是有道理的。
    【解决方案3】:

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

    这可能也不应该在 .NET 3.5 下工作,它只是靠运气工作。另外,我不知道 APIResult 是什么,所以我没有研究那部分。

    【讨论】:

      猜你喜欢
      • 2019-03-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多