PInvoke x64与.Net 4.0一起崩溃
PInvoke x64 crash with .Net 4.0
我的任务是让一些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(,我认为这是正确的,而不是问题所在。
我要尝试的一些东西:
- 调试在.Net 3.5上运行,并尝试查看有什么不同
- 为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
(进行传递。
这是基于以下对拆卸的分析:
- 您传入句柄值
e82d0080
- DLL获取该句柄并将其转换为64位值
- 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是什么,所以我没有研究那个部分。
- 当回溯以零开始时,如何调试崩溃
- 内联映射初始化的动态atexit析构函数崩溃
- 执行函数时导致崩溃的变量
- 如何将enable-if与模板参数和参数包一起使用
- 如何将PERF_AMPLE_READ与mmap一起使用
- 如何将两个不同矢量的同一位置的两个元素组合在一起
- 程序崩溃并显示"std::out_of_range"错误
- 如何将C++中的库和头与MinGW一起使用
- 为什么gmp会在这里与"invalid next size"重新定位一起崩溃?
- C++ 将 setmode 与 _O_U8TEXT 一起使用来处理 unicode 时崩溃
- QT项目在配置文件上崩溃,与发布和调试器一起运行
- 当程序与符号名称一起崩溃时,输出呼叫堆栈
- C++无锁队列与多个线程一起崩溃
- 与boost::property_tree XML解析器一起使用时,boost::协同程序库崩溃
- 自定义分配器有时会与stl向量一起崩溃
- 将GSL与Qt一起使用时崩溃
- 与shared_ptr一起崩溃
- PInvoke x64与.Net 4.0一起崩溃
- Strcpy与strchr一起使用时崩溃
- 基本的OpenGL程序崩溃,但与gdb一起工作