任何使用位域的可移植代码似乎都区分了小端和大端平台。有关此类代码的示例,请参阅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)
。
但是,无论架构如何,位操作都是明确定义的并且可以正常工作。那么,位域如何不便携?
答案 0 :(得分:67)
根据C标准,编译器可以以任何随机的方式自由地存储位字段。您永远不会对位分配的位置做出任何假设。以下是C标准未指定的一些与字段相关的事情:
未指明的行为
实施定义的行为
大/小端当然也是实现定义的。这意味着您的结构可以通过以下方式分配(假设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)
只需指出-我们一直在讨论字节字节顺序的问题,而不是位域中的字节顺序或字节顺序,这会涉及另一个问题:
如果您正在编写跨平台代码,则永远不要只将结构作为二进制对象写出来。除了上述字节序问题外,编译器之间还可能存在各种打包和格式化问题。这些语言对编译器如何在实际内存中布置结构或位域没有任何限制,因此在保存到磁盘时,必须一次写入一个结构的每个数据成员,最好以字节中性的方式。
这种打包方式会影响位域中的“位字节顺序”,因为不同的编译器可能会将位域存储在不同的方向上,并且位字节顺序会影响如何提取它们。
因此请牢记问题的两个层次-字节字节序会影响计算机读取单个标量值(例如浮点数)的能力,而编译器(和生成参数)会影响程序读取聚合结构的能力。
我过去所做的是以中立的方式保存和加载文件,并存储有关数据在内存中的布局方式的元数据。这使我可以在兼容的情况下使用“快速简便”的二进制加载路径。