为什么 "noreturn" 起作用 return?
Why does "noreturn" function return?
我阅读了 this 关于 noreturn
属性的问题,该属性用于调用者不 return 的函数。
那我用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 [=13=], %eax
call func
为什么在提供 noreturn
属性后函数 func()
return?
C 中的函数说明符是对编译器的提示,接受程度由实现定义。
首先,_Noreturn
函数说明符(或者,noreturn
,使用 <stdnoreturn.h>
)是对编译器的提示,关于 理论上的承诺 程序员说这个函数永远不会 return。基于这个承诺,编译器可以做出某些决定,对代码生成进行一些优化。
IIRC,如果用 noreturn
函数说明符指定的函数最终 return 传给它的调用者,要么
- 通过使用显式
return
语句
- 到达函数体的末尾
behaviour is undefined。您 不得 return 来自函数。
为了清楚起见,使用 noreturn
函数说明符 不会阻止 函数形式 return 调用它的调用者。这是程序员对编译器的承诺,允许它有更多的自由度来生成优化的代码。
现在,万一你早晚答应了,选择违背,结果就是UB。鼓励但不要求编译器在 _Noreturn
函数似乎能够 return 调用其调用者时发出警告。
根据章节 §6.7.4,C11
,第 8 段
A function declared with a _Noreturn
function specifier shall not return to its caller.
以及第 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 段(强调我的)
If a function f
is called where f
was previously declared with the noreturn
attribute and f
eventually
returns, the behavior is undefined. [ Note: The function may terminate by throwing an exception. —end
note ]
[ Note: Implementations are encouraged to issue a warning if a function marked [[noreturn]]
might
return. —end note ]
3 [ Example:
[[ 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";
}
—end example ]
Why function func() return after providing noreturn attribute?
因为您编写了告诉它这样做的代码。
如果您不希望您的函数 return,请调用 exit()
或 abort()
或类似函数,这样它就不会 return。
在调用 printf()
之后,除了 return 之外,您的函数还会做什么 ?
6.7.4 Function specifiers中的C Standard,第12段特别包含了一个noreturn
函数的例子,它实际上可以return - 并将行为标记为 undefined:
示例 2
_Noreturn void f () {
abort(); // ok
}
_Noreturn void g (int i) { // causes undefined behavior if i<=0
if (i > 0) abort();
}
简而言之,noreturn
是 限制,你 对你的 代码 - 它告诉编译器 "MY code won't ever return"。如果您违反了该限制,那就全由您了。
ret
只是意味着函数 returns control 返回给调用者。因此,main
执行 call func
,CPU 执行函数,然后 ret
,CPU 继续执行 main
。
编辑
所以,它 turns out、noreturn
根本 使 函数根本不是 return,它只是一个说明符,告诉编译器认为此函数的代码是以函数不会return 的方式编写的。因此,您在这里应该做的是确保此函数实际上不会 return 将控制权返回给被调用者。例如,您可以在其中调用 exit
。
另外,鉴于我读到的有关此说明符的内容,似乎为了确保该函数不会 return 到其调用点,应该调用 另一个 noreturn
在其中运行并确保后者始终是 运行 (为了避免未定义的行为)并且不会导致 UB 本身。
根据this
If the function declared _Noreturn returns, the behavior is undefined. A compiler diagnostic is recommended if this can be detected.
程序员有责任确保此函数永远不会 returns,例如exit(1) 在函数结束时。
noreturn
属性是您 向编译器做出的关于您的函数的承诺。
如果你做 return这样的函数,行为是未定义的,但这并不意味着一个理智的编译器会允许你弄乱通过删除 ret
语句完全应用程序,特别是因为编译器通常甚至能够推断出 return 确实是可能的。
但是,如果你这样写:
noreturn void func(void)
{
printf("func\n");
}
int main(void)
{
func();
some_other_func();
}
那么编译器完全删除 some_other_func
是完全合理的,如果感觉是这样的话。
no return 函数不会在条目上保存寄存器,因为这不是必需的。它使优化更容易。例如,非常适合调度程序例程。
请参阅此处的示例:
https://godbolt.org/g/2N3THC 找出不同之处
noreturn
是一个承诺。您是在告诉编译器,"It may or may not be obvious, but I know, based on the way I wrote the code, that this function will never return." 这样,编译器就可以避免设置允许函数正确 return 的机制。省略这些机制可能会让编译器生成更高效的代码。
一个函数怎么可以不return?一个例子是,如果它改为调用 exit()
。
但是如果你向编译器保证你的函数不会return,而编译器没有安排它可能使函数正确地return,然后你去并编写一个 做 return 的函数,编译器应该做什么?基本上有三种可能:
- 对你来说 "nice" 并想出一个方法来正确地使用 return 函数。
- 发出代码,当函数 returns 不正确时,它会崩溃或以任意不可预测的方式运行。
- 给你一个警告或错误信息,指出你违背了承诺。
编译器可能执行 1、2、3 或一些组合。
如果这听起来像是未定义的行为,那是因为它确实是。
编程和现实生活中的底线是:不要做出你无法兑现的承诺。其他人可能会根据您的承诺做出决定,如果您随后违背承诺,就会发生不好的事情。
正如其他人所提到的,这是典型的未定义行为。你答应 func
不会 return,但你还是做到了 return。当它坏了时,你可以捡起碎片。
尽管编译器以通常的方式编译 func
(尽管你的 noreturn
),noreturn
会影响调用函数。
您可以在汇编列表中看到这一点:编译器在 main
中假定 func
不会 return。因此,它从字面上删除了 call func
之后的所有代码(请自行查看 https://godbolt.org/g/8hW6ZR)。程序集列表没有被截断,它实际上只是在 call func
之后结束,因为编译器假定之后的任何代码都是不可访问的。因此,当 func
实际上执行 return 时,main
将开始执行 main
函数之后的任何废话 - 无论是填充、直接常数还是大量 00
字节。再次 - 非常多未定义的行为。
这是传递性的 - 在所有可能的代码路径中调用 noreturn
函数的函数本身可以被假定为 noreturn
.
TL:DR: 这是 gcc 的优化失误。
noreturn
是对编译器的承诺,该函数不会 return。这允许优化,并且在编译器很难证明循环永远不会退出,或者证明没有路径通过 returns.
的函数的情况下尤其有用。
GCC 已经优化了 main
以在 func()
returns 的情况下脱离函数的结尾,即使使用默认的 -O0
(最低优化级别)也是如此看起来你用过。
func()
的输出本身可以被认为是错过了优化;它可以在函数调用之后省略所有内容(因为调用不是 return 是函数本身可以是 noreturn
的唯一方式)。这不是一个很好的例子,因为 printf
是一个标准的 C 函数,通常 return 已知(除非你 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);
}
(代码 + Godbolt compiler explorer. 上的 x86-64 asm)
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()
将变为 return,否则它可能只是尾调用 ext()
和 jmp ext
。但是 gcc 不会尾调用 noreturn
函数,因为 loses backtrace info 用于 abort()
之类的东西。不过,显然内联它们是可以的。
Gcc 也可以通过省略 call
之后的 mov
存储进行优化。如果 ext
returns,则程序已被清理,因此生成任何代码都没有意义。 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
这个函数可以假设它没有 return,并且使用 rbx
和 rbp
而没有 saving/restoring 它们。
ARM32 的 Gcc 实际上是这样做的,但仍然干净利落地向 return 发出指令。因此,在 ARM32 上实际执行 return 的 noreturn
函数将破坏 ABI,并在调用方或之后导致难以调试的问题。 (未定义的行为允许这样做,但这至少是一个实施质量问题:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158。)
在 gcc 无法证明函数是否存在的情况下,这是一个有用的优化 return。 (不过,当函数只执行 return 时,这显然是有害的。当确定 noreturn 函数执行 return 时,Gcc 会发出警告。)其他 gcc 目标体系结构不这样做;这也是一个错过的优化。
但 gcc 还不够:优化掉 return 指令(或用非法指令替换它)将节省代码大小并保证嘈杂的失败而不是无声的腐败。
如果您要优化掉 ret
,优化掉所有仅在函数 return 有意义时才需要的东西。
因此,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
,这正是我们得到的结果。
任何以 return 结尾的 basic block 都可以假定永远不会到达。
我阅读了 this 关于 noreturn
属性的问题,该属性用于调用者不 return 的函数。
那我用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 [=13=], %eax
call func
为什么在提供 noreturn
属性后函数 func()
return?
C 中的函数说明符是对编译器的提示,接受程度由实现定义。
首先,_Noreturn
函数说明符(或者,noreturn
,使用 <stdnoreturn.h>
)是对编译器的提示,关于 理论上的承诺 程序员说这个函数永远不会 return。基于这个承诺,编译器可以做出某些决定,对代码生成进行一些优化。
IIRC,如果用 noreturn
函数说明符指定的函数最终 return 传给它的调用者,要么
- 通过使用显式
return
语句 - 到达函数体的末尾
behaviour is undefined。您 不得 return 来自函数。
为了清楚起见,使用 noreturn
函数说明符 不会阻止 函数形式 return 调用它的调用者。这是程序员对编译器的承诺,允许它有更多的自由度来生成优化的代码。
现在,万一你早晚答应了,选择违背,结果就是UB。鼓励但不要求编译器在 _Noreturn
函数似乎能够 return 调用其调用者时发出警告。
根据章节 §6.7.4,C11
,第 8 段
A function declared with a
_Noreturn
function specifier shall not return to its caller.
以及第 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 段(强调我的)
If a function
f
is called wheref
was previously declared with thenoreturn
attribute andf
eventually returns, the behavior is undefined. [ Note: The function may terminate by throwing an exception. —end note ][ Note: Implementations are encouraged to issue a warning if a function marked
[[noreturn]]
might return. —end note ]3 [ Example:
[[ 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"; }
—end example ]
Why function func() return after providing noreturn attribute?
因为您编写了告诉它这样做的代码。
如果您不希望您的函数 return,请调用 exit()
或 abort()
或类似函数,这样它就不会 return。
在调用 printf()
之后,除了 return 之外,您的函数还会做什么 ?
6.7.4 Function specifiers中的C Standard,第12段特别包含了一个noreturn
函数的例子,它实际上可以return - 并将行为标记为 undefined:
示例 2
_Noreturn void f () {
abort(); // ok
}
_Noreturn void g (int i) { // causes undefined behavior if i<=0
if (i > 0) abort();
}
简而言之,noreturn
是 限制,你 对你的 代码 - 它告诉编译器 "MY code won't ever return"。如果您违反了该限制,那就全由您了。
ret
只是意味着函数 returns control 返回给调用者。因此,main
执行 call func
,CPU 执行函数,然后 ret
,CPU 继续执行 main
。
编辑
所以,它 turns out、noreturn
根本 使 函数根本不是 return,它只是一个说明符,告诉编译器认为此函数的代码是以函数不会return 的方式编写的。因此,您在这里应该做的是确保此函数实际上不会 return 将控制权返回给被调用者。例如,您可以在其中调用 exit
。
另外,鉴于我读到的有关此说明符的内容,似乎为了确保该函数不会 return 到其调用点,应该调用 另一个 noreturn
在其中运行并确保后者始终是 运行 (为了避免未定义的行为)并且不会导致 UB 本身。
根据this
If the function declared _Noreturn returns, the behavior is undefined. A compiler diagnostic is recommended if this can be detected.
程序员有责任确保此函数永远不会 returns,例如exit(1) 在函数结束时。
noreturn
属性是您 向编译器做出的关于您的函数的承诺。
如果你做 return这样的函数,行为是未定义的,但这并不意味着一个理智的编译器会允许你弄乱通过删除 ret
语句完全应用程序,特别是因为编译器通常甚至能够推断出 return 确实是可能的。
但是,如果你这样写:
noreturn void func(void)
{
printf("func\n");
}
int main(void)
{
func();
some_other_func();
}
那么编译器完全删除 some_other_func
是完全合理的,如果感觉是这样的话。
no return 函数不会在条目上保存寄存器,因为这不是必需的。它使优化更容易。例如,非常适合调度程序例程。
请参阅此处的示例: https://godbolt.org/g/2N3THC 找出不同之处
noreturn
是一个承诺。您是在告诉编译器,"It may or may not be obvious, but I know, based on the way I wrote the code, that this function will never return." 这样,编译器就可以避免设置允许函数正确 return 的机制。省略这些机制可能会让编译器生成更高效的代码。
一个函数怎么可以不return?一个例子是,如果它改为调用 exit()
。
但是如果你向编译器保证你的函数不会return,而编译器没有安排它可能使函数正确地return,然后你去并编写一个 做 return 的函数,编译器应该做什么?基本上有三种可能:
- 对你来说 "nice" 并想出一个方法来正确地使用 return 函数。
- 发出代码,当函数 returns 不正确时,它会崩溃或以任意不可预测的方式运行。
- 给你一个警告或错误信息,指出你违背了承诺。
编译器可能执行 1、2、3 或一些组合。
如果这听起来像是未定义的行为,那是因为它确实是。
编程和现实生活中的底线是:不要做出你无法兑现的承诺。其他人可能会根据您的承诺做出决定,如果您随后违背承诺,就会发生不好的事情。
正如其他人所提到的,这是典型的未定义行为。你答应 func
不会 return,但你还是做到了 return。当它坏了时,你可以捡起碎片。
尽管编译器以通常的方式编译 func
(尽管你的 noreturn
),noreturn
会影响调用函数。
您可以在汇编列表中看到这一点:编译器在 main
中假定 func
不会 return。因此,它从字面上删除了 call func
之后的所有代码(请自行查看 https://godbolt.org/g/8hW6ZR)。程序集列表没有被截断,它实际上只是在 call func
之后结束,因为编译器假定之后的任何代码都是不可访问的。因此,当 func
实际上执行 return 时,main
将开始执行 main
函数之后的任何废话 - 无论是填充、直接常数还是大量 00
字节。再次 - 非常多未定义的行为。
这是传递性的 - 在所有可能的代码路径中调用 noreturn
函数的函数本身可以被假定为 noreturn
.
TL:DR: 这是 gcc 的优化失误。
noreturn
是对编译器的承诺,该函数不会 return。这允许优化,并且在编译器很难证明循环永远不会退出,或者证明没有路径通过 returns.
GCC 已经优化了 main
以在 func()
returns 的情况下脱离函数的结尾,即使使用默认的 -O0
(最低优化级别)也是如此看起来你用过。
func()
的输出本身可以被认为是错过了优化;它可以在函数调用之后省略所有内容(因为调用不是 return 是函数本身可以是 noreturn
的唯一方式)。这不是一个很好的例子,因为 printf
是一个标准的 C 函数,通常 return 已知(除非你 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);
}
(代码 + Godbolt compiler explorer. 上的 x86-64 asm)
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()
将变为 return,否则它可能只是尾调用 ext()
和 jmp ext
。但是 gcc 不会尾调用 noreturn
函数,因为 loses backtrace info 用于 abort()
之类的东西。不过,显然内联它们是可以的。
Gcc 也可以通过省略 call
之后的 mov
存储进行优化。如果 ext
returns,则程序已被清理,因此生成任何代码都没有意义。 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
这个函数可以假设它没有 return,并且使用 rbx
和 rbp
而没有 saving/restoring 它们。
ARM32 的 Gcc 实际上是这样做的,但仍然干净利落地向 return 发出指令。因此,在 ARM32 上实际执行 return 的 noreturn
函数将破坏 ABI,并在调用方或之后导致难以调试的问题。 (未定义的行为允许这样做,但这至少是一个实施质量问题:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158。)
在 gcc 无法证明函数是否存在的情况下,这是一个有用的优化 return。 (不过,当函数只执行 return 时,这显然是有害的。当确定 noreturn 函数执行 return 时,Gcc 会发出警告。)其他 gcc 目标体系结构不这样做;这也是一个错过的优化。
但 gcc 还不够:优化掉 return 指令(或用非法指令替换它)将节省代码大小并保证嘈杂的失败而不是无声的腐败。
如果您要优化掉 ret
,优化掉所有仅在函数 return 有意义时才需要的东西。
因此,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
,这正是我们得到的结果。
任何以 return 结尾的 basic block 都可以假定永远不会到达。