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

1

10

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.

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

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.

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.

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

The MCVE follows.

#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 to bug report: https://bugreport.apple.com/web/

Reference number 36382481

Warty answered 8/1, 2018 at 21:26 Comment(17)
I recommend you use a debugger to pinpoint the general problem area that results in a segfault then either debug it yourself or extract that into smaller program that successfully crashes and share so we can help you debugApotheosize
Hi, and thanks. Is there any debugger other than the one in Xcode that will work with code compiled with Apple's g++? I've tried using the one in Xcode and it simply gets stuck at the "base" class constructor. It won't even step into it - that's one of the reasons I suspect the compiler is essentially optimizing away the constructor.Warty
Try to compile with -fsanitize=address and run your program. It's available in both clang and gcc. This tool is quite good at catching such kind of problems. Most likely you have undefinded behaviour in your code.Easygoing
"The source code is too long to post and I cannot reproduce it with a simpler code fragment." So, try to manufacture minimal reproducible example. If you can't reproduce the issue with smaller code fragment, you are taking out too much of the code (maybe the issue is due to the code you deemed irrelevant?). Typical cause of such issues is undefined behavior somewhere in your code. Can't tell more without, at least, minimal reproducible example.Southing
Well i might be wrong but if the segfault happens at base class constructor then issue is in base class constructor and you can extract that into a new program so it can crash independentlyApotheosize
So to boil everything down -- you have bugs in a big program. Also, it doesn't matter how many compilers it runs on -- that one compiler that is used extensively throughout the industry that shows the error is what you should be concerned about.Etam
@Warty -- I'm looking for pointers on how to figure out if this is an actual bug in my code -- These types of issues really can be spotted by an experienced C++ programmer who knows most, if not all of the pitfalls. If you don't know what to look for, you aren't going to find the problem since everything will look "ok" to you.Etam
Well, extract a minimal example, even if that minimal example is large. First step is to copy all code together into a single file. If that already "fixes" the issue, you might be violating the One Definition Rule (ODR), for example.Abad
The beauty of making a MCVE is as you narrow in on isolating the problem code, the bug often jumps out and dances around, mocking you for not spotting it sooner.Wisla
I have managed to create a MCVE for this problem that boils the code down to 72 lines. Is this too long to post here? Assuming not, what is the best way to post it?Warty
It doesn't seem possible to post a code sample in a comment, and the system won't let me post another question (question limit is 1??)... so I am not sure how to go about posting my MCVE.Warty
I added the MCVE by editing my original post... sorry for being a bumbling idiot, I'm new to this forum.Warty
@Warty if (bools_) delete bools_; -- 1) Wrong form of delete. It should be delete []. 2) There is no need to check for null when issuing a call to delete[].Etam
@Etam - thanks, corrected with an edit. But that doesn't solve or explain the segfault issue.Warty
I've updated my MCVE to remove all use of Base::bools_, the Base::Initialize method and the virtual method Base::Rule. This new code still crashes.Warty
A user on another forum pointed out that the two levels of inheritance are not necessary to make the code crash. Remove the definition of Derived_B and instantiate a Derived_A object instead, and the code will still crash with the latest clang.Warty
@Etam Well, this time it seems to be a bug in Clang.Ursel
U
14

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; }

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]

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.

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 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.

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.

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 document can be found here: https://refspecs.linuxfoundation.org/cxxabi-1.75.html

It explicitly mentions nvalign:

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:

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).

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.


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:

Ursel answered 10/1, 2018 at 20:2 Comment(5)
Found another question on this: #46474738Ursel
Thank you, yes I have gotten this answer also on another board, particularly the hint to compile with -fsanitize=undefined, and verified that the problem is incorrect address alignment. Thank you for the detailed explanation though, that is way beyond the level of my knowledge of assembly code generation. In any case, I submitted a bug report to Apple. The bug reference number is 36382481, in case you would like to add any information, or reference it in your own bug report, if you plan to or have submitted one.Warty
@Warty I am going to submit bug reports to both clang and gcc tomorrow. I will edit the answer and add link to them later.Ursel
I will definitely bookmark this question / answer. As one of the freak cases where a low-rep newcomer reports a problem and it actually is a bug in the compiler. :-DSodomite
3 compiler bugs, hats off to you!Pervasive

© 2022 - 2024 — McMap. All rights reserved.