【问题标题】:How do bit fields and their alignments work in C programming?位域及其对齐如何在 C 编程中工作?
【发布时间】:2014-05-04 16:35:07
【问题描述】:

我需要您的帮助来了解位域在 C 编程中的工作原理。

我已经声明了这个结构:

struct message
{
    unsigned char first_char : 6;
    unsigned char second_char : 6;
    unsigned char third_char : 6;
    unsigned char fourth_char : 6;
    unsigned char fifth_char : 6;
    unsigned char sixth_char : 6;
    unsigned char seventh_char : 6;
    unsigned char eigth_char : 6;
}__packed message;

我使用 sizeof(message) 将结构的大小保存为整数。

我认为大小的值将是 6,因为 6 * 8 = 48 位,即 6 个字节, 但它的大小值是 8 个字节。

谁能向我解释一下为什么,以及位域和它们的对齐方式是如何工作的?

编辑

我忘了解释我使用结构的情况。 假设我以这种形式收到 6 个字节的数据包: void * packet

然后我像这样转换数据:

message * msg = (message *)packet;

现在我想打印每个成员的值,所以虽然我将成员声明为 6 位,但成员使用 8 位,这会在打印时导致错误的结果。 例如我收到下一个数据:

00001111 11110000 00110011 00001111 00111100 00011100

我认为成员的价值如下所示:

first_char = 000011

second = 111111

third = 000000

fourth = 110011

fifth = 000011

sixth = 110011

seventh = 110000

eigth = 011100

但这不是什么,我希望我解释得很好,如果不是请告诉我。

【问题讨论】:

  • 你声明了 8 个字符,为什么你会期望它是 6 个字节长?
  • 您是否尝试实现base64 encoding/decoding
  • 我查了谷歌,一无所获。在 8 个字符中的每一个中,我只使用 6 位,如果算上它,你会得到 48 位,即 6 个字节。

标签: c struct alignment field bit


【解决方案1】:

位域不必跨越不同的底层元素(“单元”),因此您可以看到每个域都占用一个完整的无符号字符。行为是实现定义的,尽管如此;参看。 C11 6.7.2.1/11:

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

此外,根据 6.7.2.1/4 中的限制,任何位域都不得大于单个单元的大小:

指定位域宽度的表达式应为整数常量 具有不超过对象宽度的非负值的表达式 如果省略了冒号和表达式,将指定的类型。

