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

Leo*_*d99 54 c portability cross-platform low-level bit-fields

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

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

例如,考虑以下位域:

struct ParsedInt {
    unsigned int f1:1;
    unsigned int f2:3;
    unsigned int f3:4;
};
uint8_t i;
struct ParsedInt *d = &i;
只是一种简洁易懂的说法d->f2.

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

Lun*_*din 72

根据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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 同时,bitfield通常与pragma一起用来告诉编译器不要使用填充(即使没有效率来执行CPU的所需对齐),编译器行为也不是愚蠢的.上述两个原因的结果:只剩下2个案例,一个用于大端机器,一个用于小端.这就是为什么在低级头文件中只能获得2个版本的原因. (2认同)
  • @ xryl669但是,当您拥有一个版本的100%可移植文件时,为什么要使用两个版本的完全不可移植的文件?两种情况都将导致相同的机器代码。 (2认同)
  • @ xryl669代码的问题不是按位运算符,而是“魔术数”的使用。它应该被写成`s [0] = VERSION | 人道法;`。从理论上讲,位字段是一个好主意,但是C标准完全无法支持它们。以我的经验,使用位字段的代码更容易出错,因为使用它们的程序员总是对位字段做出很多隐式假设,而实际上并不能保证这些假设。 (2认同)
  • @ xryl669相反,如果像我每天使用嵌入式编程一样每天进行此操作,则位操作将变得非常琐碎。您可以通过`s [0] = VERSION |解决问题 IHL_SET(val);`IHL_SET是一个简单的宏:#define IHL_SET(x)((x << IHL_OFFSET)&IHL_MASK)。(遮罩是可选的)。花了我10秒钟时间写,没有任何努力。 (2认同)

Mic*_*urr 16

据我所知,bitfields纯粹是编译器构造

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

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

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


miz*_*izo 9

ISO/IEC 9899: 6.7.2.1/10

实现可以分配足够大的任何可寻址存储单元以保持比特字段.如果剩余足够的空间,则紧跟在结构中另一个位字段之后的位字段将被打包到同一单元的相邻位中.如果剩余的空间不足,则是否将不适合的位场放入下一个单元或重叠相邻单元是实现定义的.在一个单元内(高阶到低阶或低阶到高阶)的位字分配顺序是实现定义的.可寻址存储单元的对齐未指定.

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

另见EXP11-C.不要将期望一种类型的运算符应用于不兼容类型的数据.


Die*_*Epp 6

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

struct x {
    unsigned int a : 4;
    unsigned int b : 8;
    unsigned int c : 4;
};
Run Code Online (Sandbox Code Playgroud)

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

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

AAAABBBB BBBBCCCC
Run Code Online (Sandbox Code Playgroud)

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

BBBBAAAA CCCCBBBB
Run Code Online (Sandbox Code Playgroud)

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

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

  • 事实并非如此,C标准既没有指定字段的字节顺序也没有位字节的位顺序.编译器可以随意在任何地方分配这些位. (5认同)
  • @Lundin:我不是在谈论C标准,我在谈论C标准的实现. (4认同)
  • 你能详细说明你是如何提出 BBBBAAAA CCCCBBB 的吗? (2认同)