TL:DR:这是gcc的优化失误。
`noreturn`是对编译器的承诺,表示该函数不会返回。这允许进行优化,在编译器难以证明循环永远不会退出或者无法证明函数没有返回路径的情况下特别有用。
GCC已经将`main`优化为在`func()`返回时跳出函数,即使使用默认的`-O0`(最低优化级别),看起来您也使用了这个级别。
`func()`本身的输出可能被认为是一种未能优化;它可以省略函数调用后的所有内容(因为使调用不返回是函数本身成为`noreturn`的唯一方法)。这不是一个很好的例子,因为`printf`是一个标准的C函数,通常情况下它会返回(除非您使用`setvbuf`为`stdout`提供一个缓冲区,从而导致段错误?)
我们可以使用另一个编译器不知道的函数来演示。
void ext(void);
int foo;
_Noreturn void func(int *p, int a) {
ext();
*p = a;
foo = 1;
}
void bar() {
func(&foo, 3);
}
(
在Godbolt编译器资源管理器上进行Code和x86-64汇编。)
gcc7.2对于
bar()
的输出很有趣。它内联了
func()
,并消除了
foo=3
的无用存储,只留下:
bar:
sub rsp, 8 ## align the stack
call ext
mov DWORD PTR foo[rip], 1
## fall off the end
Gcc仍然假定
ext()
会返回,否则它可以使用
jmp ext
进行尾调用
ext()
。但是gcc不会对
noreturn
函数进行尾调用,因为这会
丢失回溯信息,例如
abort()
。不过,显然内联它们是可以的。
此外,gcc还可以通过省略
call
后的
mov
存储来进行优化。如果
ext
返回,则程序将失败,因此没有生成任何该代码的必要。 Clang在
bar()
/
main()
中进行了该优化。
"
func
本身更有趣,而且是一个更大的优化遗漏.
gcc和clang都发出几乎相同的东西:
"
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
该函数可以假设它不返回,并且在不保存/恢复它们的情况下使用
rbx
和
rbp
。
ARM32的Gcc实际上确实这样做,但仍会发出指令以否则干净地返回。 因此,在ARM32上实际返回的
noreturn
函数将破坏ABI并导致调用者或后续程序中难以调试的问题。(未定义的行为允许此操作,但至少是实现质量问题:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158。)
在gcc无法证明函数是否返回时,这是一种有用的优化。(当函数只是返回时,这显然是有害的。当gcc确定noreturn函数确实返回时,会发出警告。)其他gcc目标架构不会这样做;这也是一种错过的优化。
但是gcc还不够:优化掉返回指令(或用非法指令替换它)将节省代码大小并保证产生嘈杂的失败而不是静默的损坏。
如果你要优化掉
ret
,那么优化掉只有在函数返回时才需要的所有内容是有意义的。因此,
func()
可以被编译为:
sub rsp, 8
call ext
# *p = a; and so on assumed to never happen
ud2 # optional: illegal insn instead of fall-through
每个其他存在的指令都是一个被错过的优化机会。如果声明了
ext
为
noreturn
,那么我们就得到了确切的结果。
任何以返回结尾的
基本块都可以假定永远不会被执行到。
noreturn
属性的问题。不,你链接的问题是关于C++语言中的noreturn
属性,这是一种不同的语言。 - Gerhardhexit
或等效方式结束的错误处理不会将叶函数转变为非叶函数。因此,您可以在性能关键代码的最深处使用assert
或类似语句,而这不会比正确预测的分支成本更高(而不是该函数必须设置框架、保存寄存器、避免调用者保存的寄存器等)。 - Artret
指令只意味着执行将返回到调用者。对于返回值为void
的函数,情况也是如此。换句话说,它返回控制权,而不是一个值。现在,所有的x86调用约定都会在EAX
寄存器中返回值,但是调用方和被调用方必须就此达成协议,因此如果函数返回void
或无返回值,则EAX
将只包含垃圾值。与前面的答案一样,根据C语言标准,这种行为是未定义的,但这就是汇编的意思。 - Cody Gray