74

我读了这个关于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

4

9 回答 9

123

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

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

IIRC,如果用函数说明符指定的noreturn函数最终返回给它的调用者,要么

  • 通过使用和显式return声明
  • 通过到达函数体的末端

行为未定义。您不得从该函数返回。

为了清楚起见,使用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位置调用函数并最终返​​回,则行为未定义。fnoreturnf [注意:该函数可能会通过抛出异常终止。——尾注]

[[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";
}

—结束示例]

于 2017-08-31T12:35:17.993 回答
51

为什么函数 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对代码施加限制——它告诉编译器“我的代码永远不会返回”。如果你违反了这个限制,那就是你的全部了。

于 2017-08-31T12:35:35.007 回答
27

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

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

但是如果你向编译器保证你的函数不会返回,并且编译器没有安排函数正确返回,然后你去写一个返回的函数,编译器应该怎么做?做?它基本上有三种可能:

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

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

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

编程和现实生活中的底线是:不要做出无法兑现的承诺。其他人可能会根据您的承诺做出决定,如果您违反承诺,可能会发生坏事。

于 2017-08-31T13:36:39.813 回答
16

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

如果您确实从这样的函数返回,则行为是未定义的,但这并不意味着理智的编译器将允许您通过删除ret语句完全弄乱应用程序的状态,特别是因为编译器通常甚至能够推断出回归确实是可能的。

但是,如果你这样写:

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

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

那么编译器完全删除它是完全合理的some_other_func,如果感觉的话。

于 2017-08-31T12:48:56.133 回答
11

正如其他人所提到的,这是典型的未定义行为。你承诺func不会回来,但你还是让它回来了。当它破裂时,你可以捡起碎片。

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

您可以在汇编列表中看到这一点:编译器假定 inmain不会func返回。因此,它从字面上删除了之后的所有代码call func(请参阅https://godbolt.org/g/8hW6ZR)。汇编列表不会被截断,它实际上只是在之后结束,call func因为编译器假定之后的任何代码都将无法访问。因此,当func实际返回时,main将开始执行main函数后面的任何废话——无论是填充、立即常量还是00字节海。再次 - 非常未定义的行为。

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

于 2017-08-31T18:22:24.493 回答
8

据此_

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

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

于 2017-08-31T12:39:20.177 回答
6

ret只是意味着该函数将控制权返回给调用者。因此, CPU 执行main函数call func,然后,retCPU 继续执行main.

编辑

所以,事实证明noreturn并没有使函数根本不返回,它只是一个说明符,告诉编译器这个函数的代码是以函数不会返回的方式编写的。所以,你应该在这里做的是确保这个函数实际上不会将控制权返回给被调用者。例如,您可以exit在其中调用。

此外,鉴于我已经阅读了有关此说明符的内容,似乎为了确保该函数不会返回其调用点,应该在其中调用另一个 noreturn函数并确保后者始终运行(按顺序以避免未定义的行为)并且不会导致 UB 本身。

于 2017-08-31T12:35:46.000 回答
6

没有返回函数不会将寄存器保存在条目上,因为它不是必需的。它使优化更容易。例如,非常适合调度程序例程。

请参阅此处的示例: https ://godbolt.org/g/2N3THC并发现差异

于 2017-08-31T13:04:34.210 回答
2

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


noreturn是对编译器的一个承诺,即函数不会返回。这允许优化,并且在编译器难以证明循环永远不会退出或以其他方式证明没有通过返回函数的路径的情况下尤其有用。

如果返回, GCC 已经优化main到函数的末尾,即使使用看起来像您使用func()的默认(最低优化级别)。-O0

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

Godbolt 编译器资源管理器上的代码 + 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()会返回,否则它可能只是ext()jmp ext. 但是 gcc 不会尾noreturn调用函数,因为这会丢失诸如abort(). 不过,显然内联它们是可以的。

Gcc 也可以通过在之后省略movstore来优化call。如果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不保存/恢复它们。

ARM32 的 Gcc 实际上是这样做的,但仍会发出指令以干净地返回。因此noreturn,在 ARM32 上实际返回的函数将破坏 ABI 并在调用者或更高版本中导致难以调试的问题。(未定义的行为允许这样做,但这至少是一个实施质量问题:https ://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158 。)

在 gcc 无法证明函数是否返回的情况下,这是一个有用的优化。(不过,当函数确实返回时,这显然是有害的。当 Gcc 确定 noreturn 函数确实返回时会发出警告。)其他 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,这正是我们得到的。

任何以 return 结尾的基本块都可以被假定为永远不会到达。

于 2017-09-25T14:35:53.100 回答