为什么“ 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属性后返回?

17537 次浏览

C 语言中的函数说明符是编译器的 提示,接受程度是实现定义的。

首先,_Noreturn函数说明符(或者,使用 <stdnoreturn.h>noreturn)是对编译器的一个提示,说明程序员创建的 理论上的承诺函数将永远不会返回。基于这一承诺,编译器可以做出某些决策,执行一些代码生成优化。

如果用 noreturn函数说明符指定的函数最终返回到其调用者,则

  • 通过使用和显式的 return语句
  • 通过达到功能体的末端

你从函数返回 绝对不行

为了清楚起见,使用 noreturn函数说明符 不会停止函数表单返回给它的调用者。这是程序员对编译器做出的承诺,允许它在一定程度上自由地生成优化的代码。

现在,万一,你做了一个承诺前后,选择违反这一点,结果是 UB。当 _Noreturn函数似乎能够返回给其调用者时,鼓励(但不要求)编译器生成警告。

根据 C11第6.7.4章第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++来说,其行为非常相似。引用 C++14第7.6.4章第2段(强调我的)

如果在先前使用 noreturn属性声明 f的地方调用了函数 f,并且最终调用了 f 返回,则该行为未定义。 < em > [注意: 函数可能通过引发异常而终止。ー end 注]

[注意: 如果标记为 [[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()之后,除了返回函数之外,别的还能做什么?

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放在 你的代码上的 限制,它告诉编译器 “我的代码永远不会返回”。如果你违反了规定,那都是你的错。

ret意味着函数将 控制返回给调用者。因此,main执行 call func,CPU 执行函数,然后,对于 ret,CPU 继续执行 main

剪辑

因此,它 原来noreturn制造函数根本不返回,它只是一个说明符,告诉编译器 这个函数的代码是这样写的: 函数不会返回。因此,这里应该做的是确保这个函数实际上不会将控制返回给被调用方。例如,您可以在其中调用 exit

此外,根据我对这个说明符的了解,似乎为了确保函数不会返回到它的调用点,应该调用函数内部的 另一个 noreturn函数,并确保后者始终运行(以避免出现未定义行为) ,并且不会导致 UB 本身。

根据 这个

如果声明为 _ Norereturn 的函数返回,则此行为未定义。如果可以检测到此行为,建议使用编译器诊断。

程序员的责任是确保这个函数永远不会返回,例如在函数的末尾退出(1)。

noreturn属性是 对编译器做出的关于函数的承诺。

如果你从这样一个函数返回 ,行为是未定义的,但这并不意味着一个健全的编译器会允许你通过删除 ret语句来完全混乱应用程序的状态,特别是因为编译器经常能够推断返回确实是可能的。

然而,如果你这样写:

noreturn void func(void)
{
printf("func\n");
}


int main(void)
{
func();
some_other_func();
}

那么编译器完全可以删除 some_other_func,如果感觉是这样的话。

没有返回函数不会保存条目上的寄存器,因为它是不必要的。这使得优化更加容易。例如,对于调度程序例程非常有用。

看这里的例子:

noreturn是一个承诺。你告诉编译器,“这可能是显而易见的,也可能不是,但是根据我编写代码的方式,知道,这个函数永远不会返回。”这样,编译器就可以避免设置允许函数正确返回的机制。省略这些机制可能允许编译器生成更有效的代码。

一个函数怎么可能不返回呢? 一个例子就是如果它调用 exit()

但是如果你向编译器保证你的函数不会返回,而编译器又不能让函数正确返回,然后你写了一个 是的返回的函数,那么编译器应该怎么做呢?它基本上有三种可能性:

  1. 对你好一点,想办法让函数正确返回。
  2. 发出代码,当函数不正确地返回时,它会崩溃或以任意不可预测的方式运行。
  3. 给你一个警告或错误信息,指出你违背了你的承诺。

编译器可能执行1、2、3或某种组合。

如果这听起来像未定义行为,那是因为它就是。

无论是在编程中还是在现实生活中,底线都是: 不要做出你无法兑现的承诺。其他人可能会根据你的承诺做出决定,如果你违背了承诺,坏事就会发生。

正如其他人提到的,这是典型的未定义行为。你保证 func不会回来,但你还是让它回来了。破碎的时候你得收拾残局。

尽管编译器以通常的方式编译 func(尽管您使用的是 noreturn) ,但是 noreturn会影响调用函数。

您可以在程序集清单中看到这一点: 编译器在 main中假定 func不会返回。因此,它实际上删除了 call func之后的所有代码(参见 https://godbolt.org/g/8hW6ZR)。程序集清单没有被截断,它只是在 call func之后结束,因为编译器假定在 call func之后的任何代码都是不可到达的。因此,当 func实际返回时,main将开始执行 main函数后面的任何废话——可以是填充、直接常量或者大量的 00字节。再说一遍非常有未定义行为。

这是传递性的——在所有可能的代码路径中调用 noreturn函数的函数本身可以被假定为 noreturn

TL: DR: 这是 gcc 错过的优化。


noreturn向编译器保证函数不会返回。这允许进行优化,特别是在编译器很难证明循环永远不会退出,或者证明函数中没有返回的路径的情况下。

如果 func()返回,GCC 已经对 main进行了优化,使其脱离函数的末尾,即使使用看起来像您使用的默认 -O0(最小优化级别)也是如此。

func()本身的输出可能被认为是错过了优化; 它可能只是省略了函数调用之后的所有内容(因为不返回调用是函数本身可以是 noreturn的唯一方法)。这不是一个很好的例子,因为 printf是一个标准的 C 函数,我们知道它会正常返回(除非 setvbufstdout提供一个会分段错误的缓冲区?)

让我们使用编译器不知道的另一个函数。

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);
}

(39; 1 & # 39;) ,libs: ! () ,选项: (1) ,l: & # 39; 5 & # 39; ,n: & # 39; ,n: & # 39; ; # 39; ,n: & # 39; ,n: & # 39; ,n: & # 39; ,n: & # 39; ,源: 1) ,l: & # 39; ,n: & # 39; ,n: & # 39; ,x 86-64 + clang + 5.0.0 + (Editor +% 231,+ Compiler +% 232;) ,k: 33.33333333333333333,k: 33.39,l: & # 39; ,n: & # 39; ,n: & # 39; ,s: 0,t: & # 39; & # 39;)) ,l: & # 39; 2 & # 39; ,n: & # 39; ,# 39; ,0 & # 39;)) ,版本: 4“ rel = “ nofollow noReferrer”> Godbolt 浏览器 > 编译器。)

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()将返回,否则它可能只是在 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

这个函数可以假设它不返回,并使用 rbxrbp而不保存/恢复它们。

Gcc for ARM32实际上做到了这一点,但仍然发出指令,以便在其他情况下干净地返回。因此,一个 noreturn函数如果真的返回到 ARM32,就会破坏 ABI,并在调用者或以后引起难以调试的问题。(未定义的行为允许这样做,但它至少是一个实现质量问题: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158。)

在 gcc 无法证明函数是否返回的情况下,这是一个有用的优化。(不过,当函数返回时,这显然是有害的。当确定 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,那就是我们得到的结果。

任何以返回结尾的 基本障碍都可以假定永远不会到达。