何时会出现结构包装问题

When will struct packing issues crop up?

本文关键字:包装 问题 结构 何时会      更新时间:2023-10-16

我一直在阅读有关结构位打包顺序问题的文章,但由于暴露的内容有限,我自己没有遇到过。然而,我注意到,这些讨论大多是针对非常复杂的应用。

我现在正在写一个结构来保存来自ifstream的信息,比如

struct MyFileStruct
{
    char data1[40];
    int data2;
    char data3[12];
    // etc..
};
ifstream fin;
// .. snip ..
fin.read((char*)&myfilestruct, sizeof(MyFileStruct));

只是想一想,在这个简单的场景中,是否会出现任何问题,可能是在另一个操作系统或32/64位架构中。等等。那么,确切地说,什么时候会考虑比特包装订单?

对于该特定结构,您最有可能遇到的问题是endian-ness。在小端序系统上,int的最低寻址字节包含最低有效8位。在大端序系统中,最高有效的8位。

因此,如果您将该结构的字节写入一种系统上的文件,将该文件传输到另一种系统,并将其读回,那么您将在data2中看到不同的值。

不过,您可能会在其他结构或不寻常的系统/编译器中遇到其他问题:

  • 基本类型的大小-int通常是4字节,但不是必须的。long在不同的常见系统上有不同的大小(在Windows上为4字节,在64位Linux上为8字节)。显然,如果您试图从文件中读取结构,并且期望其他C++实现实际编写的字节数不同,那么您就有问题了
  • padding-允许编译器将未使用的字节放入成员之间的结构中。这通常是为了确保对齐。例如,在许多编译器中,int成员的偏移量总是4的倍数。由于在结构中,40是4的倍数,这不会有任何区别,但如果第一个数组是39个字节,那么对齐int的实现将插入一个未使用的字节,而不对齐的实现则不会插入。在一些CPU(例如x86)上,对齐int是有帮助的,但不是必须的,在这种情况下,编译器通常有方法对结构进行注释,以决定是否填充它

由于存在这些类型的差异,通常将结构直接写入文件(或套接字)是不合法的。在特定的情况下,无论谁读取它,都有与该结构完全相同的内存表示,这意味着如果你首先准确地计算出哪些字节去了哪里,意味着什么意思,然后确保所有需要读取/写入文件的程序都能使用该格式,你就可以做到这一点。

如果序列化一个结构数组,就会遇到问题。假设结构大小为12,但它被压缩到4字节边界或8字节边界。如果磁盘上有一个由2个元素组成的数组,则第一个元素将从偏移量0开始。第二个将从位置12开始(如果您要打包到4字节边界)或从位置16开始(如果要打包到8字节边界)。因此,当您读取数组时,第一个元素会正确出现,但第二个元素(以及随后的元素)可能会出错。

请注意,在VisualStudio中,32位和64位编译的默认打包都是8字节边界,所以您可能很幸运,不会遇到问题。但是,如果您想看到这一点,请将32位编译设置为与4字节边界对齐进行编译,将64位编译设置成与8字节边界对齐(例如)。然后创建一个结构数组,如前一段所述。

包装规则(以及类似的endianness)可能会成为一个考虑因素,包括您的示例,当

  • 该程序是用不同的编译器编译的
  • 该程序是用同一编译器的不同版本编译的(理论上)
  • 该程序使用不同的编译器选项进行编译,包括但不限于更改目标操作系统、目标硬件或32/64位设置
  • 编译器指令被添加到源代码中,例如#pragma pack

一个安全的一般规则是,只有当读取结构的可执行文件是由同一可执行文件编写的时,才能保证您的代码正常工作。

当这是一个问题时,打包问题(而不是端序)的一个常见解决方案是使用非标准编译器指令以牺牲效率为代价删除打包。

这可以使用适用于Microsoft编译器的pragma pack和适用于gcc的__attribute__ ((__packed__))来完成。

一般规则是,您可以读取使用同一编译器编译的代码编写的文件(其中包括编译器选项)。最简单的形式是一个程序,它写出二进制数据,以便以后可以将其读回。除此之外,你还需要了解特定于实现的行为,没有简单的答案。