c# - 将 C 结构编码为 C# 委托(delegate)的返回值

标签 c# .net c interop marshalling

我试图从绑定(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 。

我是怎么发现这个的?

我进行了一些测试:

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

发现它适用于所有其他情况,我决定使用 windbg 对其进行 native 调试以查看它崩溃的位置(因为 Visual Studio 不允许我“介入” native 调用 带有反汇编窗口)。

猜猜我发现了什么: enter image description here

.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 个字节(structlong 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/

相关文章:

c - 从 double 的格式说明符 '%g' 开始以指数格式打印

c# - String.Format 一个整数以在丹麦文化中使用带有十进制值的千位分隔符

c# - .NET 中的自动问答 (FAQ)

c# - 保存到 Active Directory - 拒绝域管理员访问

C# - 从自定义应用程序读取嵌套事件日志

计算整个字符串,包括 C 中的空格

java - 以明文形式查看 UDP 消息时出现问题

c# - 将 Serilog 与 Caliburn Micro .Net Core 3.1 结合使用

c# - 使用反射的对象实例化适用于 VB.NET,但不适用于 C#

asp.net - PayPal 401 未经授权在现场生产中使用 .NET REST API 进行快速结帐 API 的信用卡付款