奇怪的结构成员打包在 32 位 clang 中

Bizarre struct member packing in 32-bit clang

我偶然发现了一个用 32 位 clang 打包的非常奇怪的结构成员。这里是 the link to the compiler explorer experiment.

本质上,我有以下结构:

struct _8Bytes
{
    uint64_t _8bytes;
};
struct _16Bytes : _8Bytes
{
    uint32_t  _4bytes;
};
struct Test : _16Bytes
{
    uint8_t test;
};

sizeof(_16Bytes) 是预期的 16,但是 offsetof(Test, test) 是 12,因为编译器决定在 _16Bytes::_4bytes 之后立即打包它。这非常烦人,我想首先禁用此行为,但就这样吧。

令我困惑的是,如果我按如下方式更改 _16Bytes 结构:

struct _16Bytes
{
    // Same as Test1::_16Bytes, but 8 bytes is now a member
    uint64_t _8bytes;
    uint32_t  _4bytes;
};

然后 offsetof(Test, test) 突然变成了 16。这对我来说完全没有意义 - 谁能解释一下这是怎么回事?

更重要的是,有没有办法禁用这种烦人的打包行为?

Here's my test program in Compiler Explorer using clang.
And here is the same program in OnlineGDB using gcc.

The sizeof(_16Bytes) is 16 as expected, however offsetof(Test, test) is 12 because the compiler decided to pack it right after _16Bytes::_4bytes.

这实际上对我来说非常有意义,基于标准的打包规则,假设 struct Test : _16Bytes 变成等价于此:

struct Test
{
    uint64_t _8bytes;  // inherited from `struct _8Bytes` 
    uint32_t  _4bytes; // inherited from `struct _16Bytes`
    uint8_t test;      // directly part of `struct Test`
};

这是因为这个结构是自然打包的,因为它是按数据类型从大到小的顺序排列的。 uint64_t需要8字节对齐,已经有,uint32_t需要4字节对齐,已经有,uint8_t需要1字节对齐,已经有它。因此,唯一需要的填充被添加到最后,如下所示:

struct Test
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t test;
    // 3 bytes of padding to force 8-byte alignment of the whole struct
};  // struct is 16 bytes total

所以,我希望 sizeof(_16Bytes) 是 16,offsetof(Test, test) 是 12。

然而,假设struct Test : _16Bytes变成上面的结构是一个错误的假设。实际上,当结构从另一个 class 或结构继承时,这显然是 未定义的行为 。 clang 在 Compiler Explorer 上显示此 invalid-offsetof 警告:

x86-64 clang 13.0.0(编译器 #1)的输出:

<source>:61:44: warning: offset of on non-standard-layout type 'struct Test' [-Winvalid-offsetof]
    printf("offsetof(Test, test) = %lu\n", offsetof(struct Test, test));

...gcc 对 OnlineGDB 上的同一行显示此警告:

main.cpp:61:53: warning: offsetof within non-standard-layout type ‘Test’ is undefined [-Winvalid-offsetof]
     printf("offsetof(Test, test) = %lu\n", offsetof(struct Test, test));

gcc 输出更清楚:“非标准布局类型‘Test’中的偏移量未定义”。

注意:clang 在设计上试图与 gcc 兼容。请参阅此处:https://clang.llvm.org/ --> 最终用户功能 --> “GCC 兼容性”。

你说:

then all the sudden offsetof(Test, test) is 16. This makes absolutely zero sense to me - can somebody explain what is going on?

因此,当您更改 struct _16Bytes 并看到 offsetof(Test, test) 变成一个非常奇怪和异常的值 16 时,这也是 未定义的行为,因此除了查看 clang 编译器的细节外,我们无法分析保证或可预测的行为,这是毫无意义的,因为它是标准未定义的行为,并且随时可能发生变化。所以,如果你想读取偏移量,你必须避免我认为的未定义行为并且不要使用继承。

This is extremely annoying and I would like to disable this behavior in the first place, however so be it.

More importantly, is there a way to disable this annoying packing behavior?

您必须手动打包您认为合适的结构——但是您喜欢它们。我不确定你想要什么。您是否希望 uint8_t 的偏移量为 15 而不是 12?如果是这样,请执行以下操作:

例如:

struct Test4
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t padding[3]; // explicitly place 3 bytes of padding
    uint8_t test;
};  // struct is 16 bytes total

现在,sizeof(Test4) 是 16,offsetof(Test4, test) 是 15 而不是 12。

请注意,根据您要完成的任务,您可能需要通过在单词 struct 之后和结构名称之前添加 __attribute__ ((__packed__)) 来强制删除所有自动填充。

