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

时间:2014-08-10 07:07:45

标签: c++ gcc clang padding

当涉及填充和继承时,我对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选项也是如此(显然输出中的大小会有所不同)。

x86和x86_64版本的Microsoft Visual Studio 2010 C ++编译器都没有此问题(即它们以相同的方式布置结构С1和C2)

如果它不是一个bug并且是设计的,那么我的问题是:

  1. 分配或不分配派生结构的字段的准确规则是什么 在后填充中(例如,为什么C2不会发生?)
  2. 有没有办法用switch / attributes覆盖这种行为(即就像MSVC一样布局)?
  3. 提前致谢。

    弗拉基米尔

4 个答案:

答案 0 :(得分:4)

对于每个反对这个问题和OP的人来说,对于他的手写memcpy到底是多么糟糕,UB都自以为是地愤慨...考虑到libc ++和libstdc ++的实现者都陷入了同样的困境。在可预见的将来,了解何时重新使用填充以及何时不重新使用填充实际上非常重要。在OP上很好地提出了这个问题。

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

  

如果D是基类,则将sizeof(C)更新为最大值(sizeof(C),offset(D)+ nvsize(D))。

此处“将[POD类型]的dsize,nvsize和nvalign定义为它们的普通大小和对齐方式,”但非POD类型的nvsize定义为“ 非虚拟大小” ,即没有虚拟碱基(也没有尾部填充)的O的大小。”因此,如果D为POD,则我们永远不会在其尾部填充中嵌套任何内容;而如果D是 not 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
}

答案 1 :(得分:1)

您的代码显示未定义的行为,因为C和C2不是POD,并且不允许通过其数据的随机位进行memcpying。

然而,在稍长的时间内,这是一个复杂的问题。平台上现有的C ABI(Unix)允许这种行为(这适用于允许它的C ++ 98)。然后委员会改变了C ++ 03和C ++ 11中不兼容的规则。至少,Clang可以改用更新的规则。当然,Unix上的C ABI没有改变以适应新的C ++ 11规则来放置填充,因此编译器不能完全更新,因为这会破坏所有ABI。

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

Windows一直禁止在他们的C ABI中使用这种做法,因此我没有意识到这一点。

答案 2 :(得分:1)

不同之处在于,如果该对象已经是&#34;而不仅仅是数据&#34;那么允许编译器使用前一个对象的填充。并且不支持使用memcpy操纵它。

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

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

答案 3 :(得分:0)

感谢大家的帮助。

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

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