Gcc /clang在基结构的后填充中布局派生结构的字段
gcc/clang lay out fields of a derived struct in the back-padding of base struct
我对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) = %dn",
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) = %dn",
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是在结构体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选项也是如此(输出的大小显然会有所不同)。
x86和x86_64版本的Microsoft Visual Studio 2010 c++编译器都没有这个问题(即它们布局结构С1和C2相同)
如果这不是一个bug,是由设计的,那么我的问题是:
- 分配或不分配派生结构的字段的精确规则是什么
- 是否有任何方法可以用开关/属性覆盖这种行为(即像MSVC一样布局)?
提前感谢。
俄罗斯对于每个不喜欢这个问题的人,以及OP的自我回答,对于他手写的memcpy
是多么可怕的愤慨……考虑一下libc++和libstdc++的实现都掉进了同一个坑。在可预见的未来,真正重要的是理解尾填充何时被重用,何时不被重用。OP提出这个问题很好。
结构布局的Itanium ABI规则在这里。相关的措辞是
如果D是基类,则将sizeof(C)更新为max (sizeof(C), offset(D)+nvsize(D))。
这里"[POD类型]的dsize、nvsize和nvalign被定义为它们的普通大小和对齐方式",但非POD类型的nvsize被定义为"对象的非虚拟大小,即不含虚拟基(也不含尾部填充)的0的大小"。所以如果D是POD,我们不会把任何东西塞进它的尾部填充物;然而,如果D是而不是 POD,则允许将下一个成员(或基)嵌套到其尾部填充中。
因此,任何非pod类型(即使是非常普通的可复制类型!)都必须考虑将重要数据塞进尾部填充的可能性。这通常违反了实现者的假设,即允许对平凡的可复制类型做什么(即,您可以平凡地复制它们)。
Wandbox测试用例:
#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=: %dn", int(c1.m_c)); // 4
b1 = b2;
printf("after operator=: %dn", int(c1.m_c)); // 4
printf("before std::copy: %dn", int(c1.m_c)); // 4
std::copy(&b2, &b2 + 1, &b1);
printf("after std::copy: %dn", int(c1.m_c)); // 64, or 0, or anything but 4
}
您的代码显示未定义的行为,因为C和C2不是pod,并且不允许对其数据的随机位进行记忆。
然而,从长远来看,这是一个复杂的问题。平台(Unix)上现有的C ABI允许这种行为(这是c++ 98允许的)。然后,委员会在c++ 03和c++ 11中不兼容地更改了规则。Clang至少可以切换到更新的规则。当然,Unix上的C ABI并没有改变以适应新的c++ 11填充规则,所以编译器不能完全更新,因为这会破坏所有的ABI。
我相信GCC正在为5.0存储打破abi的更改,这可能是其中之一。
Windows总是在他们的C ABI中禁止这种做法,因此没有问题,据我所知。
不同的是,编译器允许使用前一个对象的填充,如果该对象已经"不仅仅是数据",并且不支持使用memcpy
操作它。
B
结构不仅仅是数据,因为它是一个派生对象,因此它的空闲空间可以被使用,因为如果你在memcpy
-一个B
实例周围,你已经违反了契约。
B2
只是一个结构,向后兼容性要求它的大小(包括空闲空间)只是内存,您的代码允许使用memcpy
。
谢谢大家的帮助。
底线,c++编译器允许在布局派生结构体的字段时重用非pod结构体的尾部填充。GCC和clang都使用这个权限,而MSVC没有。GCC似乎有-Wabi警告标志,它应该有助于捕获潜在的ABI不兼容的情况,但是上面的示例没有产生任何警告。
看起来防止这种情况发生的唯一方法是注入显式的尾部填充字段。
- 更正GLSL无绑定纹理句柄中的结构布局
- 假设传递给 OpenGL 的结构数组的内存布局存在潜在错误
- COM 互操作结构定义与内存布局不匹配
- 结构中的内存布局差异
- 执行匿名结构更改布局和填充
- 在匿名结构中包装灵活数组时,MSVC结构布局是否更改
- 非结构对象布局
- 为什么这个结构不是标准布局
- C 的不同内存布局包装结构
- 对象/结构等的C++内存布局是什么?
- 结构前缀的布局
- 将模板生成的类分配给具有相同布局的 C 结构
- 如何知道 c 中结构数据的布局
- 我们应该在什么时候使用packsize来指定结构布局
- 结构内存布局 在 C 语言中
- 对的向量和包含两个元素的结构体的向量的内存布局的区别- c++ /STL
- Gcc /clang在基结构的后填充中布局派生结构的字段
- C++结构内存布局和OpenGL glVertexPointer
- 我可以将final关键字应用于c++ 11中的POD(标准布局)结构体吗?我应该
- 当c++没有指定结构布局时,为什么glBufferData可以缓冲UBO和SSBO的结构