示例:这个非常不标准的填充,结合 packed 属性,允许 struct __attribute__ ((__packed__)) Test5 的大小为 16 而 offsetof(Test5, test) 仍然为 15:

struct __attribute__ ((__packed__)) Test5
{
    uint8_t padding[3]; // explicitly place 3 bytes of padding
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t test;
};

另请查看 C++ 中的 alignas() specifier,看看它是否可以用来创建您想要的效果。并且,查看 #pragma pack.

请记住,您也可以通过直接将结构包含为另一个结构的成员来实现某些期望的结果,而不是像您所做的那样使用继承。在另一个结构中包含一个结构可以避免未定义的行为,而进行继承并期望某些偏移结果会调用未定义的行为。

最后,您可以考虑编写 Python 脚本来为您自动生成 C++ 代码,为您生成任何必要的结构定义和处理 padding/packing/alignment,以及序列化问题。您可以在 YAML(我认为首选)或 JSON 文件中定义数据包。我认为这是很常见的做法——使用 Python 为您自动生成 C 或 C++。 I show how to import yaml files in Python here但是,如果可能的话,请避免使用 Python 自动生成 C 或 C++,因为它最终可能会创建 更多 代码的复杂性、复杂的抽象以及对试图创建 less 的开发人员的困惑。但是,那是你根据你的总体情况、用例和架构来决定的。

这是我最终的完整测试代码:

https://onlinegdb.com/nfp19v8m3

/******************************************************************************

Welcome to GDB Online.
GDB online is an online compiler and debugger tool for C, C++, Python, Java, PHP, Ruby, Perl,
C#, VB, Swift, Pascal, Fortran, Haskell, Objective-C, Assembly, HTML, CSS, JS, SQLite, Prolog.
Code, Compile, Run and Debug online from anywhere in world.

GS 
15 Jan. 2022 

See: 

*******************************************************************************/
#include <iostream>

struct _8Bytes
{
    uint64_t _8bytes;
};

struct _16Bytes : _8Bytes
{
    uint32_t  _4bytes;
};

struct _16Bytes2
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
};

struct Test : _16Bytes
{
    uint8_t test;
};

struct Test2
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t test;
};

struct Test3 : _16Bytes2
{
    uint8_t test;
};

struct Test4
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t padding[3]; // explicitly place 3 bytes of padding
    uint8_t test;
};

struct __attribute__ ((__packed__)) Test5
{
    uint8_t padding[3]; // explicitly place 3 bytes of padding
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t test;
};


int main()
{
    printf("sizeof(_8Bytes) = %lu\n", sizeof(_8Bytes));
    printf("sizeof(_16Bytes) = %lu\n", sizeof(_16Bytes));
    printf("sizeof(_16Bytes2) = %lu\n", sizeof(_16Bytes2));
    printf("sizeof(Test) = %lu\n", sizeof(Test));
    printf("sizeof(Test2) = %lu\n", sizeof(Test2));
    printf("sizeof(Test3) = %lu\n", sizeof(Test3));
    printf("sizeof(Test4) = %lu\n", sizeof(Test4));
    printf("sizeof(Test5) = %lu\n", sizeof(Test5));
    printf("\n");
    
    printf("offsetof(Test, test) = %lu\n", offsetof(Test, test));
    printf("offsetof(Test2, test) = %lu\n", offsetof(Test2, test));
    printf("offsetof(Test3, test) = %lu\n", offsetof(Test3, test));
    printf("offsetof(Test4, test) = %lu\n", offsetof(Test4, test));
    printf("offsetof(Test5, test) = %lu\n", offsetof(Test5, test));

    return 0;
}

参考文献:

  1. https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html - __attribute__ ((__packed__)) 的官方 gcc 文档,clang 也支持。在页面中搜索“packed”。
  2. Default inheritance access specifier - 我从未见过不指定访问说明符(publicprotectedprivate)的继承。我必须在这里找到它。

另请参阅:

  1. https://en.cppreference.com/w/cpp/language/alignof
  2. https://en.cppreference.com/w/cpp/language/alignas
  3. *****What is the difference between "#pragma pack" and "__attribute__((aligned))" - 简而言之,#pragma pack(1) // set packing AND alignment to 1 // place struct definition here #pragma pack() // unset packing AND alignment 类型语法比 gcc 属性语法更具限制性,本质上等同于__attribute__((packed,aligned(1))),这不一定是您想要的,因为您可能希望将结构打包到 1 字节但不对齐到 1 字节!
    1. 另请参阅:Anybody who writes #pragma pack(1) may as well just wear a sign on their forehead that says “I hate RISC” <-- 不要成为那样的人!所以,只需使用 __attribute__ ((__packed__)) 而不是 #pragma pack(1)!