为什么“noreturn”函数返回?
我读了关于noreturn
属性的这个问题,它用于不返回给调用者的函数。
然后我用C做了一个程序
#include <stdio.h> #include <stdnoreturn.h> noreturn void func() { printf("noreturn func\n"); } int main() { func(); }
并使用此代码生成组装:
.LC0: .string "func" func: pushq %rbp movq %rsp, %rbp movl $.LC0, %edi call puts nop popq %rbp ret // ==> Here function return value. main: pushq %rbp movq %rsp, %rbp movl $0, %eax call func
为什么函数func()
在提供noreturn
属性后返回?
C中的函数说明符是编译器的提示 ,接受的程度是实现定义的。
首先, _Noreturn
函数说明符(或者, noreturn
,使用<stdnoreturn.h>
)是编译器提示程序员做出的理论承诺 ,该函数永远不会返回。 基于这个承诺,编译器可以做出某些决定,对代码生成进行一些优化。
IIRC,如果用noreturn
函数指定符指定的函数最终返回给它的调用者,
- 通过使用和显式的
return
语句 - 达到function体的尽头
行为是不确定的 。 你不能从函数返回。
为了清楚noreturn
,使用noreturn
函数说明符不会停止函数forms返回到调用者。 这是程序员向编译器做出的一个承诺,它允许更多的自由度来生成优化的代码。
现在,如果你早晚做出了承诺,select违反这个,结果是UB。 当_Noreturn
函数似乎能够返回给调用者时,鼓励编译器产生警告,但不是必需的。
根据§6.7.4, C11
,第8段
用
_Noreturn
函数说明符声明的函数不应返回给调用者。
和第12段,( 注意评论!! )
EXAMPLE 2 _Noreturn void f () { abort(); // ok } _Noreturn void g (int i) { // causes undefined behavior if i <= 0 if (i > 0) abort(); }
对于C++
,行为非常相似。 从§7.6.4章节引用C++14
第2段( 强调我的 )
如果函数
f
被调用,其中f
先前是用noreturn
属性声明的,而f
最终返回,则行为是未定义的。 [注意:函数可以通过抛出exception来终止。 – 注意][注意:如果标有
[[noreturn]]
可能返回,鼓励实现发出警告。 – 注意]3 [例如:
[[ noreturn ]] void f() { throw "error"; // OK } [[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0 if (i > 0) throw "positive"; }
– 例子]
为什么函数func()在提供noreturn属性后返回?
因为你写的代码告诉它。
如果你不想你的函数返回,调用exit()
或者abort()
或者类似的方法,这样它就不会返回。
调用printf()
之后,除了返回函数外,还有什么function呢?
6.7.4函数说明符中的C标准 ,第12段特别包含了一个实际返回的noreturn
函数的例子 – 并将行为标记为未定义 :
例2
_Noreturn void f () { abort(); // ok } _Noreturn void g (int i) { // causes undefined behavior if i<=0 if (i > 0) abort(); }
简而言之, noreturn
是你放在你的代码上的限制 – 它告诉编译器“我的代码永远不会返回” 。 如果你违反了这个限制,这一切都在你身上。
noreturn
是一个承诺。 你告诉编译器:“它可能也可能不明显,但是我知道,根据我写代码的方式,这个函数永远不会返回。” 这样,编译器可以避免设置允许函数正常返回的机制。 将这些机制排除在外可能会使编译器生成更高效的代码。
一个函数怎么不能返回? 例如,如果它调用exit()
来代替。
但是,如果你承诺编译器,你的函数将不会返回,并且编译器不安排它可以让函数正常返回,然后你去写一个函数返回,编译器应该是什么做? 它基本上有三种可能性:
- 对你“很好”,想办法让函数正常返回。
- 发出代码,当函数不正确地返回时,它会以任意不可预知的方式崩溃或行为。
- 给你一个警告或错误信息,指出你违背了诺言。
编译器可能会做1,2,3或一些组合。
如果这听起来像未定义的行为,那是因为它。
在现实生活中编程的底线是:不要做出你不能保持的承诺。 别人可能会根据你的诺言做出决定,如果你违背诺言,就会发生坏事。
noreturn
属性是您对编译器提供的有关函数的承诺。
如果你从这样一个函数返回,行为是不确定的,但这并不意味着一个理智的编译器将允许你通过去除ret
语句来完全弄乱应用程序的状态,尤其是因为编译器通常甚至能够推断回报确实是可能的。
但是,如果你写这个:
noreturn void func(void) { printf("func\n"); } int main(void) { func(); some_other_func(); }
那么编译器完全去除some_other_func
是完全合理的,如果感觉就像这样。
正如其他人所说,这是典型的未定义的行为。 你答应func
不会返回,但你仍然返回。 rest时你可以拿起那些东西。
尽pipe编译器以通常的方式编译func
(尽pipe你是直接编译的),但是noreturn
会影响调用函数。
你可以在汇编列表中看到这个:编译器已经假设, main
, func
不会返回。 因此,它真的删除了所有在call func
之后的代码(请参阅https://godbolt.org/g/8hW6ZR )。 程序集清单不会被截断,它实际上只是在call func
之后结束,因为编译器假定之后的任何代码都是不可访问的。 所以,当func
实际返回时, main
将开始执行main
函数后面的任何废话 – 不pipe是填充,即时常量还是00
字节的大海。 再次 – 非常不明确的行为。
这是传递性的 – 在所有可能的代码path中调用noreturn
函数的函数本身可以被假定为是noreturn
。
据此
如果声明_Noreturn的函数返回,则行为是未定义的。 如果可以检测到,则build议使用编译器诊断。
程序员有责任确保这个函数永不返回,例如在函数结束时退出(1)。
ret
只是意味着该函数将控制权返回给调用者。 因此, main
call func
,CPU执行该函数,然后,用ret
,CPU继续执行main
。
编辑
所以, 事实certificate , noreturn
并没有使函数不能返回,它只是一个说明符,告诉编译器这个函数的代码是以这样一种方式编写的,函数不会返回 。 所以,你应该在这里做的是确保这个函数实际上不会返回控制权给被调用者。 例如,你可以在里面调用exit
。
另外,鉴于我已经读过关于这个说明符,似乎为了确保函数不会返回到它的调用点,应该调用另一个 noreturn
函数在里面,并确保后者总是运行(在为了避免未定义的行为)并不会导致UB本身。
没有返回function不能保存条目上的寄存器,因为它是没有必要的。 它使优化更容易。 非常适合调度程序例如。
看到这里的例子: https : //godbolt.org/g/2N3THC和发现差异
TL:DR:这是由gcc错过的优化 。
noreturn
是编译器的一个承诺,即函数不会返回。 这允许优化,特别是在编译器难以certificate循环不会退出的情况下,或者certificate没有通过返回函数的path的情况下尤其有用。
如果func()
返回,GCC已经优化了main
函数,即使使用默认的-O0
(最小优化级别),它看起来就像你用的那样。
func()
本身的输出可以被认为是错过的优化; 它可能只是省略函数调用后的所有内容(因为调用不返回是函数本身可以返回的唯一方法)。 这不是一个很好的例子,因为printf
是一个标准的C函数,已知它会正常返回(除非你设置了一个缓冲区来设置segfault?
让我们使用编译器不知道的另一个函数。
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); }
( 在Godbolt编译器资源pipe理器上的代码+ x86-64 asm )。
bar()
gcc7.2输出很有意思。 它内联func()
,并消除foo=3
死亡存储,只留下:
bar: sub rsp, 8 ## align the stack call ext mov DWORD PTR foo[rip], 1 ## fall off the end
海湾合作委员会仍然认为ext()
将返回,否则它可能只是与jmp ext
尾称为ext()
。 但是,gcc并不会去除noreturn
,因为这会丢失 abort()
这样的事件的回溯信息 。 显然内联他们是好的,但。
在call
之后,Gcc可以通过省略mov
store来进行优化。 如果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
而不保存/恢复它们。
GCC for ARM32实际上是这样做的,但仍然发出指令,否则干净地返回。 所以在ARM32上实际返回的noreturn
函数将会破坏ABI,并在调用者或更高版本中导致难以debugging的问题。 (未定义的行为允许这样做,但至less是一个质量问题: https : //gcc.gnu.org/bugzilla/show_bug.cgi?id = 82158。 )
在gcc无法certificate函数是否返回的情况下,这是一个有用的优化。 (当函数返回时显然是有害的,但是当确定一个noreturn函数返回时,gcc会发出警告。)其他的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
,那正是我们得到的。
以回报结束的任何基本块都可以假定为永远不会到达。