【讨论】:

  • 我想为 §6.7.2.1 ¶4 的报价给你一个额外的 +1。您可能会对此进行扩展以注意到类型已更改为 unsigned long,您将获得完全不同的打包行为。
  • @JonathanLeffler:我认为unsigned long 本身只是有条件地支持。唯一普遍允许的单位类型是_Boolsigned intunsigned int (6.7.2.1/5)... :-(
  • 是的;有很多这样的限制……我在我的答案中添加了演示代码,在相当严格的编译器警告下与 GCC 4.9.0 一起工作,并显示了 unsigned charunsigned shortunsigned int 和 @987654329 的不同结果@ 在 64 位平台上。
  • 我认为 C 标准禁止跨分配单元的位字段——我认为这个限制很愚蠢,但仍然存在。是我记错了,还是 C11 改变了规则?
  • @supercat:不知道,我没有任何旧版本的标准。我的猜测是它总是被允许的,虽然......
【解决方案2】:

几乎所有关于位域的内容都是由实现定义的。特别是,如何将位字段打包在一起是实现定义的。实现不需要让位域跨越可寻址存储单元的边界,而您的实现似乎没有。

ISO/IEC 9899:2011 §6.7.2.1 结构和联合说明符

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

这绝不是位域的“实现定义”特性的终结。

[请选择Kerek SBanswer 而不是这个,因为它也包含有关§6.7.2.1 ¶4 的重要信息。]


示例代码

#include <stdio.h>

#if !defined(BITFIELD_BASE_TYPE)
#define BITFIELD_BASE_TYPE char
#endif

int main(void)
{
    typedef struct Message
    {
        unsigned BITFIELD_BASE_TYPE first_char   : 6;
        unsigned BITFIELD_BASE_TYPE second_char  : 6;
        unsigned BITFIELD_BASE_TYPE third_char   : 6;
        unsigned BITFIELD_BASE_TYPE fourth_char  : 6;
        unsigned BITFIELD_BASE_TYPE fifth_char   : 6;
        unsigned BITFIELD_BASE_TYPE sixth_char   : 6;
        unsigned BITFIELD_BASE_TYPE seventh_char : 6;
        unsigned BITFIELD_BASE_TYPE eighth_char  : 6;
    } Message;

    typedef union Bytes_Message
    {
        Message m;
        unsigned char b[sizeof(Message)];
    } Bytes_Message;

    Bytes_Message u;

    printf("Message size: %zu\n", sizeof(Message));

    u.m.first_char   = 0x3F;
    u.m.second_char  = 0x15;
    u.m.third_char   = 0x2A;
    u.m.fourth_char  = 0x11;
    u.m.fifth_char   = 0x00;
    u.m.sixth_char   = 0x23;
    u.m.seventh_char = 0x1C;
    u.m.eighth_char  = 0x3A;

    printf("Bit fields: %.2X %.2X %.2X %.2X %.2X %.2X %.2X %.2X\n",
           u.m.first_char,   u.m.second_char, u.m.third_char,
           u.m.fourth_char,  u.m.fifth_char,  u.m.sixth_char,
           u.m.seventh_char, u.m.eighth_char);

    printf("Bytes:     ");
    for (size_t i = 0; i < sizeof(Message); i++)
        printf(" %.2X", u.b[i]);
    putchar('\n');

    return 0;
}

示例编译和运行

使用 GCC 4.9.0(64 位构建;sizeof(int) == 4sizeof(long_ == 8)在 Mac OS X 10.9.2 Mavericks 上进行测试。源代码在bf.c;创建的程序是bf

$ gcc -DBITFIELD_BASE_TYPE=char -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror bf.c -o bf
$ ./bf
Message size: 8
Bit fields: 3F 15 2A 11 00 23 1C 3A
Bytes:      3F 15 2A 11 00 23 1C 3A
$ gcc -DBITFIELD_BASE_TYPE=short -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror bf.c -o bf
$ ./bf
Message size: 8
Bit fields: 3F 15 2A 11 00 23 1C 3A
Bytes:      7F 05 6A 04 C0 08 9C 0E
$ gcc -DBITFIELD_BASE_TYPE=int -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror bf.c -o bf
$ ./bf
Message size: 8
Bit fields: 3F 15 2A 11 00 23 1C 3A
Bytes:      7F A5 46 00 23 A7 03 00
$ gcc -DBITFIELD_BASE_TYPE=long -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror bf.c -o bf
$ ./bf
Message size: 8
Bit fields: 3F 15 2A 11 00 23 1C 3A
Bytes:      7F A5 46 C0 C8 E9 00 00
$

请注意,对于 4 种不同的字号,有 4 组不同的结果。还要注意,编译器不需要允许这些类型。标准说(再次第 6.7.2.1 节):

¶4 指定位域宽度的表达式应为整数常量 具有不超过对象宽度的非负值的表达式 如果省略了冒号和表达式,将指定的类型。122) 如果值为 零,声明不应有声明符。

¶5 位字段的类型应为 _Boolsigned intunsigned int 或其他一些实现定义的类型的合格或非合格版本。

122) 虽然_Bool 对象中的位数至少为CHAR_BIT,但宽度(符号数和 _Bool 的值位)可能只有 1 位。


另一个子问题

你能解释一下为什么我认为我会得到 6 号的尺寸是错误的吗?我问了很多朋友,但他们对位域知之甚少。

我不确定我对位域了解多少。除了回答 Stack Overflow 上的问题外,我从未使用过它们。它们在编写可移植软件时毫无用处,而我的目标是编写可移植软件——或者,至少,不是无缘无故不可移植的软件。

我想你假设的位布局大致相当于这个:

+------+------+------+------+------+------+------+------+
|  f1  |  f2  |  f3  |  f4  |  f5  |  f6  |  f7  |  f8  |
+------+------+------+------+------+------+------+------+

它应该代表 8 组 6 位中的 48 位,连续布局,没有空格或填充。

