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生成不正确(可能不安全)的存根。

我是怎么发现的?

我进行了一些测试:

  1. 我使用的是4字节长的结构,而不是8字节长的结构。 有用!
  2. 我没有使用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个字节( structlong long ),大多数C编译器在eax:edx返回它。 可能.NET的开发人员错过了这一点。 错误可能是有人写了size >= 8而不是size > 8

编辑:更糟糕的是,它将结果写在给定的指针上!

 before: 0x1111222244445555 after : 0x1234ABCD007BEF5C 

它将指针更改为返回值! 如您所见,调用后的第一个dword是0x1234ABCD(如在本机DLL中),第二个dword是指向值的指针,即给出的参数someVal

它更有趣,因为如果你将指针传递给StatusBlock结构 – 它实际上适用于这种特定情况(因为返回值中的第一个dword用作指针)

返回一个long变量并自己创建struct。