I suspect I might be chastised for asking about undefined behavior and that UB is a license for the compiler to do whatever it wants. Please be kind.
I’d hoping to get some clarity on what the compiler is attempting to optimize on and if there’s a better way to accomplish what we want.
This is a minified example of a weird case we found in our XCode (Clang 14.0) Intel build recently. We have a function that gets invoked when a critical assert condition fails. And depending on the configuration, it may force a crash at runtime or do something else. Here’s a minimal example of what we have:
extern bool ShouldCrash();
void InvokeNoCrashBehavior();
void FailedAssertHandler()
{
if (ShouldCrash() == false)
{
InvokeNoCrashBehavior();
}
else
{
int *fault = nullptr;
fault[0] = 0;
}
}
With -O2
optimizations, it generates the following x86 assembly as seen on Godbolt: Compiler Explorer
FailedAssertHandler(): # @FailedAssertHandler()
push rax
call ShouldCrash()
pop rax
jmp InvokeNoCrashBehavior() # TAILCALL
The above assembly just has the result of ShouldCrash()
ignored and InvokeNoCrashBehavior() is called unconditionally.
A quick tweak to either declare fault
as volatile, insert another function call within the else block, or to do the assignment as fault[1] = 1
results in the return value from ShouldCrash getting evaluated. Here’s the output with fault declared as volatile int* fault = nullptr;
Compiler Explorer
FailedAssertHandler(): # @FailedAssertHandler()
push rax
call ShouldCrash()
test al, al // EVALUATE AND JUMP ON RETURN VALUE FROM ShouldCrash
je .LBB0_2
mov dword ptr [0], 0
pop rax
ret
.LBB0_2:
pop rax
jmp InvokeNoCrashBehavior() # TAILCALL
So I have two questions:
-
What is the compiler optimizing on when it seens the deliberate null pointer access. And even if it’s UB to do this, shouldn’t the return value of ShouldCrash still be evaluated?
-
Is there a better way to force a crash?
Thanks,
jrs