簡體   English   中英

C Struct的編組作為C#委托的返回值

[英]Marshalling of C Struct as return value of C# delegate

我試圖通過綁定到本機函數的委托的值返回一個小的(8字節)結構,但是在針對.NET Framework 2.0時遇到以下錯誤(代碼似乎在定位4.0+時正常工作) :

testclient.exe中發生了未處理的“System.AccessViolationException”類型異常

附加信息:嘗試讀取或寫入受保護的內存。 這通常表明其他內存已損壞。

我懷疑我搞砸了托管類型注釋,以致返回值沒有被正確編組,但我看不出我做錯了什么。 下面是一個小型本機測試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。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM