gcc/clang 在基本结构的后填充中布置派生结构的字段[重复]

Posted

技术标签:

【中文标题】gcc/clang 在基本结构的后填充中布置派生结构的字段[重复]【英文标题】:gcc/clang lay out fields of a derived struct in the back-padding of base struct [duplicate] 【发布时间】:2014-10-03 06:46:18 【问题描述】:

当涉及到填充和继承时,我对 gcc 和 clang 如何布局结构感到困惑。这是一个示例程序:

#include <string.h>
#include <stdio.h>

struct A

    void* m_a;
;

struct B: A

    void* m_b1;
    char m_b2;
;

struct B2

    void* m_a;
    void* m_b1;
    char m_b2;
;

struct C: B

    short m_c;
;

struct C2: B2

    short m_c;
;

int main ()

    C c;
    memset (&c, 0, sizeof (C));
    memset ((B*) &c, -1, sizeof (B));

    printf (
        "c.m_c = %d; sizeof (A) = %d sizeof (B) = %d sizeof (C) = %d\n", 
        c.m_c, sizeof (A), sizeof (B), sizeof (C)
        );

    C2 c2;
    memset (&c2, 0, sizeof (C2));
    memset ((B2*) &c2, -1, sizeof (B2));

    printf (
        "c2.m_c = %d; sizeof (A) = %d sizeof (B2) = %d sizeof (C2) = %d\n", 
        c2.m_c, sizeof (A), sizeof (B2), sizeof (C2)
        );

    return 0;

输出:

$ ./a.out
c.m_c = -1; sizeof (A) = 8 sizeof (B) = 24 sizeof (C) = 24
c2.m_c = 0; sizeof (A) = 8 sizeof (B2) = 24 sizeof (C2) = 32

结构 C1 和 C2 的布局不同。在 C1 中,m_c 分配在 struct B1 的后填充中,因此被第二个 memset () 覆盖;对于 C2,它不会发生。

使用的编译器:

$ clang --version
Ubuntu clang version 3.3-16ubuntu1 (branches/release_33) (based on LLVM 3.3)
Target: x86_64-pc-linux-gnu
Thread model: posix

$ c++ --version
c++ (Ubuntu 4.8.2-19ubuntu1) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

-m32 选项也是如此(显然,输出中的大小会有所不同)。

Microsoft Visual Studio 2010 C++ 编译器的 x86 和 x86_64 版本都没有这个问题(即它们对结构 С1 和 C2 的布局相同)

如果这不是错误并且是设计使然,那么我的问题是:

    分配或不分配派生结构的字段的精确规则是什么 在背面填充中(例如,为什么 C2 不会发生这种情况?) 有没有办法用开关/属性覆盖此行为(即像 MSVC 一样布局)?

提前致谢。

弗拉基米尔

【问题讨论】:

mentorembedded.github.io/cxx-abi/abi.html#layout 对于 POD 类型,整个类型都保持不变(据说是为了与 C 兼容)。对于明显不是 C 的类型(例如,如果您向 B2 添加析构函数),它会重用填充。 该文档并未真正涵盖此问题。它主要委托给 C ABI,并没有解释为什么它拥有它拥有的算法。 @Puppy 该文档指定了布局的确切规则,并说它没有涵盖问题是......奇怪。 @MarcGlisse:例如,它没有提到标准中关于此的更改规则。或旧版 ABI。或 POD 的布局。 “我们忽略 POD 的尾部填充,因为该标准的早期版本不允许我们将其用于其他任何事情,而且它有时允许更快地复制类型。” 【参考方案1】:

对于每个对这个问题投反对票的人以及 OP 对他的手写 memcpy 是多么可怕的 UB 的自以为是的愤慨的自我回答...考虑到 libc++ 和 libstdc++ 的实现者都落入了完全相同的坑。在可预见的未来,真正非常重要了解何时重用尾部填充以及何时不重用。非常感谢 OP 提出这个问题。

结构布局的 Itanium ABI 规则是 here。相关措辞是

如果 D 是基类,将 sizeof(C) 更新为 max (sizeof(C), offset(D)+nvsize(D))。

