Marshal.SizeOf和sizeof的区别,我就是不明白

9

迄今为止,我一直认为Marshal.SizeOf是计算非托管堆上可平坦化结构体的内存大小的正确方式(这似乎是SO和几乎所有其他网站上的共识)。

但在阅读了一些有关Marshal.SizeOf的注意事项之后(此文章中“但有一个问题…”之后),我尝试了一下,现在完全感到困惑:

public struct TestStruct
{
    public char x;
    public char y;
}

class Program
{
    public static unsafe void Main(string[] args)
    {
        TestStruct s;
        s.x = (char)0xABCD;
        s.y = (char)0x1234;

        // this results in size 4 (two Unicode characters)
        Console.WriteLine(sizeof(TestStruct));

        TestStruct* ps = &s;

        // shows how the struct is seen from the managed side... okay!      
        Console.WriteLine((int)s.x);
        Console.WriteLine((int)s.y);

        // shows the same as before (meaning that -> is based on 
        // the same memory layout as in the managed case?)... okay!
        Console.WriteLine((int)ps->x);
        Console.WriteLine((int)ps->y);

        // let's try the same on the unmanaged heap
        int marshalSize = Marshal.SizeOf(typeof(TestStruct));
        // this results in size 2 (two single byte characters)
        Console.WriteLine(marshalSize);

        TestStruct* ps2 = (TestStruct*)Marshal.AllocHGlobal(marshalSize);

        // hmmm, put to 16 bit numbers into only 2 allocated 
        // bytes, this must surely fail...
        ps2->x = (char)0xABCD;
        ps2->y = (char)0x1234;

        // huh??? same result as before, storing two 16bit values in 
        // only two bytes??? next will be a perpetuum mobile...
        // at least I'd expect an access violation
        Console.WriteLine((int)ps2->x);
        Console.WriteLine((int)ps2->y);

        Console.Write("Press any key to continue . . . ");
        Console.ReadKey(true);
    }
}

这里出了什么问题?字段解引用操作符'->'假定什么内存布局?'->'是否是寻址非托管结构的正确操作符?或者Marshal.SizeOf是非托管结构的错误大小操作符?
我没有找到任何用我理解的语言解释这个问题,除了“...结构布局是无法发现的...”和“...在大多数情况下...”这种含糊不清的东西。

"Marshal.SizeOf是计算非托管堆上可平移结构体的内存大小的正确方法" - 如果尺寸是以.NET术语测量的,我会默认使用Unsafe.SizeOf<T>(); 如果这给您带来了困扰,我很抱歉。 - Marc Gravell
我认为这篇文章可以解决你的疑虑 https://www.codeproject.com/Articles/97711/sizeof-vs-Marshal-SizeOf - Khoa Nguyen
5
该结构体没有指定CharSet属性的[StructLayout]特性。默认值是CharSet.Ansi,而原生C和C++代码经常会犯这种错误。在世界其他地方开始使用个人计算机之前,使用1字节存储字符是可行的,在字符集方面也拖了一段时间。重点是Marshal.SizeOf告诉你何时与本地代码进行交互时会发生什么,而sizeof则不会。如果您的代码破坏了内存,则会导致出现不会立即崩溃程序的错误,这就是“unsafe”的含义。 - Hans Passant
5
无论如何,应用[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)],现在结构体声明具有一个非常理想的属性,即“可平铺性”。 非托管布局与托管布局完全相同,这使得pinvoke非常有效。 Marshal.SizeOf() 执行您预期的操作。 另一个使用这种技术的地方是试图使 System.IO.MemoryMappedFiles 命名空间变得高效的代码。 - Hans Passant
4个回答

7
区别在于:sizeof运算符需要一个类型名称,并告诉您要为该结构的实例分配多少字节的托管内存。这不一定是堆栈内存;当它们是数组元素、类字段等时,结构体会被分配到堆上。相比之下,Marshal.SizeOf接受类型对象或类型的实例,并告诉您需要分配多少字节的非托管内存。由于各种原因,这些可能不同。类型名称给您一个提示:Marshal.SizeOf旨在在将结构体驱动到非托管内存时使用。
另一个差异在于,sizeof运算符只能使用未管理的类型名称;即,仅其字段是整数类型、布尔值、指针等的结构体类型。(有关精确定义,请参阅规范。)相比之下,Marshal.SizeOf可以使用任何类或结构体类型。

1
是的,没错,这是我已经在各处阅读到的。但这并不能解释为什么我可以在分配的16位内存中存储(总共)32位值。听起来太好了,但也许我应该把它当作一个商业模型去探索...;-) - oliver
2
@oliver 现在尝试使用 Unsafe.SizeOf<TestStruct>()sizeof(TestStruct);它们会返回 4 - Marc Gravell
@oliver “正确”的方式是有上下文的;如果你说的是你的类型如何映射到托管代码,那么:当然;如果你说的是你的类型如何映射到 P/Invoke:那么:请问 Marshal - Marc Gravell
@MarcGravell:好的,我想我明白了。我知道任何我传递结构体给的C/C++代码可能会假设不同的内存布局。但我的问题只与理解C#部分有关。 - oliver
2
@oliver 访问冲突很棘手;我不确定能够保证发现每种情况 - 非托管内存本质上就是这样令人头疼 :) - Marc Gravell
显示剩余2条评论

