位域的分配是否特定于不同的 Endianness 实现

问题描述

众所周知,我们有两种字节序:大字节序和小字节序。

假设一个整数占用 4 个字节,因此整数 1 的布局应该是 0x01 0x00 0x00 0x00(小端)和 0x00 0x00 0x00 0x01 表示大端。

要检查机器是小端还是大端,我们可以编写如下代码

int main()
{
    int a = 1;
    char *p = (char *)&a;
    // *p == 1 means little endian,otherwise,big endian

    return 0;
}

据我所知,*p 被分配了第一个八位字节:0x01 代表小端,0x00 代表大端(上面两个粗体部分),这就是代码的工作原理。

现在我不太明白位域如何处理不同的字节序。

假设我们有这样一个结构:

typedef struct {
    unsigned char a1 : 1;
    unsigned char a2 : 1;
    unsigned char a6 : 3;
}Bbit;

我们的任务如下:

Bbit bit;
bit.a1 = 1;
bit.a2 = 1;

这段代码是具体实现的吗?我问的是 bit.a1bit.a2 的值在小端是 1 还是在大端是 0?或者它们绝对是 1,不管字节顺序如何?

解决方法

对于位域,不仅定义了字节字节序实现,还定义了位字节序。

C standard 的第 6.7.2.1p11 节关于结构状态:

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

因此编译器可以自由地按照它认为合适的方式对结构中的位域进行重新排序。作为一个例子,这里是在 Linux 上表示 /usr/include/netinet/ip.h 中的 IP 头的结构:

struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
    u_int8_t tos;
    u_int16_t tot_len;
    u_int16_t id;
    u_int16_t frag_off;
    u_int8_t ttl;
    u_int8_t protocol;
    u_int16_t check;
    u_int32_t saddr;
    u_int32_t daddr;
    /*The options start here. */
  };

在这里你可以看到有两个字段是位字段,它们的声明顺序取决于使用的字节序。

所以这意味着如果您通过网络发送原始结构,则不能依赖于任何特定的字节(或位)排序。

以您的示例为例,添加一些内容以查看表示:

Bbit bit;
bit.a1 = 1;
bit.a2 = 1;
unsigned char *p = (unsigned char *)&bit;
printf("%02x\n",*p);

大端系统可能会打印a0,而小端系统可能会打印03。这是假设未使用的位恰好设置为 0。

,

C 标准甚至不要求表示整数的字节必须是 big-endian 顺序或 little-endian 顺序(它们可能混合)更不用说位域的顺序了。这些东西是实现定义的,这意味着 C 标准没有指定它们,但要求将它们记录在编译器手册或其他文档中。以字节或其他单位表示的位字段的顺序不必与对象中的字节顺序匹配。

,

假设我们有一个结构体:

typedef struct {
    unsigned char a1 : 1;
    unsigned char a2 : 1;
    unsigned char a6 : 3;
}Bbit;

和定义:

Bbit byte;

假设 byte 存储在单个字节中并且当前已清零:0000 0000

byte.a1 = 1;

这会将名为 a1 的位设置为 1。如果 a1 是第一位,那么 byte 变成了 1000 0000,但是如果 a1 是第五位,那么 byte 变成了 0000 1000 ,如果 a1 是第八位,那么 byte 变成了 0000 0001

byte.a2 = 1;

这会将名为 a2 的位设置为 1。如果 a2 是第二位,那么 byte 有(可能)变成 1100 0000,但如果 a2 是第六位,那么 byte 有(可能)变成 0000 1100,如果 a2 是第七位,那么 byte 变成了 0000 0011。 (这些只是“可能的”,因为不能保证这些位遵循一些合理的顺序。编译器不太可能会特意弄乱这个例子。)

当涉及到存储的值时,字节序不是一个因素。每次赋值时只更改表示指定位域的位,并且分配的值会减少到该位数(如果该值对于该位数来说太大,则具有实现定义的行为)。