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

dew*_*ald 81 c c++ bit-manipulation endianness bit

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

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

在具有GCC编译器的英特尔处理器上,字段在显示时显示在内存中.Message.version是缓冲区中的前3位,然后Message.type是.如果我找到各种编译器的等效结构包装选项,这将是跨平台的吗?

Ste*_*non 97

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

例如,即使是单个编译器也可能根据目标平台的字节顺序不同地放置位字段.

  • 为什么C标准不能保证位字段的顺序? (6认同)
  • 这是很难一致的和可移植定义字节中位的"秩序",更可能跨越字节边界位的顺序.你所定义的任何定义都不符合相当数量的现有做法. (6认同)
  • 是的,例如,GCC 特别指出位字段是根据 ABI 排列的,而不是根据实现排列的。因此,仅仅停留在单个编译器上并不足以保证顺序。架构也必须进行检查。对于便携性来说,这确实是一场噩梦。 (3认同)
  • implementaiton-defined允许特定于平台的优化.在某些平台上,位字段之间的填充可以改善访问,想象一下32位int中的四个七位字段:每隔8位对齐它们对于具有字节读取的平台是一个重大改进. (2认同)

Jos*_*hua 42

从编译器到编译器,位字段差异很大,抱歉.

对于GCC,大端机器首先布置了大端,而小端机器首先布置了小端.

K&R说"结构的相邻[bit-]字段成员在依赖于实现的方向上被打包到依赖于实现的存储单元中.当另一个字段后面的字段不适合时...它可以在单元之间分割,或者单元可以是宽度为0的未命名字段强制填充......"

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

最后一个语句也适用于填充引起的非位域 - 但是所有编译器似乎都有一些强制结构字节打包的方法,正如我在GCC中发现的那样.

  • 我的 K&R 是后 ANSI 的。 (2认同)
  • 现在这很尴尬:我没有意识到他们发布了 ANSI 后的修订版。我的错! (2认同)

Mic*_*urr 33

应避免使用位域 - 即使在同一平台上,它们在编译器之间也不是很容易移植.来自C99标准6.7.2.1/10 - "结构和联合说明符"(C90标准中有类似的措辞):

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

你不能保证位字段是否会"跨越"一个int边界,你不能指定一个位域是从int的低端开始还是从int的高端开始(这与处理器是否是独立的无关) big-endian或little-endian).

喜欢位掩码.使用内联(甚至宏)来设置,清除和测试位.

  • 此外,当处理在程序外没有外部表示的位标志时(即在磁盘上或寄存器中或在由其他程序访问的存储器中等),位域是高度优选的. (7认同)
  • 可以在编译时确定位域的顺序. (2认同)
  • @mozzbozz查看http://www.planix.com/~woods/projects/wsg2000.c并搜索`_BIT_FIELDS_LTOH`和`_BIT_FIELDS_HTOL`的定义和用法 (2认同)

pie*_*fou 9

字节序正在谈论字节顺序而不是位顺序.如今,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
Run Code Online (Sandbox Code Playgroud)

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

Bob*_*phy 6

大多数时候,可能是,但不要把农场押在上面,因为如果你错了,你就会失去巨大的.

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

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

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

这是一个项目,我的工作就一个问题,因为运行于Sparc服务器进程将查询客户机,并找出它是大端,并假设它可能只是喷二元结构在网络上和客户端可以应付.这在PowerPC客户端上运行良好,并且在680x0客户端上大量崩溃.我没有编写代码,并且花了很长时间才找到问题.但是一旦我这么做就很容易解决.