为什么bit endianness是bitfields中的一个问题?

时间:2011-05-18 10:50:31

标签: c cross-platform portability low-level bit-fields

任何使用位域的可移植代码似乎都区分了小端和大端平台。有关此类代码的示例,请参阅declaration of struct iphdr in linux kernel。我无法理解为什么比特字节序是一个问题。

据我所知,bitfields纯粹是编译器构造,用于促进位级操作。

例如,请考虑以下位域:

struct ParsedInt {
    unsigned int f1:1;
    unsigned int f2:3;
    unsigned int f3:4;
};
uint8_t i;
struct ParsedInt *d = &i;
在这里,写d->f2只是一种简洁易懂的说法(i>>1) & (1<<4 - 1)

但是,无论架构如何,位操作都是明确定义的并且可以正常工作。那么,位域如何不便携?

7 个答案:

答案 0 :(得分:67)

根据C标准,编译器可以以任何随机的方式自由地存储位字段。您永远不会对位分配的位置做出任何假设。以下是C标准未指定的一些与字段相关的事情:

未指明的行为

  • 分配用于保存位字段(6.7.2.1)的可寻址存储单元的对齐。

实施定义的行为

  • 位字段是否可以跨越存储单元边界(6.7.2.1)。
  • 单位内的位域分配顺序(6.7.2.1)。

大/小端当然也是实现定义的。这意味着您的结构可以通过以下方式分配(假设16位整数):

PADDING : 8
f1 : 1
f2 : 3
f3 : 4

or

PADDING : 8
f3 : 4
f2 : 3
f1 : 1

or

f1 : 1
f2 : 3
f3 : 4
PADDING : 8

or

f3 : 4
f2 : 3
f1 : 1
PADDING : 8

适用哪一个?猜测一下,或者阅读编译器的深入后端文档。添加大端或小端的32位整数的复杂性。然后添加一个事实,即允许编译器在位字段内的任何位置添加任意数量的填充字节,因为它被视为结构(它不能在结构的最开头添加填充) ,但在其他地方)。

然后我甚至没有提到如果你使用普通的“int”作为位字段类型=实现定义的行为,或者你使用除(unsigned)int =实现定义的行为之外的任何其他类型会发生什么。 / p>

所以回答这个问题,没有便携式位域代码这样的东西,因为C标准对于如何实现位字段非常模糊。可以信任的唯一比特字段是布尔值的块,程序员不关心内存中位的位置。

唯一可移植的解决方案是使用逐位运算符而不是位字段。生成的机器代码将完全相同,但具有确定性。对于任何系统,按位运算符在任何C编译器上都是100%可移植的。

答案 1 :(得分:14)

  

据我了解,bitfields纯粹是编译器构造

这就是问题的一部分。如果位字段的使用仅限于编译器“拥有”的内容,那么编译器如何打包或排序它们对任何人来说都不是什么问题。

但是,位域可能更常用于模拟编译器域外部的构造 - 硬件寄存器,通信的“线”协议或文件格式布局。这些东西对如何布局位有严格的要求,并且使用位域来对它们进行建模意味着你必须依赖于实现定义的 - 更糟糕的是 - 编译器如何布局位域的未指定行为

简而言之,位字段的指定不足以使它们对它们最常用的情况有用。

答案 2 :(得分:9)

ISO / IEC 9899: 6.7.2.1 / 10

  

实施可以分配任何   可寻址的存储单元足够大   举行一场比赛。如果有足够的空间   仍然是一个立即的现场   跟随另一个比特场   结构应包装成   相同单元的相邻位。如果   没有足够的空间,无论是否   放入不适合的比特字段   下一个单位或相邻的重叠   单位是实施定义的。的的   比特字段的分配顺序   在一个单位内(从高阶到低阶   或者从低阶到高阶)   实施德网络定义。对齐   可寻址存储单元的   unspeci音响编

使用位移操作更安全,而不是在尝试编写可移植代码时对位字段排序或对齐做出任何假设,无论系统字节顺序或位数如何。

另见EXP11-C. Do not apply operators expecting one type to data of an incompatible type

答案 3 :(得分:6)

位字段访问是根据对底层类型的操作实现的。在示例中,unsigned int。所以如果你有类似的东西:

struct x {
    unsigned int a : 4;
    unsigned int b : 8;
    unsigned int c : 4;
};

当您访问字段b时,编译器访问整个unsigned int,然后移位并屏蔽相应的位范围。 (好吧,它不是必须,但我们可以假装它。)

