UP | HOME

Ping's Tech Notes

谨慎使用C语言里的联合(union)和位域(bit field)!

Ping Zhou, 2024-01-21

对于内核、驱动、嵌入式系统等底层开发来说,C语言的bit field(位域)和union(联合)都是常用的特性。 位域可以让我们在结构体中指定某些成员占多少位,这在同硬件打交道的时候特别有用。例如硬件要求某个32位的消息里,第31位是flag,其余是value,用位域定义的数据结构:

typedef struct msg_t {
​
  uint32_t flag : 1;
  uint32_t value : 31;
} msg_t;

在程序里可以直接操作结构体成员那样访问flag和value,而不用手动去对32位消息进行位操作,这些编译器都给我们做了。 我们还可以加上联合(union),使得我们既可以访问里面的成员,也可以按照一个32位数访问整个消息:

typedef struct msg_fields_t {
  uint32_t flag : 1;
  uint32_t value : 31;
} msg_fields_t;
​
typedef struct msg_t {
  union {
    uint32_t raw;
    msg_fields_t fields;
  };
} msg_t;

union告诉编译器,raw和fields这两个成员在结构体里占用同样的内存地址。因为这两个成员都是32位,因此raw就是整个32位的消息,而通过fields可以访问该消息的flags和value。 但是,在同时使用union和bit field的时候要注意,union和bit field如果互相套在一起,编译器产生的内存排列可能和你想的不一样!

考虑这个例子,有时候硬件定义的消息里面,有些位域是多用途的,比如某个32-bit里面,有8-bit既可以当作v1,也可以当作v2,如果能用union给同样的8-bit安排2个成员v1, v2,它们占用相同的内存地址,又可以用不同的名字访问,岂不美哉?

typedef struct {
  uint32_t f1 : 8;
  union {
    uint32_t v1 : 8;
    uint32_t v2 : 8;
  };
  uint32_t f2 : 16;
} u1_t;

在上面这个例子里,32-bit的消息,0-7位是f1,然后是8位的v1/v2,用union共用,剩下16位是f2。看起来不错?

但是实际跑起来,你用sizeof打印一下这个结构体的大小就发现不对了:

sizeof(u1_t)=12

用了gcc和clang,都是同样的结果。显然编译器并没有按照我们设想的,把f1, v1/v2, f2这几个成员按照8位,8位,16位这样排列在一个32位的结构里。相反,编译器给他们分别排了32位,也就是f1占了32位,v1/v2共同32位,f2也是32位,整个结构体是3个32位,也就是12字节。

有人可能会问,给结构体加上packed属性,是否能解决这个问题?并不能,我们看一下:

typedef struct __attribute__((packed)) u1p_t {
  uint32_t f1 : 8;
  union {
    uint32_t v1 : 8;
    uint32_t v2 : 8;
  };
  uint32_t f2 : 16;
} u1p_t;
​
// sizeof(u1p_t)=7

加了packed属性后,编译器给它排成了7字节,为什么呢? 通过把结构体在内存里的数据打印出来,我发现,u1pt在内存里是这么排列的(加了packed属性后):

于是整个结构体加了packed属性后,大小变成了7字节。

显然,编译器对于union里面套的这两个v1/v2,没有按照我们的预期分配8-bit,而是按照它们的类型给了个4字节(32位),然后因为packed属性,这4个字节紧接着f1后面。

那如果把union里的v1/v2换成uint8t呢?似乎可以解决这个问题:

typedef struct __attribute__((packed)) u1p2_t {
  uint32_t f1 : 8;
  union {
    uint8_t v1 : 8;
    uint8_t v2 : 8;
  };
  uint32_t f2 : 16;
} u1p2_t;
​
// sizeof(u1p2_t)=4

但是,这是因为v1/v2正好是8位,也就是1个uint8,如果我们要union里的位域不是8-bit,比如这样:

typedef struct __attribute__((packed)) u3p2_t {
  uint32_t f1 : 8;
  union {
    uint8_t v1 : 4;
    uint8_t v2 : 4;
  };
  uint32_t f2 : 20;
} u3p2_t;
​
// sizeof(u3p2_t)=5

这个结构体排列又不对了。还是刚才的问题,编译器按照v1/v2的类型,给了8-bit,排在f1后面,然后f2按照20-bit,分配了3个字节,于是整个结构体变成了5字节。

总结一下: