C Struct的编组作为C#委托的返回值
我试图通过绑定到本机函数的委托的值返回一个小的(8字节)结构,但是在面向.NET Framework 2.0时遇到以下错误(代码似乎在定位4.0+时正常工作) :
testclient.exe中发生了未处理的“System.AccessViolationException”类型exception
附加信息:尝试读取或写入受保护的内存。 这通常表明其他内存已损坏。
我怀疑我搞砸了托管类型注释,这样返回值没有被正确编组,但我看不出我做错了什么。 下面是一个小型本机测试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); } }
值得注意的是,如果委托的返回类型是Uint64
则应用程序在.NET 2.0和4.0情况下都按预期工作。 但是,我不应该这样做; StatusBlock
应该正确编组。
在针对.NET 4.0时我是否幸运? 任何洞察我做错的事情都会非常感激。
它无论如何都是.NET中的错误。
TL;博士
.NET Framework 2生成不正确(可能不安全)的存根。
我是怎么发现的?
我进行了一些测试:
- 我使用的是4字节长的结构,而不是8字节长的结构。 有用!
- 我没有使用x86,而是使用了x64。 有用!
确定它适用于所有其他情况,我决定使用windbg对其进行原生调试以查看它崩溃的位置(因为Visual Studio不允许我使用反汇编窗口“介入”本机call
)。
猜猜我发现了什么:
.NET框架生成了一个调用memcpy
的存根,当它尝试复制到edi
时失败,当时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
,该函数实际上应该返回一个指向本地函数stack
struct
的指针,调用者应该使用memcpy
将它复制到它自己的stack
(这是C的一部分)规范并由编译器实现 – 程序员只需“返回”一个结构,编译器就可以完成繁重的工作。
但是,对于8个字节( struct
或long long
),大多数C编译器在eax:edx
返回它。 可能.NET的开发人员错过了这一点。 错误可能是有人写了size >= 8
而不是size > 8
…
编辑:更糟糕的是,它将结果写在给定的指针上!
before: 0x1111222244445555 after : 0x1234ABCD007BEF5C
它将指针更改为返回值! 如您所见,调用后的第一个dword
是0x1234ABCD(如在本机DLL中),第二个dword
是指向值的指针,即给出的参数someVal
!
它更有趣,因为如果你将指针传递给StatusBlock
结构 – 它实际上适用于这种特定情况(因为返回值中的第一个dword
用作指针)
解
返回一个long
变量并自己创建struct。