现在,不能发生这种情况的一个原因是 §6.7.2.1 ¶4 中的规则,当您将类型 T 用于位域时,位域的宽度不能大于CHAR_BIT * sizeof(T)。在您的代码中,Tunsigned char,因此位域不能大于 8 位,否则它们会跨越存储单元边界。当然,您的只有 6 位,但这意味着您无法将第二个位域放入存储单元。如果Tunsigned short,那么只有两个 6 位字段适合 16 位存储单元;如果 T 是 32 位的 int,则可以容纳五个 6 位字段;如果 T 是 64 位 unsigned long,则可以容纳 10 个 6 位字段。

另一个原因是访问这种跨越字节边界的位字段会相当低效。例如,给定(Message 在我的示例代码中定义):

Message bf = …initialization code…

int nv = 0x2A;
bf.second_char = nv;

假设代码将值视为存储在压缩字节数组中,其中字段重叠字节边界。那么代码需要设置下面标有y的位:

             Byte 0             |            Byte 1
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| x | x | x | x | x | x | y | y | y | y | y | y | z | z | z | z |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

这是一种位模式。 x 位可能对应于first_charz 位可能对应于 third_char 的一部分;和y 位到second_char 的旧值。因此,赋值必须复制 Byte 0 的前 6 位,并将新值的 2 位分配给后两位:

((unsigned char *)&bf)[0] = (((unsigned char *)&bf)[0] & 0xFC) | ((nv >> 4) & 0x03);
((unsigned char *)&bf)[1] = (((unsigned char *)&bf)[1] & 0x0F) | ((nv << 4) & 0xF0);

如果将其视为 16 位单元,则代码相当于:

((unsigned short *)&bf)[0] = (((unsigned char *)&bf)[0] & 0xFC0F) | ((nv << 4) & 0x03F0);

32 位或 64 位分配有点类似于 16 位版本:

((unsigned int  *)&bf)[0] = (((unsigned int  *)&bf)[0] & 0xFC0FFFFF) |
                                           ((nv << 20) & 0x03F00000);
((unsigned long *)&bf)[0] = (((unsigned long *)&bf)[0] & 0xFC0FFFFFFFFFFFFF) |
                                           ((nv << 52) & 0x03F0000000000000);

这对位域内的位布局方式做出了一组特定的假设。不同的假设得出的表达式略有不同,但如果将位字段视为连续的位数组,则需要类似的东西。

相比之下,实际使用每字节 6 位布局,分配变得简单得多:

((unsigned char *)&bf)[1] = nv & 0x3F;

编译器忽略显示的掩码操作是合法的,因为填充位中的值是不确定的(但该值必须是 8 位赋值)。

访问位域所需的代码量是大多数人避免使用它们的原因之一。不同的编译器可以为相同的定义做出不同的布局假设这一事实意味着值不能在不同类型的机器之间可靠地传递。通常,ABI 将定义标准 C 没有定义的细节,但如果一台机器是 PowerPC 或 SPARC,而另一台基于 Intel,那么所有的赌注都没有了。做转移和掩饰自己会变得更好;至少计算成本是可见的。

【讨论】:

  • 我不确定我是否理解,你能解释一下为什么我认为我会得到 6 的大小是错误的吗?我问了很多朋友,但他们不太了解 bit字段。
  • 这里有很多有用的信息,但我不同意社论。位域是完全可移植的(只要您使用可移植的基类型),现代编译器可以很好地优化它们;结果代码更具可读性,特别是对于实现为单个位的布尔标志,并且更易于维护。位域不能以跨平台的方式简单地序列化这一事实并不使它们的使用不可移植;否则,您必须避免使用除 char 以外的任何数据类型(因为字节序因平台而异),尤其是浮点(不能依赖 IEEE 浮点数)...
  • 我想这取决于您对可移植性的定义。我的包括序列化问题——你的似乎没有。
  • ... 虽然您不能使用位域来描述 x-plat 序列化格式(这很不幸),但您仍然可以使用它们来减小数据大小;一个典型的用途是将几个标志(可能是非常小的枚举)打包成一个struct 数据类型,它将被实例化很多次。当然,这种压缩通常是过早的优化,但使用位域的优点是您可以在需要时进行优化,而无需更改任何客户端代码。
  • @JonathanLeffler:您对可移植性的定义显然让您忽略了端序性。这是对“包括序列化问题”的非常灵活的定义:)
猜你喜欢
  • 1970-01-01
  • 2012-03-03
  • 2010-10-28
  • 2010-12-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-25
  • 1970-01-01
相关资源
最近更新 更多