Gcc /clang在基结构的后填充中布局派生结构的字段

gcc/clang lay out fields of a derived struct in the back-padding of base struct

本文关键字:结构 布局 派生 字段 填充 Gcc clang      更新时间:2023-10-16

我对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,是由设计的,那么我的问题是:

  1. 分配或不分配派生结构的字段的精确规则是什么
  2. 是否有任何方法可以用开关/属性覆盖这种行为(即像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不兼容的情况,但是上面的示例没有产生任何警告。

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