Attention people who think UB is fine
blog.habets.se
blog.habets.se
Another alternative is to use `__builtin_unreachable()` (not standard, but available in GCC and Clang).
Before that the observable side-effect, device_command=42, has already happened. With time travel allowed, could also have been omitted, which would have been a real problem.
I misread the article to think that is was about the latter problem. You are right, my mistake. I didn't consider that anyone could see the former as a problem at all: It's just the compiler doing exactly what it's told to do.
It's not a command to compiler to stop generating code.
It's a permission slip that allows the compiler to assume that code that follows is dead code, eligible to be optimized out, including eliminating potentially dead stores and loads.
Which is to say, it's precisely what any other UB is, which is why deliberate use of UB is very bad idea.
double f(void* p) {
return * (double*) p;
}
No one is surprised that defensive code isn't issued to guard against p being NULL, unaligned, or pointing to a uint16_t. You told the compiler that p is actually pointing to a double, and it generates code accordingly.It's the same thing here: You told the compiler very explicitly that anything following this point is unreachable, and it generated code accordingly.
I'm not defending writing code with UB. I would have preferred the compiler issuing a warning saying "you say this is unreachable, I don't believe that it is", when it's this obvious. Maybe issue a trap instruction. But there's nothing wrong it, per se. When you choose to override the compiler's judgment and insist that this point in code is unreachable, then the compiler takes your word for it.
Of course I would also prefer a warning, or at least a guarding trap instruction. And maybe compilers should try harder to be friendly in this way.
But they can't be perfect, because the program specified something syntactically valid, but semantically invalid. The module/compiler pass that could theoretically detect it may simply not be issued the necessary information. E.g. a solver may be given the input that a value is in a given range, and from that simplify an operation. If the input is wrong, so is the output.
Reminds me of:
On two occasions I have been asked, 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question.
In the example from the article, though, there could be a pass that simply eliminated dead code. That pass is not a parser of the raw code, so how is it to know that actually, the "dead code" isn't?And a standard that would require dead code elimination to replace not with empty, but with a trap instruction, could go very wrong. You don't want `if (false) { … }` to become `if (false) { trap(); }`. You want it just removed, if a previous pass deemed it dead. Or do you want `int main() { for(;;) {cli_loop();} }` to force adding a trap in the unreachable code after the loop? That's a lot of generated trap instructions.
So in general this means solving the halting problem. Yes, specifically in the post's example, "surely a sufficiently smart compiler could detect something this obvious". Maybe so, but how often is the bug actually this obvious?
Now if this actually made it into the C++26 standard, I've not paid to get the official one, but it's in the N5050 draft: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/n50...
Yes, into the shame cube I go. It doesn't change anything, and it applies equally to C and C++, but you are correct. Thanks.
Even if that's the case, it's not "undefined" in the sense that it doesn't permit "now your program is the attacker's program".
Here are two examples of where it's clear that the UB from the underlying C is still present and the optimization passes are still able to do things that wouldn't be possible without UB, like falling through functions and deleting safety checks inserted by the Fil-C runtime:
https://news.ycombinator.com/item?id=49677463 https://news.ycombinator.com/item?id=49681273
I'm not really convinced you can take LLVM and just flip a few poison bits off and remove a couple optimization passes and know that UB isn't going to explode in your face.
But then it's not UB, and not the topic at hand. So I wouldn't call it "sort of true" for a statement about UB, because it doesn't apply to not-UB.
It's also different in a consteval context. But that doesn't make it less true outside consteval context.
You saw it yourself: unreachable generates no code and suppresses the function epilog (duh - it's unreachable) but null deref generated a trap instruction in GCC. If you wanted a trap instruction, maybe this compiler reliably generates one from a hardcoded null deref.
When you say "this UB does ... (like generates a trap instruction)" you are being hyper specific for a given compiler version and platform and optimization level and any other knob. It may not stay the same after you update the compiler. And what happens a lot is that newer, better optimizations (such as more opportunity to autovectorization), which attract people to newer compiler versions, may exploit UB in more creative ways.
For example, to exploit autovectorization, compilers may want to push some test out of a loop. If this test contains UB, this optimization will change where the UB is triggered. So that trap instruction may happen at a different place in the code, depending on optimizations.
> "the definition of std::unreachable is: invokes undefined behaviour" does not mean that all undefined behaviour is equivalent to calling std::unreachable.
Well, obviously not. The definition of UB is that anything can happen, so by transitive property it means that std::unreachable can trigger any behavior.
That's the whole point of UB, and the point of my post: It's UB! You cannot reason about what will happen! The whole point of the post is "No you CANNOT reason about what will happen". So of course I am making no claims about what will happen, just three interesting observations about what did in fact happen when I happened to try it, to prove this point.
> If you wanted a trap instruction, maybe this compiler reliably generates one from a hardcoded null deref.
No, even in the examples in my post I show that it's not reliable. It's UB!
Hell, we get very different output just between -O1 (https://godbolt.org/z/33e1nGE38) and -O2 (https://godbolt.org/z/3coTqP31W).
[1] https://doc.rust-lang.org/std/macro.unreachable.html [2] https://doc.rust-lang.org/std/hint/fn.unreachable_unchecked....
So the fact that `unreachable!()` does not require the use of the keyword `unsafe` means that it can't be like C++ `std::unreachable()`.
Though, it would be better if programmers read deliberate instances of UB as if they literally said "std::unreachable()" at that point in the code, as that would better convey how the optimizer passes will see things after dozens of frontend and middle-end passes have run before it.
Except that isn't true.
There are basically two kinds of undefined behavior.
The first is undefinable behavior, the kind that easily goes disastrously wrong, like writing to random memory locations.
The second is incidentally undefined behavior, something where there just isn't enough implementation consensus for the standard to settle on it, like realloc(p, 0).
There's no standard terminology I'm aware of to distinguish the two. But the distinction exists.
And where programmers read deliberate instances of UB, it's usually the latter, e.g., writing directly to an address memory mapped to device I/O in that particular environment.
There is: implementation-defined behavior. Your example of writing to an MMIO address is such an example, as the standard says casting an int to a pointer is implementation-defined. (However, the C standard has plenty of cases of undefined behavior that probably should have been implementation-defined.)
This distinction exists, but not to the compiler. And the compiler is the thing building your binary. You can complain if you like about how UB that could potentially instead have been implementation-defined is UB instead.
But until then, the compiler will mangle your code either way, whether you feel it is fair or not.
If you think a point in the program should not be reached unless there is a bug somewhere, you can write abort() rather than unreachable(), among other possibilities.
abort() doesn't return; any code after it is actually unreachable; we can confidently write
abort();
unreachable(); // redundant
The compiler can already assume that abort() doesn't return, and so anything after it is unreachable. But it may not assume that abort() itself is not reached.So in essence we can get some of the same optimizations, somewhat blunted, so to speak.
For instance, if we put unreachable() at the end of a function, the compiler can omit generating the code for returning from the function; if that point is reached, the instruction pointer will wander off into whatever instructions occur after that.
If we put abort() at the end of a function, the compiler can likewise omit generating the code for returning from the function. But the instruction pointer won't wander off: there will be a call or jump to abort. So, not quite as optimized in size, but optimized still.
No. That's just one of the MANY ways to trigger UB. In the article I also trigger UB by dereferencing a null pointer instead.
In fact, here's an example where calling `unreachable()` and dereferencing a null pointer produce the EXACT same assembly: https://godbolt.org/z/538qfj9z9