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

时间:2009-09-29 01:07:11

标签: 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__));

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

7 个答案:

答案 0 :(得分:94)

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

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

答案 1 :(得分:41)

比特字段因编译器而异,对不起。

对于GCC来说,大端机器首先布置了大端,而小端机器首先将这些位置排列在最前端。

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

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

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

答案 2 :(得分:33)

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

  

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

您不能保证位字段是否会“跨越”int边界,并且您无法指定位域是从int的低端开始还是从int的高端开始(这与是否独立于处理器是big-endian或little-endian)。

首选bitmasks。使用内联(甚至宏)来设置,清除和测试位。

答案 3 :(得分:9)

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

答案 4 :(得分:6)

大多数情况下,可能,但不要在农场上打赌,因为如果你错了,你就会失败。

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

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

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

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

答案 5 :(得分:0)

感谢@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

答案 6 :(得分:-5)

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