We ran into a problem with the CGT 8.5.0 that we did not observe with earlier compilers. I tried to extract a relatively minimal example that I attached. If you compile the code with -O2 optimizations, the compiler emits code that tries to read a pointer before it is initialized. In our case, this resulted from reading random stack content and treating it as a pointer, which led to various crashes further down the line.
This is the code in question:
#include <algorithm>
struct spi
{
void callme();
};
struct tag {};
class sp
{
void* p;
spi* i;
public:
sp() : p(nullptr), i(nullptr) {}
sp(void* p, spi* i) : p(p), i(i) {} // not strictly necessary to trigger the bug (but before the compiler jumps to conclusions...)
/*
* Replace the following initialization of i with some non-null value and the bug disappears
*
* static spi gspi;
* sp(void* p, const tag&) : p(p), i(&gspi) {}
*
*/
sp(void* p, const tag&) : p(p), i(nullptr) {}
~sp() { if (i) i->callme(); }
void swap(sp& o)
{
std::swap(p, o.p);
std::swap(i, o.i);
}
};
// some (incomplete) struct
struct inc;
void somefunc(void *, void (inc::*)()) {}
class fn
{
private:
void (inc::*mf)();
void (*minv)(void *, void (inc::*)());
sp s;
int type;
public:
#pragma FUNC_CANNOT_INLINE
fn(void (inc::*f)(), inc* o)
: type((f && o) ? 3 : 0) // condition needs to be present to trigger bug
{
if(f && o) // condition must be present to trigger bug (because otherwise construction of s is optimized)
{
// at least minv must be present and written to to trigger the bug
mf = f;
minv = &somefunc;
sp(o, tag{}).swap(s);
}
}
};
fn buildfn(void (inc::*f)(), inc *o)
{
return fn(f, o);
}
The fn constructor shoud default-initialize the sp member, which in turn sets both sp members to nullptr. If both arguments are valid, the sp member is then swapped with another object that has the first pointer set to some non-null value while the second pointer is still null (sp is a stripped down shared_ptr implementation). The fact that the second pointer is still null seems to be relevant.
The temporary used for swapping is the destroyed. Note that it should have both pointers set to zero, so its destructor should do nothing (the compiler doesn't see that (GCC and Clang do) but that's another story). The compiler emits code to load s.i and then call callme() on that object (if non-null):
...
[ A0] LDW .D2T1 *B6(4),A0 ; [B_D64P] |4459|
...
MV .L1 A0,A4 ; [A_L64P] |28|
...
[ A0] CALL .S1 _ZN3spi6callmeEv ; [A_S64P] |28|
However, it doesn't bother to initialize B6(4) with zero before the load but does so only after the load:
ADD .L2X 12,A4,B6 ; [B_L64P] |16|
...
ZERO .S2 B8 ; [B_Sb64P] |55|
ZERO .D2 B9 ; [B_D64P] |55|
...
[ A0] LDW .D2T1 *B6(4),A0 ; [B_D64P] |4459|
...
STNDW .D1T2 B9:B8,*A10(12) ; [A_D64P] |16|
...
MV .L1 A0,A4 ; [A_L64P] |28|
...
[ A0] CALL .S1 _ZN3spi6callmeEv ; [A_S64P] |28|
The STNDW instruction is used to set both sp members to 0 (A10(12) is the address of the first pointer, storing a double word also overwrites B6(4), which aliases that range).
I am not sure what exactly triggers this behavior but it appears to be a rather severe bug. We decided to revert to 8.3.x for now, as we can work around this specific problem (by tweaking this code and inspecting the output) but without understanding what causes the issue we do not trust the rest of the binary either.
I'd be grateful if anyone could have a closer look at this issue.
Regards
Markus Moll