在big endian上,布局将是这样的(最重要的一点):

AAAABBBB BBBBCCCC

在小端,布局将是这样的:

BBBBAAAA CCCCBBBB

如果你想从小端访问大端布局,反之亦然,你将不得不做一些额外的工作。这种可移植性的增加会降低性能,并且由于结构布局已经不可移植,因此语言实现者采用了更快的版本。

这做了很多假设。另请注意,大多数平台上都为sizeof(struct x) == 4

答案 4 :(得分:1)

根据机器的字节顺序,位字段将以不同的顺序存储,这在某些情况下可能无关紧要,但在其他情况下可能很重要。例如,您的ParsedInt结构表示通过网络发送的数据包中的标志,小端程序机器和大端机器以与发送字节不同的顺序读取这些标志,这显然是一个问题。

答案 5 :(得分:0)

回应最突出的要点:如果您在单个编译器/ HW平台上使用它作为仅软件构造,那么字节顺序将不是问题。如果您在多个平台上使用代码或数据或需要匹配硬件位布局,那么 IS 就是一个问题。专业软件的很多是跨平台的,因此必须要关心。

这是最简单的例子:我有将二进制格式的数字存储到磁盘的代码。如果我没有明确地逐字节地写入和读取这些数据到磁盘,那么如果从相反的端系统读取它将不是相同的值。

具体例子:

int16_t s = 4096; // a signed 16-bit number...

让我们说我的程序附带了我想要读入的磁盘上的一些数据。假设我想在这种情况下将其加载为4096 ...

fread((void*)&s, 2, fp); // reading it from disk as binary...

这里我把它读作16位值,而不是显式字节。 这意味着如果我的系统匹配存储在磁盘上的字节序,我得到4096,如果它没有,我得到16 !!!!!

因此,最常用的字节顺序是批量加载二进制数,然后如果你不匹配则执行bswap。在过去,我们将数据存储在磁盘上作为大端,因为英特尔是奇怪的人,并提供高速指令来交换字节。如今,英特尔非常普遍,在大端系统上经常使Little Endian成为默认值并进行交换。

较慢但以字节序结尾的中性方法是按字节进行所有I / O,即:

uint_8 ubyte;
int_8 sbyte;
int16_t s; // read s in endian neutral way

// Let's choose little endian as our chosen byte order:

fread((void*)&ubyte, 1, fp); // Only read 1 byte at a time
fread((void*)&sbyte, 1, fp); // Only read 1 byte at a time

// Reconstruct s

s = ubyte | (sByte << 8);

请注意,这与您编写的用于执行endian交换的代码相同,但您不再需要检查字节顺序。你可以使用宏来减少痛苦。

我使用了程序使用的存储数据的示例。 提到的另一个主要应用是写硬件寄存器,其中这些寄存器具有绝对排序。这出现的一个非常常见的地方是图形。获得字节错误,您的红色和蓝色通道会被颠倒!同样,问题在于可移植性 - 您可以简单地适应给定的硬件平台和图形卡,但如果您希望相同的代码在不同的机器上运行,则必须进行测试。

这是一个经典测试:

typedef union { uint_16 s; uint_8 b[2]; } EndianTest_t;

EndianTest_t test = 4096;

if (test.b[0] == 12) printf("Big Endian Detected!\n");

请注意,也存在位域问题,但它与字节序问题正交。

答案 6 :(得分:0)

只需指出-我们一直在讨论字节字节顺序的问题,而不是位域中的字节顺序或字节顺序,这会涉及另一个问题:

如果您正在编写跨平台代码,则永远不要只将结构作为二进制对象写出来。除了上述字节序问题外,编译器之间还可能存在各种打包和格式化问题。这些语言对编译器如何在实际内存中布置结构或位域没有任何限制,因此在保存到磁盘时,必须一次写入一个结构的每个数据成员,最好以字节中性的方式。

这种打包方式会影响位域中的“位字节顺序”,因为不同的编译器可能会将位域存储在不同的方向上,并且位字节顺序会影响如何提取它们。

因此请牢记问题的两个层次-字节字节序会影响计算机读取单个标量值(例如浮点数)的能力,而编译器(和生成参数)会影响程序读取聚合结构的能力。

我过去所做的是以中立的方式保存和加载文件,并存储有关数据在内存中的布局方式的元数据。这使我可以在兼容的情况下使用“快速简便”的二进制加载路径。