我试图从绑定(bind)到 native 函数的委托(delegate)中按值返回一个小的(8 字节)结构,但在以 .NET Framework 2.0 为目标时遇到以下错误(代码似乎在以目标为目标时正常工作4.0+):
An unhandled exception of type 'System.AccessViolationException' occurred in testclient.exe
Additional information: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
我怀疑我弄乱了托管类型注释,以至于返回值没有被正确编码,但我看不出我做错了什么。下面是重现问题的小型 native 测试 DLL 和托管客户端的代码。
C/C++ Win32 (x86) 测试 DLL
//Natural alignment, blittable, sizeof(StatusBlock) == 8
struct StatusBlock{
std::uint32_t statusA;
std::uint32_t statusB;
};
/*
* When compiled this function stores the 64bit return value in the
* eax:edx register pair as expected.
*/
static StatusBlock __cdecl SomeFunction(std::uint32_t const someVal){
return StatusBlock{ someVal, 0x1234ABCD };
}
//Exported
extern "C" PVOID __stdcall GetFunctionPointer(){
return &SomeFunction;
}
C# 测试客户端
class Program
{
//Blittable, Marshal.SizeOf(typeof(StatusBlock)) == 8
[StructLayout(LayoutKind.Sequential)]
private struct StatusBlock
{
public UInt32 statusA;
public UInt32 statusB;
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate StatusBlock SomeFunction(UInt32 someVal);
[DllImport("testlib.dll",CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr GetFunctionPointer();
static void Main(string[] args)
{
var fnPtr = GetFunctionPointer();
System.Diagnostics.Debug.Assert(fnPtr != IntPtr.Zero);
var someFn = (SomeFunction)Marshal.GetDelegateForFunctionPointer(fnPtr, typeof(SomeFunction));
/*
* Crashes here with a System.AccessViolationException when targeting .NET Framework 2.0.
* Works as expected when targeting .NET Framework 4.0 +
*/
var statusBlock = someFn(22);
}
}
值得注意的是,如果委托(delegate)的返回类型是 Uint64
,那么应用程序在 .NET 2.0 和 4.0 中都可以正常工作。但是,我不必这样做; StatusBlock
应该正确编码。
我在定位 .NET 4.0 时运气好吗?如果能深入了解我做错了什么,我们将不胜感激。
最佳答案
这绝对是 .NET 的错误。
tl;dr
.NET Framework 2 生成不正确(可能不安全)的 stub 。
我是怎么发现这个的?
我进行了一些测试:
- 我使用了 4 字节长的结构,而不是 8 字节长的结构。有用!
- 我没有使用 x86,而是使用了 x64。有用!
发现它适用于所有其他情况,我决定使用 windbg 对其进行 native 调试以查看它崩溃的位置(因为 Visual Studio 不允许我“介入” native 调用
带有反汇编窗口)。
猜猜我发现了什么:
.NET 框架生成了一个调用 memcpy
的 stub ,当它试图复制到 edi
时失败了,当时它的值为 0x16 (= =22),这是C#代码中发送的参数!
那么,让我们看看如果我向函数发送一个有效指针会发生什么:
unsafe
{
long* ptr = &something;
uint ptr_value = (uint)ptr;
Console.WriteLine("Pointer address: {0:X}", (long)ptr);
var statusBlock = someFn(ptr_value);
Console.WriteLine("A: {0}", statusBlock.statusA);
Console.WriteLine("B: {0:X}", statusBlock.statusB);
}
输出:(当给出一个有效的指针时,它可以工作并且不会崩溃!)
Marshal.SizeOf(typeof(StatusBlock)) = 8
Running .NET Version 2
Pointer address: 49F15C
A: 0
B: 0
因此,我认为这是 .NET Framework 2 中无法解决的问题。
为什么会这样?
当一个 C 函数被定义为返回一个大于 8 字节的 struct
时,该函数实际上应该返回一个指向局部函数的 堆栈中的
并且调用者应该使用 struct
的指针memcpy
将其复制到它自己的 stack
(这是 C 规范的一部分,由编译器实现 - 程序员只需“返回”一个结构,编译器会完成繁重的工作。
然而,对于 8 个字节(struct
或 long long
),大多数 C 编译器在 eax:edx
中返回它。 .NET 的开发人员可能已经忽略了这一点。错误可能是有人写了 size >= 8
而不是 size > 8
...
编辑:更糟糕的是,它将结果写入给定的指针!
before: 0x1111222244445555
after : 0x1234ABCD007BEF5C
它将指针更改为返回值!如您所见,调用后的第一个 dword
是 0x1234ABCD(与 native DLL 中一样),第二个 dword
是指向值的指针,即参数 someVal
已给出!
更有趣的是,如果您将指针传递给 StatusBlock
结构 - 它实际上将适用于这种特定情况(因为返回值中的第一个 dword
用作指针)
解决方案
返回一个long
变量并自己创建结构。
关于c# - 将 C 结构编码为 C# 委托(delegate)的返回值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30363629/