这里“[POD 类型] 的 dsize、nvsize 和 nvalign 被定义为它们的普通大小和对齐方式”,但非 POD 类型的 nvsize 被定义为“非虚拟大小 一个对象的大小,它是 O 的大小,没有虚拟基础[也没有尾部填充]。”所以如果 D 是 POD,我们永远不会在它的尾部填充中嵌入任何东西;而如果 D 不是 POD,我们可以将下一个成员(或基础)嵌套到它的尾部填充中。

因此,任何非 POD 类型(即使是可简单复制的类型!)都必须考虑将重要数据填充到其尾部填充中的可能性。这通常违反了实现者关于允许对普通可复制类型做什么的假设(即,您可以简单地复制它们)。

Wandbox test case:

#include <algorithm>
#include <stdio.h>

struct A 
    int m_a;
;

struct B : A 
    int m_b1;
    char m_b2;
;

struct C : B 
    short m_c;
;

int main() 
    C c1  1, 2, 3, 4 ;
    B& b1 = c1;
    B b2  5, 6, 7 ;

    printf("before operator=: %d\n", int(c1.m_c));  // 4
    b1 = b2;
    printf("after operator=: %d\n", int(c1.m_c));  // 4

    printf("before std::copy: %d\n", int(c1.m_c));  // 4
    std::copy(&b2, &b2 + 1, &b1);
    printf("after std::copy: %d\n", int(c1.m_c));  // 64, or 0, or anything but 4

【讨论】:

【参考方案2】:

您的代码表现出未定义的行为,因为 C 和 C2 不是 POD,并且不允许对其数据的随机位进行 memcpying。

但是,从长远来看,这是一个复杂的问题。平台 (Unix) 上现有的 C ABI 允许这种行为(这是针对 C++98 的,它允许这种行为)。然后委员会在 C++03 和 C++11 中不兼容地更改了规则。至少,Clang 可以切换到更新的规则。当然,Unix 上的 C ABI 并没有改变以适应新的 C++11 规则以将内容放入填充中,因此编译器不能完全更新,因为这会破坏所有 ABI。

我相信 GCC 正在为 5.0 存储破坏 ABI 的更改,这可能就是其中之一。

Windows 总是在其 C ABI 中禁止这种做法,因此据我所知没有问题。

【讨论】:

你能说出 clang 选项吗?此外,gcc-5 中破坏 ABI 的更改基本上将在库中。 不,我不能。我只看到了一些实现。【参考方案3】:

不同之处在于,如果前一个对象已经“不仅仅是数据”并且不支持使用 memcpy 操作它,则允许编译器使用前一个对象的填充。

B 结构不仅仅是数据,因为它是一个派生对象,因此可以使用它的松弛空间,因为如果你是 memcpy-ing 周围的 B 实例,你已经违反了合同。

B2 只是一个结构,向后兼容性要求它的大小(包括松弛空间)只是您的代码可以使用memcpy 使用的内存。

【讨论】:

【参考方案4】:

感谢大家的帮助。

底线是,C++ 编译器允许在布局派生结构的字段时重用非 POD 结构的尾部填充。 GCC 和 clang 都使用了这个权限,MSVC 没有。 GCC 似乎有 -Wabi 警告标志,这应该有助于捕捉潜在的 ABI 不兼容的情况,但它没有产生上述示例的警告。

看起来防止这种情况发生的唯一方法是注入显式尾部填充字段。

【讨论】:

什么,所以你可以继续通过 memcpying 生成 UB 吗?只是不要做未定义的事情。 问题是关于布局结构字段 gcc/clang 与 msvc,memset 只是为了说明我的意思。无论如何,谢谢,我得到了我需要的信息。 -Wabi 仅适用于 GCC 版本之间,请参阅 -fabi-version -Wabi 在这里不相关,因为 GCC 遵循的规则来自 Itanium C++ ABI,并且在 GCC 版本之间没有改变(Clang 也遵循 Itanium C++ ABI,但 MSVC 没有)

以上是关于gcc/clang 在基本结构的后填充中布置派生结构的字段[重复]的主要内容,如果未能解决你的问题,请参考以下文章

如果我布置结构的字段以便它们不需要任何填充,那么符合标准的 C++ 编译器可以添加额外的东西吗?

结构填充和非阻塞通信缓冲区问题导致的 MPI 派生数据类型问题

使 gcc/clang 将函数识别为内置函数

相当于其他编译器中的 gcc/clang 的 March=native?

如何在 gcc/clang 中的错误和警告之间添加换行符/边框

编译器:gcc, clang, llvm