TL:DR: It's a missed-optimization by gcc.
noreturn
is a promise to the compiler that the function won't return. This allows optimizations, and is useful especially in cases where it's hard for the compiler to prove that a loop won't ever exit, or otherwise prove there's no path through a function that returns.
GCC already optimizes main
to fall off the end of the function if func()
returns, even with the default -O0
(minimum optimization level) that it looks like you used.
The output for func()
itself could be considered a missed optimization; it could just omit everything after the function call (since having the call not return is the only way the function itself can be noreturn
). It's not a great example since printf
is a standard C function that is known to return normally (unless you setvbuf
to give stdout
a buffer that will segfault?)
Lets use a different function that the compiler doesn't know about.
void ext(void);
//static
int foo;
_Noreturn void func(int *p, int a) {
ext();
*p = a; // using function args after a function call
foo = 1; // requires save/restore of registers
}
void bar() {
func(&foo, 3);
}
(Code + x86-64 asm on the Godbolt compiler explorer.)
gcc7.2 output for bar()
is interesting. It inlines func()
, and eliminates the foo=3
dead store, leaving just:
bar:
sub rsp, 8 ## align the stack
call ext
mov DWORD PTR foo[rip], 1
## fall off the end
Gcc still assumes that ext()
is going to return, otherwise it could have just tail-called ext()
with jmp ext
. But gcc doesn't tailcall noreturn
functions, because that loses backtrace info for things like abort()
. Apparently inlining them is ok, though.
Gcc could have optimized by omitting the mov
store after the call
as well. If ext
returns, the program is hosed, so there's no point generating any of that code. Clang does make that optimization in bar()
/ main()
.
func
itself is more interesting, and a bigger missed optimization.
gcc and clang both emit nearly the same thing:
func:
push rbp # save some call-preserved regs
push rbx
mov ebp, esi # save function args for after ext()
mov rbx, rdi
sub rsp, 8 # align the stack before a call
call ext
mov DWORD PTR [rbx], ebp # *p = a;
mov DWORD PTR foo[rip], 1 # foo = 1
add rsp, 8
pop rbx # restore call-preserved regs
pop rbp
ret
This function could assume that it doesn't return, and use rbx
and rbp
without saving/restoring them.
Gcc for ARM32 actually does that, but still emits instructions to return otherwise cleanly. So a noreturn
function that does actually return on ARM32 will break the ABI and cause hard-to-debug problems in the caller or later. (Undefined behaviour allows this, but it's at least a quality-of-implementation problem: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158.)
This is a useful optimization in cases where gcc can't prove whether a function does or doesn't return. (It's obviously harmful when the function does simply return, though. Gcc warns when it's sure a noreturn function does return.) Other gcc target architectures don't do this; that's also a missed optimization.
But gcc doesn't go far enough: optimizing away the return instruction as well (or replacing it with an illegal instruction) would save code size and guarantee noisy failure instead of silent corruption.
And if you're going to optimize away the ret
, optimizing away everything that's only needed if the function will return makes sense.
Thus, func()
could be compiled to:
sub rsp, 8
call ext
# *p = a; and so on assumed to never happen
ud2 # optional: illegal insn instead of fall-through
Every other instruction present is a missed optimization. If ext
is declared noreturn
, that's exactly what we get.
Any basic block that ends with a return could be assumed to never be reached.