将 C 结构编组为 C# 委托的 return 值

Marshalling of C Struct as return value of C# delegate

我正在尝试 return 从绑定到本机函数的委托中按值构造一个小的(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.

我怀疑我弄乱了托管类型注释,以至于 return 值没有被正确编组,但我看不出我做错了什么。下面是重现问题的小型本机测试 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);
    }
}

值得注意的是,如果委托的 return 类型是 Uint64,则应用程序在 .NET 2.0 和 4.0 情况下都按预期工作。但是,我不必这样做; StatusBlock 应该正确编组。

我在定位 .NET 4.0 时运气好吗?任何对我做错事的见解都将不胜感激。

这绝对是 .NET 的错误。

tl;博士

.NET Framework 2 生成不正确(可能不安全)的存根。

我是怎么发现的?

我进行了一些测试:

  1. 我使用了 4 字节长的结构,而不是 8 字节长的结构。有效!
  2. 我没有使用 x86,而是使用了 x64。有效!

发现它在所有其他情况下都有效,我决定使用 windbg 对其进行本机调试以查看它崩溃的位置(因为 Visual Studio 不允许我 "step in" 本机 call 与反汇编 window).

猜猜我发现了什么:

.NET 框架生成了一个正在调用 memcpy 的存根,当它试图复制到 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 函数被定义为 return 一个大于 8 字节的 struct 时,该函数实际上应该 return 一个指向局部函数的 struct 的指针stack 并且调用者应该使用 memcpy 将其复制到它自己的 stack (这是 C 规范的一部分,由编译器实现 - 程序员只需 "returns" a struct 和编译器做繁重的工作)。

然而,对于 8 个字节(structlong long),大多数 C 编译器 return 它在 eax:edx 中。 .NET 的开发人员可能已经忽略了这一点。错误可能是有人写了 size >= 8 而不是 size > 8...

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

before: 0x1111222244445555
after : 0x1234ABCD007BEF5C

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

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

解决方案

Return 一个 long 变量并自己创建结构。