UB is bizarre in this case

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:

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

  2. Is there a better way to force a crash?

Thanks,
jrs

You can use __builtin_trap() or __builtin_unreachable(), for example.

LLVM assumes that the null pointer access can never happen at runtime and uses that to determine that the else branch can never execute. Hence ShouldCrash() == false must always be true and there is no need to evaluate ShouldCrash, unless it has side effects.

I think you’ll need to use __builtin_trap, with __builtin_unreachable LLVM will also determine that the else branch can never execute.

1 Like