【问题标题】:C++ code segfaults when compiled -O with Apple's LLVM compiler, but not with g++ -7.2.0使用 Apple 的 LLVM 编译器编译 -O,但使用 g++ -7.2.0 编译时 C++ 代码段错误
【发布时间】:2018-01-08 21:26:21
【问题描述】:

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

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

一些描述性细节:代码使用虚拟基类和派生类,并且某些被实例化的派生类具有调用从“基”类(实际上是派生类,但在更高级别)继承的非虚拟方法的构造函数继承层次结构比我所谓的“派生”类)初始化“基”类数据。该方法调用在派生类中被覆盖的虚拟方法。我意识到这是一件危险的事情,但从我(可能有限)对 C++ 的理解来看,它似乎应该可以工作,因为派生类构造函数的主体在“基”类虚拟表建立之前不会执行.在任何情况下,在调用“基”类的初始化方法期间都不会发生段错误。

段错误发生在“基”类构造函数中,并且仅当构造函数的主体为空时。如果我在构造函数中添加调试行以在到达该点时打印出来,则打印出调试行并且代码正常运行。我的猜测是,由于某种原因,编译器正在优化应该在“基”类的构造函数的主体执行之前发生的初始化,包括 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;
}

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

参考号 36382481

【问题讨论】:

  • 我建议您使用调试器来查明导致段错误的一般问题区域,然后自行调试或将其提取到成功崩溃并共享的较小程序中,以便我们帮助您调试
  • 您好,谢谢。除了 Xcode 中的调试器之外,还有其他调试器可以处理使用 Apple 的 g++ 编译的代码吗?我试过在 Xcode 中使用它,它只是卡在“基”类构造函数中。它甚至不会介入——这就是我怀疑编译器本质上优化构造函数的原因之一。
  • 尝试使用 -fsanitize=address 编译并运行您的程序。它在 clang 和 gcc 中都可用。这个工具非常擅长捕捉这类问题。很可能您的代码中有未定义的行为。
  • "源代码太长,无法发布,我无法用更简单的代码片段重现它。" 所以,尝试制造minimal reproducible example。如果您无法用较小的代码片段重现问题,则说明您删除了太多代码(可能问题是由于您认为不相关的代码造成的?)。此类问题的典型原因是代码中某处的未定义行为。至少,如果没有minimal reproducible example,就无法说出更多。
  • @PaulMcKenzie 嗯,这次好像是 Clang 的一个 bug。

标签: c++ clang llvm-clang virtual-inheritance


【解决方案1】:

这看起来像是 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 字节 偏移量放置,在 8 字节 处对齐 Base2 实例,即使 alignof(Base2) 为 16。这仍然是正确的行为,因为这是Base2 中所有成员字段之间的最大对齐。从虚拟基类Base1 继承的对齐不需要保留,因为Base1 由派生类Base3 实例化,它负责正确对齐Base1

问题在于 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,但这种假设对于具有虚拟基数的类是错误的。

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


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

它明确提到了nvalign

nvalign(O):对象的非虚对齐,即 没有虚碱基的 O 对齐。

然后是如何分配的解释:

虚拟基地以外的成员分配

如果 D 不是空基类或 D 是数据成员:从偏移量开始 dsize(C),如果需要对齐到 nvalign(D) 则递增 基类或对齐(D)的数据成员。在此偏移处放置 D 除非这样做会导致两个组成部分(直接或间接) 具有相同偏移量的相同类型。如果这样的组件类型 发生冲突,将候选偏移量增加 nvalign(D) 为基 类或通过 align(D) 获取数据成员,然后重试,重复直到 成功发生(不迟于 sizeof(C) 四舍五入到 所需的对齐方式)。

看起来 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

所以目前存在三个错误。我都举报了:

【讨论】:

  • 在此找到另一个问题:stackoverflow.com/questions/46474238/…
  • 谢谢,是的,我也在另一块板上得到了这个答案,特别是使用 -fsanitize=undefined 编译的提示,并验证问题是地址对齐不正确。不过,感谢您的详细解释,这超出了我对汇编代码生成的了解。无论如何,我向 Apple 提交了错误报告。错误参考号是 36382481,以防您想添加任何信息,或者在您自己的错误报告中引用它,如果您计划或已经提交。
  • @fizzixgal 我明天将向 clang 和 gcc 提交错误报告。稍后我将编辑答案并添加指向它们的链接。
  • 我一定会收藏这个问题/答案。作为低代表新人报告问题并且它实际上是编译器中的错误的怪异案例之一。 :-D
  • 3 个编译器错误,向你致敬!
猜你喜欢
  • 2011-07-13
  • 1970-01-01
  • 2012-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-09
相关资源
最近更新 更多