为什么 "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 outnoreturn 根本 使 函数根本不是 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 的函数,编译器应该做什么?基本上有三种可能:

  1. 对你来说 "nice" 并想出一个方法来正确地使用 return 函数。
  2. 发出代码,当函数 returns 不正确时,它会崩溃或以任意不可预测的方式运行。
  3. 给你一个警告或错误信息,指出你违背了承诺。

编译器可能执行 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 已知(除非你 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);
}

(代码 + 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,并且使用 rbxrbp 而没有 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 都可以假定永远不会到达。