将 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 生成不正确(可能不安全)的存根。
我是怎么发现的?
我进行了一些测试:
- 我使用了 4 字节长的结构,而不是 8 字节长的结构。有效!
- 我没有使用 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 个字节(struct
或 long 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
变量并自己创建结构。
我正在尝试 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 生成不正确(可能不安全)的存根。
我是怎么发现的?
我进行了一些测试:
- 我使用了 4 字节长的结构,而不是 8 字节长的结构。有效!
- 我没有使用 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 个字节(struct
或 long 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
变量并自己创建结构。