C / C ++:强制位字段顺序和对齐


87

我读到,结构中位字段的顺序是特定于平台的。如果我使用不同的特定于编译器的打包选项,那该如何保证数据在写入时以正确的顺序存储?例如:

struct Message
{
  unsigned int version : 3;
  unsigned int type : 1;
  unsigned int id : 5;
  unsigned int data : 6;
} __attribute__ ((__packed__));

在具有GCC编译器的Intel处理器上,这些字段如图所示布置在内存中。Message.version是缓冲区的前3位,然后Message.type是。如果我为各种编译器找到了等效的struct包装选项,这是否可以跨平台使用?


17
由于缓冲区是一组字节而不是位,因此“缓冲区中的前3位”不是一个精确的概念。您会认为第一个字节的最低3位是前3位还是最高3位?
caf

2
在网络上传输时,“缓冲区的前3位”定义得非常好。
约书亚

2
@Joshua IIRC,以太网发送每个字节的最低位显著第一(这就是为什么广播位是它在哪里)。
tc。

当您说“便携式”和“跨平台”时,是什么意思?不管目标操作系统是什么,可执行文件都将正确访问订单-或-无论工具链是什么,代码都将编译?
加雷特·克拉伯恩

Answers:


103

不,它不是完全可移植的。结构的打包选项是扩展程序,它们本身并不完全可移植。除此之外,C99§6.7.2.1第10段说:“单位内位域的分配顺序(从高位到低位或从低位到高位)是实现定义的。”

例如,即使是单个编译器,也可能会根据目标平台的字节顺序以不同的方式安排位字段。


是的,例如,海湾合作委员会(GCC)特别指出,位域是根据ABI而不是根据实现来排列的。因此,仅停留在单个编译器上不足以保证排序。架构也必须检查。确实,可移植性有些噩梦。
underscore_d

10
为什么C标准不保证位字段的顺序?
亚伦·坎贝尔

7
很难一致且可移植地定义字节内的位“顺序”,更不用说可能跨越字节边界的位的顺序了。您所确定的任何定义都将与大量现有实践不符。
斯蒂芬·佳能

2
实现定义的允许特定于平台的优化。在某些平台上,位字段之间的填充可以改善访问,想象一下32位int中的四个7位字段:每8位对齐一次对于具有字节读取功能的平台来说是一项重大改进。
peterchen '16


45

抱歉,编译器之间的位字段差异很大。

使用GCC时,大型字节序机器首先将大端的位置进行排列,而小型字节序机器首先将小端的位置进行排列。

K&R说:“结构的相邻[bit]字段成员在与实现相关的方向上打包到与实现相关的存储单元中。当另一个字段之后的字段不适合时...可以在单元之间拆分,也可以将该单元拆分为填充。宽度为0的未命名字段强制执行此填充...”

因此,如果需要与机器无关的二进制布局,则必须自己完成。

由于填充,最后一条语句也适用于非位域-但是,所有编译器似乎都具有某种强制结构字节包装的方法,正如我所看到的,您已经为GCC找到了。


鉴于K&R确实是预先标准化的,并且真的已经在很多领域被取代了,所以它真的被认为是有用的参考吗?
underscore_d

1
我的K&R是ANSI后的。
约书亚

1
现在这很尴尬:我没有意识到他们发布了ANSI后版本。我的错!
underscore_d

35

应避免使用位域-即使对于同一平台,它们在编译器之间也不易移植。从C99标准6.7.2.1/10-“结构和联合说明符”(C90标准中有类似的措词):

一个实现可以分配任何足够大的可寻址存储单元来容纳位域。如果有足够的空间,应将紧随结构中另一个位域的位域打包到同一单元的相邻位中。如果剩余空间不足,则将实现不适当的位字段放入下一个单元还是与相邻单元重叠。单位内位域的分配顺序(从高位到低位或从低位到高位)是实现定义的。未指定可寻址存储单元的对齐方式。

您无法保证位字段是否会“跨越”一个int边界,也无法指定位字段是从int的低端还是int的高端开始(这与处理器是大端或小端)。

最好使用位掩码。使用内联(甚至宏)设置,清除和测试这些位。


2
位域的顺序可以在编译时确定。
格雷格·伍兹

9
同样,当处理位标记在程序外部(即在磁盘上或在寄存器中或在其他程序访问的存储器中)没有外部表示时,位字段也是高度首选的。
格雷格·伍兹

1
@ GregA.Woods:如果确实是这种情况,请提供描述方式的答案。谷歌搜索时,除了您的评论,我什么都找不到……
mozzbozz 2014年

1
@ GregA.Woods:对不起,应该写我提到的评论。我的意思是:您说“可以在编译时确定位域的顺序”。我对此无能为力。
mozzbozz 2015年

2
@mozzbozz看一看planix.com/~woods/projects/wsg2000.c和搜索的定义和使用_BIT_FIELDS_LTOH以及_BIT_FIELDS_HTOL
格雷戈A.伍兹

