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

Posted

技术标签:

【中文标题】C/C++:强制位域顺序和对齐【英文标题】:C/C++: Force Bit Field Order and Alignment 【发布时间】:2010-12-02 04:24:37 【问题描述】:

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

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

【问题讨论】:

由于缓冲区是一组字节,而不是位,因此“缓冲区中的前 3 位”不是一个精确的概念。你会认为第一个字节的 3 个最低位是前 3 个位,还是 3 个最高位? 在网络上传输时,“缓冲区中的前 3 位”结果是非常定义明确的。 @Joshua IIRC,以太网传输每个字节的最低有效位first(这就是广播位在哪里的原因)。 当您说“便携”和“跨平台”时,您指的是哪个?无论目标操作系统如何,可执行文件都将正确访问顺序 - 或者 - 无论工具链如何,代码都将编译? 【参考方案1】:

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

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

【讨论】:

是的,例如,GCC 特别指出,位域是按照 ABI 排列的,而不是实现。因此,仅仅停留在单个编译器上并不足以保证排序。也必须检查架构。真的是便携性的噩梦。 为什么 C 标准不保证位域的顺序? 很难一致且可移植地定义字节内的位“顺序”,更不用说可能跨越字节边界的位顺序了。您确定的任何定义都将无法与大量现有实践相匹配。 实现定义允许特定于平台的优化。在某些平台上,位字段之间的填充可以改善访问,想象一个 32 位 int 中的四个 7 位字段:在每 8 位对齐它们对于具有字节读取的平台来说是一个显着的改进。 packed 是否强制排序:***.com/questions/1756811/… 如何强制位排序:***.com/questions/6728218/gcc-compiler-bit-order【参考方案2】:

位域因编译器而异,抱歉。

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

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

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

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

【讨论】:

K&R 是否真的被认为是一个有用的参考,因为它是预先标准化的并且(我认为?)可能在许多领域被取代? 我的 K&R 在 ANSI 之后。 现在很尴尬:我没有意识到他们已经发布了一个 post-ANSI 版本。我的错!【参考方案3】:

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

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

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

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

【讨论】:

位域的顺序可以在编译时确定。 此外,当处理在程序之外没有外部表示的位标志时(即在磁盘上或在寄存器中或在其他程序访问的内存中等),位域是高度优选的。 @GregA.Woods:如果确实是这样,请提供一个描述如何的答案。谷歌搜索时,除了您的评论外,我什么也找不到... @GregA.Woods:对不起,应该写我提到的评论。我的意思是:您说“位域的顺序可以在编译时确定。”。我对此无能为力,也不知道该怎么做。 @mozzbozz 查看planix.com/~woods/projects/wsg2000.c 并搜索_BIT_FIELDS_LTOH_BIT_FIELDS_HTOL 的定义和使用【参考方案4】:

字节顺序是指字节顺序而不是位顺序。 现在,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

【讨论】:

a 和 b 的输出表明字节序仍在讨论位顺序和字节顺序。 位排序和字节排序问题的精彩示例 你真的编译并运行了代码吗? “a”和“b”的值对我来说似乎不合逻辑:你基本上是说编译器会因为字节序而交换一个字节内的半字节。在“d”的情况下,字节序不应该影响 char 数组中的字节顺序(假设 char 是 1 个字节长);如果编译器这样做,我们将无法使用指针遍历数组。另一方面,如果您使用了两个 16 位整数的数组,例如: uint16 data[]=0x1234,0x5678;那么 d 在小端系统中肯定是 0x7856。 如果标准说“实现定义”,那么所有的赌注都没有。【参考方案5】:

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

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

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

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

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

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

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

【讨论】:

我认为说使用位字段是愚蠢的说法是错误的,因为它提供了一种非常简洁的方式来表示硬件寄存器,它是用 C 语言创建的。 @trondd:不,它们是为了节省内存而创建的。位域并不打算映射到外部数据结构,例如内存映射的硬件寄存器、网络协议或文件格式。如果它们打算映射到外部数据结构,那么包装顺序就会被标准化。 使用位可以节省内存。使用位域可以提高可读性。使用更少的内存更快。使用位允许更复杂的原子操作。在现实世界的应用程序中,需要性能和复杂的原子操作。这个答案对我们不起作用。 @BenVoigt 可能是真的,但如果程序员愿意确认他们的编译器/ABI 的顺序符合他们的需要,并相应地牺牲快速可移植性 - 那么他们当然可以 履行该职责。至于 9*,哪些权威的“现实世界编码员”认为所有位域的使用都是“不专业/懒惰/愚蠢”,他们在哪里声明了这一点? 使用更少的内存并不总是更快;使用更多内存并减少读取后操作通常更有效,而处理器/处理器模式可以使这一点更加真实。

以上是关于C/C++:强制位域顺序和对齐的主要内容,如果未能解决你的问题,请参考以下文章

关于位域在结构体的应用

C语言 | 位域的使用详解

位域使用 &middot; 今天又是美好的一天!

自然对齐和强制对齐

计算机组成原理 王道考研2021 第二章:数据的表示和运算 -- C语言中的强制类型转换数据的存储和排列(数据的“大端方式”和“小端方式”存储数据按“边界对齐”方式存储)

“现在打包强制记录的字节对齐”是啥意思?