3
我认为你仍未解决的问题是你特定情况下正在发生的事情:
&ps2->x
0x02ca4370  <------
    *&ps2->x: 0xabcd 'ꯍ'
&ps2->y
0x02ca4372  <-------
    *&ps2->y: 0x1234 'ሴ'

你正在写入和读取(可能)未分配的内存。由于你所处的内存区域,这是不会被检测到的。
在我的系统上,以下操作将复现预期的行为(但你的情况可能有所不同):
  TestStruct* ps2 = (TestStruct*)Marshal.AllocHGlobal(marshalSize*10000);

  // hmmm, put to 16 bit numbers into only 2 allocated 
  // bytes, this must surely fail...
  for (int i = 0; i < 10000; i++)
  {
    ps2->x = (char)0xABCD;
    ps2->y = (char)0x1234;
    ps2++;
  }

每当我过去超出分配的内存边界时,都会收到访问冲突的错误。但也可能有些情况我甚至没有注意到...;-) 所以更理解我想尽可能澄清在不安全世界中正在发生的事情。 - oliver
@Oliver -- 请看我的编辑。这样做旨在 1) 防止 AllocHGlobal 在内存对齐方面进行舍入,以保证更好的访问效率,并且 2) 更好地确保检测到您的访问冲突。 - zzxyz
有趣的是,无论我是从IDE还是从资源管理器启动应用程序,它都会抛出StackOverflowException或OutOfMemoryException异常。我认为这表明内存破坏部门发生了严重的问题。;-) - oliver
有趣。我不会期望那样。我得到的是:在ConsoleApplication3.exe中,0x7760A879处发生未处理的异常(ntdll.dll):0xC0000374:堆已损坏(参数:0x77645910)。不管怎样... 我想这就是为什么它是"未定义"的原因 :) - zzxyz
奇怪...是我的Windows 10出了问题吗?还是我已经成为了Spectre/Meltdown漏洞的受害者,导致我的内存出现了问题?开个玩笑... - oliver
1
@oliver - 每天学点新东西。我的程序堆栈非常接近分配的堆指针。所以...是的,堆栈损坏...毫不奇怪。 - zzxyz

2

char 默认情况下会转换为 ANSI 字节。这使得它可以与大多数 C 库进行互操作,并且对于 .NET 运行时的操作是基本的。

我认为正确的解决方案是将 TestStruct 更改为:

public struct TestStruct
{
    [System.Runtime.InteropServices.MarshalAs(UnmanagedType.U2)]
    public char x;
    [System.Runtime.InteropServices.MarshalAs(UnmanagedType.U2)]
    public char y;
}

UnmanagedType.U2 表示无符号的 2 字节长整型,这使它等同于 C 头文件中的 wchar_t 类型。

通过注意细节,可以实现将 C 结构平滑地移植到 .NET,这为与本地库的互操作打开了许多大门。


1
“->”字段解引用操作符假定什么内存布局?
由CLI决定。
寻址非托管结构体是否使用“->”操作符是正确的?
这是一个模糊的概念。在未受管控内存中,有通过CLI访问的结构体:这些遵循CLI规则。还有一些结构体仅仅是为了给访问同一内存的未受管代码(例如C/C++)起个别名。这遵循那个框架的规则。编组通常指P/Invoke,但这不一定适用于此处。
对于未受管结构体,“Marshal.SizeOf”是否是错误的大小运算符?
我会默认使用Unsafe.SizeOf<T>,它本质上是sizeof(T)——在CLI/IL中非常明确定义(包括填充规则等),但在C#中不可能。

我不明白"->"运算符在应用于不安全指针时如何会产生歧义。无论指向的内存如何分配,任何不安全指针都是相同的,不是吗?而且当我只分配了16位内存时,我的32位数据去哪了呢? - oliver
@oliver 你说你分配了16位?CLI报告那个是4字节。忽略Marshal:那只是在谈论P/Invoke,而你并没有做P/Invoke。当我说“不明确”时,我的意思是“非托管结构”是不明确的;->运算符将遵循CLI规则,并且在相同的规则下是完全定义良好的,这些规则表明它是4字节。 - Marc Gravell
我曾经认为将marshalSize(==2)传递给Marshal.AllocHGlobal会分配2个字节的内存...这不是真的吗? - oliver
如果你将 2 传递给 Marshal.AllocHGlobal,那么 a: 是的,你分配了 2 个字节,b: 该死,不要这样做 - 没必要为了 2 个字节担心非托管分配器,c: 如果内存没有标记为无效,那是你自己的问题,因为你没有请求足够的内存,而且 (d?) 如果内存标记为无效也是你的问题:最终,一旦以任何方式使用 unsafe,你 显式 承担任何错误的责任。 - Marc Gravell
没有问题,这是我的错误。我不会责怪C#。但是为了能够承担责任,我需要理解发生了什么。在这方面,我缺少一些容易获取的信息。我的意思是,内存布局并不是什么高深的科学,我想。 - oliver
1
@oliver 好的,说到 C# 的话就可以信任 sizeof(Foo) 或者 Unsafe.SizeOf<Foo>()(在 IL 语言里它们是相同的东西,只是 sizeof 不能用于泛型)。 - Marc Gravell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接