11

endianness在谈论字节顺序而不是位顺序。如今,有99%的确定位顺序是固定的。但是,在使用位域时,应考虑字节序。请参见下面的示例。

#include <stdio.h>

typedef struct tagT{

    int a:4;
    int b:4;
    int c:8;
    int d:16;
}T;


int main()
{
    char data[]={0x12,0x34,0x56,0x78};
    T *t = (T*)data;
    printf("a =0x%x\n" ,t->a);
    printf("b =0x%x\n" ,t->b);
    printf("c =0x%x\n" ,t->c);
    printf("d =0x%x\n" ,t->d);

    return 0;
}

//- big endian :  mips24k-linux-gcc (GCC) 4.2.3 - big endian
a =0x1
b =0x2
c =0x34
d =0x5678
 1   2   3   4   5   6   7   8
\_/ \_/ \_____/ \_____________/
 a   b     c           d

// - little endian : gcc (Ubuntu 4.3.2-1ubuntu11) 4.3.2
a =0x2
b =0x1
c =0x34
d =0x7856
 7   8   5   6   3   4   1   2
\_____________/ \_____/ \_/ \_/
       d           c     b   a

6
a和b的输出表明字节序仍在讨论位顺序和字节顺序。
Windows程序员

比特排序和字节排序问题的一个很好的例子
Jonathan

1
您实际上编译并运行了代码吗?对我来说,“ a”和“ b”的值似乎不合逻辑:您基本上是说,由于字节顺序,编译器将在一个字节内交换半字节。在“ d”的情况下,字节序不应影响char数组中的字节顺序(假定char为1字节长);如果编译器这样做,我们将无法使用指针遍历数组。另一方面,如果您使用了两个16位整数的数组,例如:uint16 data [] = {0x1234,0x5678}; 那么在小端系统中d肯定为0x7856。
Krauss

6

可能大多数时候,但不要在农场上打赌,因为如果您错了,您将损失惨重。

如果确实需要完全相同的二进制信息,则需要创建带有位掩码的位域-例如,对Message使用无符号短(16位),然后使用诸如versionMask = 0xE000之类的东西来表示三个最高位。

结构中的对齐方式也存在类似的问题。例如,Sparc,PowerPC和680x0 CPU都是big-endian,Sparc和PowerPC编译器的常见默认设置是在4字节边界上对齐结构成员。但是,我用于680x0的一个编译器仅在2字节边界上对齐-并且没有选择来更改对齐方式!

因此,对于某些结构,Sparc和PowerPC上的大小是相同的,但在680x0上较小,并且某些成员在该结构内的内存偏移量不同。

这是我从事的一个项目的问题,因为在Sparc上运行的服务器进程将查询客户端,并发现它是big-endian,并假定它可以将二进制结构喷射到网络上,而客户端可以应付。而且在PowerPC客户端上运行良好,在680x0客户端上崩溃了很多。我没有编写代码,并且花了很长时间才发现问题。但是一旦完成,就很容易修复。


1

感谢@BenVoigt对您的非常有用的评论开始

不,创建它们是为了节省内存。

Linux源代码确实使用位字段来匹配外部结构:/usr/include/linux/ip.h对于IP数据报的第一个字节具有此代码

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8    ihl:4,
                version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
        __u8    version:4,
                ihl:4;
#else
#error  "Please fix <asm/byteorder.h>"
#endif

但是,鉴于您的评论,我正在放弃尝试使此功能适用于多字节位字段frag_off的问题


-9

当然,最好的答案是使用一个将位字段读/写为流的类。只是不能保证使用C位字段结构。更不用说在现实世界的编码中使用它是不专业/懒惰/愚蠢的。


5
我认为这是错误的状态,这是愚蠢的使用位字段,因为它提供了一个非常干净的方式来表示硬件寄存器,它的成立是为了模型,在C
trondd

13
@trondd:不,它们是为了节省内存而创建的。位字段无意映射到外部数据结构,例如内存映射的硬件寄存器,网络协议或文件格式。如果打算将它们映射到外部数据结构,则包装顺序将被标准化。
Ben Voigt

2
使用位可以节省内存。使用位字段可提高可读性。使用较少的内存速度更快。使用位允许更复杂的原子操作。在现实世界中的应用程序中,需要性能和复杂的原子操作。这个答案对我们不起作用。
johnnycrash

@BenVoigt可能是正确的,但是如果程序员愿意确认他们的编译器/ ABI的顺序符合他们的需求,并相应地牺牲了快速的可移植性,那么他们当然可以履行这一职责。至于9 *,哪个权威的“现实世界编码人员”认为位域的所有使用都是“非专业/懒惰/愚蠢的”,他们在哪里指出?
underscore_d

2
使用更少的内存并不总是更快。通常,使用更多的内存并减少读取后的操作会更有效,并且处理器/处理器模式可以使这种情况更加真实。
戴夫牛顿
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.