使用Apple的LLVM编译器-O而不是g ++ -7.2.0编译-O时,C ++代码段出错 [英] C++ code segfaults when compiled -O with Apple's LLVM compiler, but not with g++ -7.2.0

查看:80
本文介绍了使用Apple的LLVM编译器-O而不是g ++ -7.2.0编译-O时,C ++代码段出错的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Update: I've created an even more M, but still CVE that reproduces the crash. Summary: removed all use of the Bool* bools_ field in Base class (but it still must be defined or the crash does not happen). Also removed Base::Initialize() and the virtual method Rule from Base and its descendants. New MCVE is attached.

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

I've managed to create an MCVE for this code and posted it below.

一些描述性的细节:代码使用虚拟基类和派生类,并且某些实例化的派生类具有构造函数,这些构造函数调用从基"类(实际上是派生类,但在派生类中继承)的非虚拟方法.继承层次结构,而不是我所谓的派生"类)来初始化基"类数据.该方法调用在派生类中重写的虚拟方法.我意识到这是一件危险的事情,但是从我(可能有限)的C ++理解来看,它似乎应该起作用,因为派生类构造函数的主体要等到基"类虚拟表被建立后才执行.无论如何,在调用基"类的初始化方法期间都不会发生段错误.

Some descriptive details: the code uses virtual base and derived classes, and certain derived classes that are instantiated have constructors that call a non-virtual method inherited from a "base" class (actually a derived class, but higher up in the inheritance hierarchy than what I am calling the "derived" classes) to initialize "base" class data. That method calls a virtual method that is overridden in the derived classes. I realize that that is a dangerous thing to do, but from my (possibly limited) understanding of C++, it seems like it should work because the body of the derived class constructor does not execute until the "base" class virtual tables are set up. In any case, the segfault does NOT occur during the call to the "base" class's initialization method.

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

The segfault occurs in the "base" class constructor, and ONLY when the body of the constructor is empty. If I add a debugging line to the constructor to print out when that point is reached, the debugging line is printed out and the code runs normally. My guess is that for some reason the compiler is optimizing away the initializations that should happen before the body of the "base" class's constructor is executed, including the setting up of the vtable.

如主题行所述,使用Apple的g ++或g ++ 7.2.0进行未经优化的编译时,此代码运行良好,而使用g ++ 7.2.0甚至编译为-O3时,该代码也运行良好.只有在使用Apple的g ++ LLVM实现编译 -O2 -O3 时,它才会出现段错误.该编译器的 g ++ --version 的输出为:

As the subject line says, this code runs fine when compiled without optimization using either Apple's g++ or g++ 7.2.0, and it runs fine when compiled even -O3 using g++ 7.2.0. It ONLY segfault when compiled -O2 or -O3 with Apple's LLVM implementation of g++. The output of g++ --version for that compiler is:

% /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

Reference number 36382481

推荐答案

这看起来像是Clang中的一个错误,该错误是由无效生成的未对齐SSE商店引起的.下面是一个基于您的代码的最小示例:

This looks like a bug in Clang caused by invalid generation of unaligned SSE stores. Below is a minimal example based on your code:

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使用相同的布局):

This is layout generated by Clang (GCC uses the same layout):

*** 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]

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

We can see that Base3 is merged with Base1, so they share address. Base2 is instantiated by Base3 and is placed afterwards with 8 byte offset, aligning Base2 instance at 8 bytes, even though alignof(Base2) is 16. This is still correct behavior, since this is the maximum alignment among all member fields in Base2. Alignment inherited from virtual base class Base1 does not need to be preserved, as Base1 is instantiated by derived class Base3 which is responsible for aligning Base1 properly.

问题在于Clang生成的代码:

The problem is with the code Clang generates:

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

Clang决定使用一条 movaps 指令初始化 data1 _ data2 _ ,这条指令需要16字节对齐,但是 Base2 实例仅对齐8个字节,从而导致段错误.

Clang decides to initialize both data1_ and data2_ with a single movaps instruction which requires 16 byte alignment, but Base2 instance is only 8-bytes aligned, leading to segfault.

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

Looks like Clang assumes it can use 16-byte aligned stores because alignof(Base2) is 16, but such assumption is wrong for classes with virtual bases.

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

If you need a temporary solution, you can disable usage of SSE instructions with -mno-sse flag. Note that this can have performance impact.

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

Itanium ABI document can be found here: https://refspecs.linuxfoundation.org/cxxabi-1.75.html

它明确提到了 nvalign :

nvalign(O):对象的非虚拟对齐方式,即没有虚拟碱基的O的对齐方式.

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

然后说明如何完成分配:

Then there is an explanation on how allocation is done:

虚拟基地以外的成员的分配

如果D不是一个空基类,或者D是一个数据成员:从offset开始dsize(C),必要时增加以与nvalign(D)对齐基类或align(D)作为数据成员.将D放置在此偏移处除非这样做会导致以下两个因素(直接或间接)具有相同偏移量的相同类型.如果是这样的组件类型发生冲突,将候选偏移量增加nvalign(D)作为基数类或通过align(D)获取数据成员,然后重试,重复直到成功发生(将不早于sizeof(C)向上取整为所需的对齐方式.

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 .我们还可以在上面的记录布局转储中看到这一点.

Looks like both Clang and GCC honor Itanium ABI, properly aligning Base2 using non-virtual alignment. We can also see that in the record layout dump above.

您可以使用 -fsanitize = undefined (包括GCC和Clang)编译程序,以在运行时获取此假阳性警告消息:

You can compile your program with -fsanitize=undefined (both GCC and Clang) to get this false-positive warning message at runtime:

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


因此,目前存在三个错误.我已经报告了所有人:


So there are three bugs at the moment. I have reported all of them:

这篇关于使用Apple的LLVM编译器-O而不是g ++ -7.2.0编译-O时,C ++代码段出错的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