91

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

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然后是。如果我找到各种编译器的等效结构打包选项,这将是跨平台的吗?

4

7 回答 7

110

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

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

于 2009-09-29T01:31:06.957 回答
47

位字段因编译器而异,抱歉。

使用 GCC,大端机器首先布置位大端,而小端机器首先布置位小端。

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

因此,如果您需要机器独立的二进制布局,您必须自己做。

由于填充,最后一条语句也适用于非位域 - 但是所有编译器似乎都有某种方式强制对结构进行字节打包,正如我看到你已经为 GCC 发现的那样。

于 2009-09-29T01:28:02.623 回答
36

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

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

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

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

于 2009-09-29T05:11:53.207 回答
11

字节序是在谈论字节顺序而不是位顺序。如今,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
于 2009-09-29T01:31:33.650 回答
6

大多数时候,可能,但不要把农场押在上面,因为如果你错了,你会损失惨重。

如果您真的非常需要具有相同的二进制信息,则需要使用位掩码创建位域 - 例如,您对消息使用无符号短(16 位),然后使 versionMask = 0xE000 之类的东西代表三个最高位。

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

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

这是我从事的一个项目的问题,因为在 Sparc 上运行的服务器进程会查询客户端并发现它是大端的,并假设它可以将二进制结构喷射到网络上并且客户端可以应付。这在 PowerPC 客户端上运行良好,在 680x0 客户端上崩溃了。代码不是我写的,找了好久才发现问题。但是一旦我这样做了就很容易修复。

于 2009-09-29T04:53:13.477 回答
2

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

于 2019-09-27T03:27:32.010 回答
-10

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

于 2009-09-29T05:17:04.177 回答