使用 Apple 的 LLVM 编译器编译 -O 时出现 C++ 代码段错误,但使用 g++ -7.2.0 时则不会

C++ code segfaults when compiled -O with Apple's LLVM compiler, but not with g++ -7.2.0

更新:我创建了一个更多的 M,但仍然是重现崩溃的 CVE。摘要:删除了 Base class 中对 Bool* bools_ 字段的所有使用(但它仍然必须定义,否则不会发生崩溃)。还从 Base 及其后代中删除了 Base::Initialize() 和虚拟方法 Rule。附上新的 MCVE。

我已经设法为此代码创建了一个 MCVE 并将其发布在下方。

一些描述性细节:代码使用虚基和派生classes,并且实例化的某些派生classes具有调用从[=37继承的非虚方法的构造函数=] class(实际上是派生的 class,但在继承层次结构中比我所说的 "derived" classes 更高)来初始化 "base" class数据。该方法调用在派生的 classes 中重写的虚方法。我意识到这是一件危险的事情,但根据我对 C++ 的(可能有限的)理解,它似乎应该起作用,因为派生的 class 构造函数的主体直到 "base" 才会执行class 虚拟表已设置。在任何情况下,段错误都不会在调用 "base" class 的初始化方法期间发生。

段错误发生在 "base" class 构造函数中,并且仅当构造函数的主体为空时。如果我向构造函数添加调试行以在到达该点时打印出来,则调试行被打印出来并且代码正常运行。我的猜测是,出于某种原因,编译器正在优化应该在 "base" class 的构造函数主体执行之前发生的初始化,包括 vtable 的设置。

如主题行所述,此代码在使用 Apple 的 g++ 或 g++ 7.2.0 未经优化编译时运行良好,甚至在使用 g++ 7.2.0 编译 -O3 时运行良好。它仅在使用 Apple 的 g++ LLVM 实现编译 -O2-O3 时出现段错误。该编译器的 g++ --version 的输出是:

% /usr/bin/g++ --version

Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin17.3.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Content

紧随其后的是 MCVE。

#include <iostream>

using namespace std;

class OriginalBaseClass {
public:
  OriginalBaseClass(long double data1 = 1, long int data2 = 1) : data1_(data1), data2_(data2) { cout << "In OriginalBaseClass constructor\n"; }
private:
  long double data1_;
  long int data2_;
};

class Base : public virtual OriginalBaseClass {
public:
  Base(long int data1 = 0, long int data2 = 0) : data1_(data1), data2_(data2) { cout << "In Base constructor\n"; }
  virtual ~Base();
private:
  bool* bools_;
  long int data1_;
  long int data2_;
};

Base::~Base()
{
  cout << "In Base destructor\n";
}

class Derived_A : public virtual Base {
public:
  Derived_A() { cout << "In Derived_A constructor\n"; }
};

class Derived_B : public Derived_A {
public:
  Derived_B() : OriginalBaseClass(), Base(4, 1), Derived_A() { cout << "In Derived_B constructor\n"; }
};

int main()
{
  Derived_B Derb;
}

Link 错误报告:https://bugreport.apple.com/web/

参考编号36382481

这看起来像是 Clang 中的错误,由无效生成未对齐的 SSE 存储引起。以下是基于您的代码的最小示例:

struct alignas(16) Base1 { };

struct Base2 : virtual Base1 {
    __attribute__((noinline)) Base2() : data1_(0), data2_(0) { }

    long dummy_, data1_, data2_;
};

struct Base3 : virtual Base2 { };

int main() { Base3 obj; }

这是 Clang 生成的布局(GCC 使用相同的布局):

*** Dumping AST Record Layout
         0 | struct Base1 (empty)
           | [sizeof=16, dsize=16, align=16,
           |  nvsize=16, nvalign=16]

*** Dumping AST Record Layout
         0 | struct Base2
         0 |   (Base2 vtable pointer)
         8 |   long dummy_
        16 |   long data1_
        24 |   long data2_
         0 |   struct Base1 (virtual base) (empty)
           | [sizeof=32, dsize=32, align=16,
           |  nvsize=32, nvalign=8]

*** Dumping AST Record Layout
         0 | struct Base3
         0 |   (Base3 vtable pointer)
         0 |   struct Base1 (virtual base) (empty)
         8 |   struct Base2 (virtual base)
         8 |     (Base2 vtable pointer)
        16 |     long dummy_
        24 |     long data1_
        32 |     long data2_
           | [sizeof=48, dsize=40, align=16,
           |  nvsize=8, nvalign=8]

我们可以看到Base3Base1合并了,所以他们共享地址。 Base2Base3 实例化,之后放置 8 字节 偏移量,将 Base2 实例对齐 8 字节,即使 alignof(Base2) 是 16。这 仍然是正确的 行为,因为这是 Base2 中所有成员字段之间的最大对齐方式。从虚基 class Base1 继承的对齐不需要保留,因为 Base1 由派生的 class Base3 实例化,它负责对齐 [=15] =] 正确。

问题出在 Clang 生成的代码上:

mov    rbx,rdi ; rdi contains this pointer
...
xorps  xmm0,xmm0
movaps XMMWORD PTR [rbx+0x10],xmm0

Clang 决定用一条需要 16 字节对齐的 movaps 指令来初始化 data1_data2_,但是 Base2 实例只有 8 字节对齐,导致段错误。

看起来 Clang 假设它可以使用 16 字节对齐的存储,因为 alignof(Base2) 是 16,但这种假设对于具有虚拟基数的 classes 是错误的。

如果您需要临时解决方案,可以使用 -mno-sse 标志禁用 SSE 指令。请注意,这可能会影响性能。


Itanium ABI 文档可以在这里找到:https://refspecs.linuxfoundation.org/cxxabi-1.75.html

它明确提到 nvalign:

nvalign(O): the non-virtual alignment of an object, which is the alignment of O without virtual bases.

然后是关于如何分配的解释:

Allocation of Members Other Than Virtual Bases

If D is not an empty base class or D is a data member: Start at offset dsize(C), incremented if necessary for alignment to nvalign(D) for base classes or to align(D) for data members. Place D at this offset unless doing so would result in two components (direct or indirect) of the same type having the same offset. If such a component type conflict occurs, increment the candidate offset by nvalign(D) for base classes or by align(D) for data members and try again, repeating until success occurs (which will occur no later than sizeof(C) rounded up to the required alignment).

看起来 Clang 和 GCC 都支持 Itanium ABI,使用非虚拟对齐正确对齐 Base2。我们也可以在上面的记录布局转储中看到。


您可以使用 -fsanitize=undefined(GCC 和 Clang)编译您的程序,以在运行时获得此误报警告消息:

main.cpp:29:5: runtime error: constructor call on misaligned address 0x7ffd3b895dd8 for type 'Base2', which requires 16 byte alignment
0x7ffd3b895dd8: note: pointer points here
 e9 55 00 00  ea c6 2e 02 9b 7f 00 00  01 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  f8 97 95 34

所以目前有三个错误。我已经全部举报了: