我读了this关于noreturn
属性的问题,该属性用于不返回调用者的函数。
然后我在C中制作了一个程序。
#include <stdio.h>
#include <stdnoreturn.h>
noreturn void func()
{
printf("noreturn func\n");
}
int main()
{
func();
}
使用this生成代码汇编:
.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
会返回?
答案 0 :(得分:119)
C中的函数说明符是编译器的提示,接受程度是实现定义的。
首先,_Noreturn
函数说明符(或noreturn
,使用<stdnoreturn.h>
)是编译器关于理论承诺的提示。程序员,这个函数永远不会返回。基于此承诺,编译器可以做出某些决定,对代码生成执行一些优化。
IIRC,如果用noreturn
函数说明符指定的函数最终返回其调用者,
return
声明behaviour is undefined。你 MUST NOT 从函数返回。
为清楚起见,使用noreturn
函数说明符不会停止函数表单返回其调用者。这是程序员对编译器的承诺,允许它更自由地生成优化代码。
现在,如果您早先及以后做出承诺,请选择违反此规则,结果为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
,[[noreturn]]
最终 返回,行为未定义。 [注意:该函数可能会因抛出异常而终止。 -结束 注意][注意:如果标记为
[[ 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"; }
的函数可能会鼓励实施,则会发出警告 返回。 - 尾注]3 [例如:
with open('sample_plan.xml', 'rb') as payload: headers = {'auth-token': 'abCdeFgh'} files = {'data': ('sample_plan.xml', payload, 'text/xml')} req = requests.post(url='http://abc123.com/index.php/plan/', \ headers=headers, files=files)
-end example]
答案 1 :(得分:47)
为什么在提供noreturn属性后函数func()会返回?
因为你编写了告诉它的代码。
如果您不希望自己的功能退回,请拨打exit()
或abort()
或类似内容,以便其无法返回。
else 你的函数除了在调用printf()
后返回时还会做什么?
6.7.4函数说明符中的C Standard,第12段具体包含了一个可以实际返回的noreturn
函数的示例 - 并将行为标记为未定义:
示例2
_Noreturn void f () {
abort(); // ok
}
_Noreturn void g (int i) { // causes undefined behavior if i<=0
if (i > 0) abort();
}
简而言之,noreturn
是一个限制 你放在你的代码上 - 它告诉编译器& #34;我的代码永远不会回归&#34; 。如果你违反了这个限制,那一切都在你身上。
答案 2 :(得分:25)
yarn start
是一个承诺。你告诉编译器,“它可能会或可能不会很明显,但我根据我编写代码的方式知道这个函数永远不会返回。”这样,编译器可以避免设置允许函数正确返回的机制。省略这些机制可能允许编译器生成更有效的代码。
一个函数如何不返回?一个例子是,如果它改为调用noreturn
。
但是如果你向编译器保证你的函数不会返回,并且编译器没有安排它使函数能够正确返回,那么你去写一个做的函数< / em> return,编译器应该做什么?它基本上有三种可能性:
编译器可能会执行1,2,3或某种组合。
如果这听起来像未定义的行为,那是因为它是。
在现实生活中编程的底线是:不要做出你无法保留的承诺。其他人可能已根据您的承诺做出决定,如果您违背承诺,可能会发生不好的事情。
答案 3 :(得分:15)
noreturn
属性是您向编译器提供有关您的函数的承诺。
如果做从这样的函数返回,行为是未定义的,但这并不意味着理智的编译器将允许您通过删除{{1}完全混乱应用程序的状态}语句,特别是因为编译器通常甚至能够推断出返回确实是可能的。
但是,如果你这样写:
ret
那么编译器完全删除noreturn void func(void)
{
printf("func\n");
}
int main(void)
{
func();
some_other_func();
}
是完全合理的,如果感觉就好了。
答案 4 :(得分:11)
正如其他人所提到的,这是经典的未定义行为。你承诺func
不会回来,但无论如何你都让它回来了。你可以在休息时拿起它们。
虽然编译器以通常的方式编译func
(尽管你的noreturn
),noreturn
会影响调用函数。
您可以在程序集列表中看到这一点:编译器在main
中假定func
不会返回。因此,它完全删除了call func
之后的所有代码(请参阅https://godbolt.org/g/8hW6ZR处的自己)。汇编列表没有被截断,它实际上只是在call func
之后结束,因为编译器假定之后的任何代码都无法访问。因此,当func
确实返回时,main
将开始执行main
函数后面的任何废话 - 无论是填充,立即常量还是00
字节的大海。再次 - 非常不确定的行为。
这是传递性的 - 在所有可能的代码路径中调用noreturn
函数的函数本身可以假定为noreturn
。
答案 5 :(得分:8)
答案 6 :(得分:6)
ret
只是意味着该函数将 control 返回给调用者。因此,main
执行call func
,CPU执行该函数,然后,使用ret
,CPU继续执行main
。
修改的
所以,它turns out,noreturn
使函数根本不返回,它只是一个告诉编译器的说明符该函数的代码是以函数不会返回的方式编写的。所以,你应该做的是确保这个函数实际上不会将控制权返还给被调用者。例如,您可以在其中调用exit
。
另外,鉴于我已经阅读过关于此说明符的内容,似乎为了确保函数不会返回其调用点,应该调用另一个 { {1}}在其中运行并确保后者始终运行(以避免未定义的行为)并且不会导致UB本身。
答案 7 :(得分:6)
没有返回功能不会保存条目上的寄存器,因为它不是必需的。它使优化更容易。例如,非常适合调度程序例程。
请参阅此处的示例: https://godbolt.org/g/2N3THC并发现差异
答案 8 :(得分:2)
TL:DR:这是gcc 的错过优化。
noreturn
是对编译器的承诺,该函数不会返回。这允许优化,特别是在编译器很难证明循环不会退出的情况下,或者证明没有路径通过返回的函数时,这种情况很有用。
如果main
返回,GCC已经优化func()
以使函数结束,即使使用默认的-O0
(最低优化级别),它也会使用。{/ p>
func()
本身的输出可视为错过优化;它可以在函数调用之后省略所有内容(因为调用不返回是函数本身可以noreturn
)的唯一方式。这不是一个很好的例子,因为printf
是一个已知正常返回的标准C函数(除非你setvbuf
给stdout
一个会出现段错误的缓冲区?)
让我们使用编译器不知道的不同函数。
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);
}
( {+ 3}}上的代码+ 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
Gcc仍然认为ext()
将会返回,否则它可能只是尾部调用ext()
jmp ext
。但是gcc不会对noreturn
函数进行尾调用,因为abort()
之类的内容会mov
。显然,将它们列入内容是可以的。
Gcc可以通过省略call
之后的ext
商店进行优化。如果bar()
返回,则程序被清除,因此生成任何代码都没有意义。 Clang确实在main()
/ func
中进行了优化。
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
本身更有趣,错失优化。
gcc和clang两者几乎都发出同样的事情:
rbx
此函数可以假定它不会返回,并使用rbp
和noreturn
而不保存/恢复它们。
ARM32的Gcc实际上是这样做的,但仍会发出指令,否则会干净利落地返回。因此,在ARM32上实际返回的ret
函数将破坏ABI并导致调用者或更高版本中的难以调试的问题。 (未定义的行为允许这样做,但它至少是一个实施质量问题:Godbolt compiler explorer。)
在gcc无法证明函数是否返回的情况下,这是一个有用的优化。 (当函数确实返回时,这显然是有害的。当确定noreturn函数确实返回时,Gcc会发出警告。)其他gcc目标体系结构不会这样做;这也是错过的优化。
但gcc还远远不够:优化掉返回指令(或用非法指令替换它)可以节省代码大小并保证噪声失败而不是静默损坏。
如果您要优化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
被声明为substr
,那就是我们得到的。
任何以回复结束的loses backtrace info都可以假定永远不会到达。