【发布时间】: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),我认为这是正确的,而不是问题。
我要尝试的一些事情:
- 针对 .Net 3.5 运行调试并尝试查看有什么不同。
- 为 dll 创建 C++/cli 包装器,而不是使用 PInvoke。
如果有人能发现这里发生的事情或给我任何想法,那就太好了。
【问题讨论】:
-
你看过这个问题吗? stackoverflow.com/questions/10852634/…
-
你看过这个问题吗? stackoverflow.com/questions/10852634/…
-
谢谢@Hans。这是有道理的,除了它是分配此句柄/指针事物而不是 .Net 的本机 dll。我们只是把它传回给我们的东西。无论我们从哪个 .Net 版本调用它,它都是针对同一个运行时库编译的。分配一个大于 4GB 的指针显然是一个问题,并且这个“句柄”的 dll 中使用的长数据类型存在问题,但我仍然不明白为什么它在 .Net 3.5 及更低版本中完全有效。