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 的 March=native?