为什么“noreturn”函数会返回值?

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

为什么带有 noreturn 属性的函数 func() 会在执行后返回?


22
请原谅我的询问,但是这个“noreturn”特性对任何人有什么用处呢?它被用于/滥用于什么场景? - Martin James
16
@MartinJames,你的问题看起来像是这个的复制:这里 :P - ForceBru
17
我读到了关于C语言中noreturn属性的问题。不,你链接的问题是关于C++语言中的noreturn属性,这是一种不同的语言。 - Gerhardh
14
@MartinJames 的noreturn允许编译器进行一些优化。最重要的是,以 exit 或等效方式结束的错误处理不会将叶函数转变为非叶函数。因此,您可以在性能关键代码的最深处使用 assert 或类似语句,而这不会比正确预测的分支成本更高(而不是该函数必须设置框架、保存寄存器、避免调用者保存的寄存器等)。 - Art
5
“Here function return value.”实际上不是这样的。x86的ret指令只意味着执行将返回到调用者。对于返回值为void的函数,情况也是如此。换句话说,它返回控制权,而不是一个值。现在,所有的x86调用约定都会在EAX寄存器中返回值,但是调用方和被调用方必须就此达成协议,因此如果函数返回void或无返回值,则EAX将只包含垃圾值。与前面的答案一样,根据C语言标准,这种行为是未定义的,但这就是汇编的意思。 - Cody Gray
显示剩余13条评论
9个回答

123
在C语言中,函数限定符是对编译器的一个提示,其接受程度由实现定义。首先,_Noreturn函数限定符(或者使用<stdnoreturn.h>中的noreturn)是程序员对于该函数永远不会返回的一种理论承诺,基于这个承诺,编译器可以做出某些决策并执行一些优化以生成代码。如果使用了noreturn函数限定符指定的函数最终通过使用显式的return语句或到达函数体末尾返回给其调用者,则行为未定义,您不能从该函数返回。使用noreturn函数限定符并不会阻止函数返回给其调用者,而是程序员向编译器做出的一种承诺,允许编译器更自由地生成优化的代码。如果您之前做出了承诺,但后来选择违反承诺,则结果是未定义的。编译器鼓励但不强制产生警告,当_Noreturn函数似乎能够返回给其调用者时。根据第6.7.4节第8段的规定,《C11》。

使用_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段(我强调的是

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 ]


1
@MartinJames:我对gcc的pure/const属性有同感,这些属性基本上只是声明开发人员承诺编写一个不会产生任何副作用的函数,或者nonnull属性,这是一个承诺,你不会将空指针传递给函数,因此编译器可以(允许它删除函数内部可能进行的任何null检查)。 - vgru
21
@MartinJames - 这个特性对于静态分析工具来说非常有益,尤其是那些执行最坏情况堆栈深度分析的工具。如果一个确实不会返回的函数没有 noreturn 属性,这些工具的结果将是不正确的。这也可能有助于优化(每次调用此类函数时最多可以节省几个字节),因为编译器不必考虑函数的返回;在调用函数中的控制流只是在那一点停止。 - phonetagger
37
简短概括:noreturn 不意味着 "此函数不会返回",而是意味着 "告诉编译器它可以假设此函数不会返回"。 它存在的目的不是为了让你的工作更轻松,而是为了让编译器的工作更轻松。 - Hong Ooi
15
@MartinJames:以这种方式考虑:确定一个函数是否会返回结果,就是“停机问题”。然而,这也是一种有趣的性质,既与正确性有关,又与优化有关。那么,当我们要决定一个实际上是无法决定的属性时,该怎么办呢?我们必须向程序员咨询。请注意,比C语言具有更多表达能力的类型系统通常也具有此类类型。例如,Scala具有类型 Unit 表示不返回任何内容的函数和类型 Nothing 表示没有返回值的函数。 - Jörg W Mittag
4
这就是我建议使用 ud2 的原因,我认为这比返回到已经被优化并假定不会这样做的调用者更好(在某些其他可以静态证明为未定义行为的情况下也会执行终止程序的操作)。 - Random832
显示剩余9条评论

51

为什么带有noreturn属性的函数func()会返回?

因为您编写了告诉它要这样做的代码。

如果您不希望函数返回,请调用exit()abort()或类似方法,这样它就不会返回。

在调用printf()后,您的函数除了返回之外还能做什么呢?

C标准中的6.7.4函数说明符第12段特别包括一个示例,即一个可以实际返回的noreturn函数,并将其行为标记为未定义的

示例2

_Noreturn void f () {
    abort(); // ok
}
_Noreturn void g (int i) {  // causes undefined behavior if i<=0
    if (i > 0) abort();
}

简而言之,noreturn 是一种限制条件,由你的代码中设置 - 它告诉编译器“我的代码永远不会返回”。如果你违反了这个限制,那就全是你自己的责任。


7
即使程序中存在未定义行为,向后发送信号并在编译器能够生成程序之前将其终止是合法的。(这可能不符合物理学原理,但标准没有问题。) - Ray
7
实际上,随着时间的推移,你可以说“是的,我知道,严格来讲它是未定义的,但实际上不会发生什么太糟糕的事情”的次数越来越少了。编译器越聪明,如果你给它们任何借口,它们就似乎会给自己更多的自由去做一些非常疯狂的事情。 - Steve Summit
3
这是一个 UB(未定义行为)的真实例子:https://godbolt.org/g/uZibdL 。最近的 clang++ 编译器将没有 return 的非 void 函数编译成无限循环,显然是有意为之,以帮助您捕捉错误,如果您错过了警告的话(或者可能是因为它没有警告而导致的情况,因为它不确定该路径是否会被执行)。这在 C 语言中不会发生,因为只有在调用方使用返回值时才是 C99/C11 中的 UB - Peter Cordes
4
@phonetagger,我们不使用奇特(并且难以忘记的)例子是因为我们认为UB会导致时光旅行的紫色龙追逐鼻鬼进入爆炸的冰箱。我们这样做是为了防止人们假定UB必须以某种方式工作,因为所有其他选项都是荒谬的。 "即使是荒谬的结果也是可能的,没有任何假设是安全的" 是我们想传达的思想。一些UB的表现实在太奇怪了,以至于没有人会预料到。 (我甚至没写过这条评论;我只是运行了一个包含i = i ++;的程序,然后它就出现了。) - Ray
2
相比之下,安德鲁在他的开场句中直接回答了OP的问题。答案不是“因为它是UB,它可以做任何事情;天哪,就像随机事件一样,它回来了!多么幸运!”答案是,它返回是因为OP的代码要求如此。当然,没有提到UB的答案会有所遗漏,安德鲁也这样做了。但现在,Sourav编辑后的答案是更好的答案。 - phonetagger
显示剩余7条评论

28

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

一个函数如何才能不返回?其中一个例子是调用exit()

但是如果你向编译器承诺你的函数不会返回,而编译器没有为让函数正确返回做好准备,然后你写了一个确实返回的函数,编译器应该怎么办?它基本上有三种可能:

  1. 对你友好并找到一种方法使函数正确返回。
  2. 当函数不正确返回时,发出行为崩溃或行为任意的代码。
  3. 给你一个警告或错误消息,指出你违反了自己的承诺。

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

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

无论在编程还是现实生活中,最重要的是:不要做出你无法履行的承诺。别人可能已经根据你的承诺做出决定,如果你违反了你的承诺,不好的事情就会发生。


1
换句话说,不要欺骗编译器 - Jens

16

noreturn 属性是您向编译器承诺的关于函数的承诺。

如果从这样的函数中返回,行为将是未定义的,但这并不意味着一个健康的编译器会允许您通过删除 ret 语句来完全破坏应用程序的状态,尤其是编译器通常能够推断出确实可能存在返回。

但是,如果您写入以下内容:

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

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

如果编译器觉得合适,完全可以将some_other_func删除。


关于操纵应用程序状态,我曾经写过一个主函数没有返回语句的程序,唯一关闭它的方法是注销用户账户。无法通过关闭窗口(由于某种原因,有一个空白图形窗口),任务管理器或终端强制退出。顺便提一下,这是在 Windows 上运行的,不确定使用了哪个编译器。 - Solomon Ucko

11

正如其他人所提到的,这是经典的未定义行为。你承诺 func 不会返回,但你还是让它返回了。当这个承诺被打破时,你需要自己去处理结果。

虽然编译器按照通常的方式编译了 func(尽管你使用了 noreturn),但是 noreturn 影响调用函数。

您可以在汇编清单中看到这一点:编译器在 main 中假定 func 不会返回。因此,它实际上删除了所有在 call func 后面的代码(可在https://godbolt.org/g/8hW6ZR查看)。汇编清单并没有截断,它只是在 call func 后面直接结束,因为编译器假设该语句后面的任何代码都将无法到达。因此,当 func 实际上返回时,main 将开始执行紧随其后的任何垃圾代码 - 无论是填充、立即常数还是一堆 00 字节。再次强调 - 这是非常未定义的行为。

这是可传递的 - 如果一个函数在所有可能的代码路径中调用了一个 noreturn 函数,那么该函数本身也被认为是 noreturn


哎呀,这很奇怪。noreturn 似乎会禁用尾调用优化(如果 noreturn 函数调用 printf,则还会停止内联)。即使使用 -O3,也可以看到这一点:https://godbolt.org/g/vFWp8E。Clang 4.0 和 gcc 7.2 在这方面是相同的。 - Peter Cordes
啊,这是因为gcc的noreturn处理特别禁用了尾调用优化,以获得更好的abort()回溯:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55747#c4。 - Peter Cordes

8
根据这个,如果声明了_Noreturn的函数返回,则行为是未定义的。如果可以检测到此情况,建议使用编译器诊断。程序员有责任确保此函数永远不会返回,例如在函数末尾使用exit(1)。

6
ret的意思是函数将控制权返回给调用者。因此,main调用func,CPU执行该函数,然后使用ret,CPU继续执行main编辑 所以,结果表明,noreturn并不是让函数完全不返回,它只是一个说明符,告诉编译器此函数的代码被编写成这样的方式,即该函数不会返回。因此,在这里,您应确保该函数实际上不会将控制权返回给调用者。例如,您可以在其中调用exit
此外,根据我读到的关于此说明符的内容,为了确保函数不会返回到其调用点,应在其中调用另一个 noreturn函数,并确保后者始终运行(以避免未定义的行为),并且不会引起UB本身。

1
这是正确的,但你应该阅读这个链接:https://dev59.com/4Wkv5IYBdhLWcg3wghIi - kocica
@FilipKočica,当你发表这个评论时,我正在阅读的内容正是这个。 - ForceBru
3
我认为这个问题的核心是提问者期望 noreturn 执行某些操作,以防止他的代码返回,而实际上 noreturn 实际上将限制 根据标准,他编写的代码允许做什么 - Andrew Henle
noreturn 的链条必须在某个地方结束,例如抛出异常、调用 longjmp、进行像 _exit(2) 这样的系统调用,或者使用内联汇编进行一些 hacky 操作。但是,除非你做了其他不返回的操作,否则你应该调用另一个 noreturn 函数(这包括 exit(3)abort(3))。 - Peter Cordes

6

没有返回函数在进入时不保存寄存器,因为这不是必要的。这使得优化更容易。例如,对于调度程序很有用。

在此处查看示例:https://godbolt.org/g/2N3THC 并观察差异。


@cmaster,没有属性能让代码变得合理,只有程序员的知识才能做到。 - 0___________
我只是遇到了这个问题,“noreturn函数永远不能成为性能关键路径的一部分”这样的写法是错误的:完全可以编写包含在性能关键路径中的noreturn调用的程序(通过使用longjmp())。然而,那些程序将不可避免地是无脑的。条件“至少有一半理智的代码”就排除了那些情况。希望这样可以澄清事情。(对于我的混乱而精确的语言表示抱歉。) - cmaster - reinstate monica
糟糕,忘了加上-Wall。请参见https://godbolt.org/g/8QEPBT。我在想这个“特性”实际上是不是一个bug,因为我只在ARM上看到了它。我通过使用对外部函数的调用来大大简化了代码,以便在MIPS / PowerPC / ARM64 / x86上跨越该调用保存/恢复函数-arg寄存器。仅在ARM32上,它才会覆盖一个未先保存的调用保留寄存器。ARM32的push / pop指令是特殊的(多个寄存器在一个指令中),因此ARM是否不同,无论它是优化还是错误,都有可能。 - Peter Cordes
@cmaster:你说得对,noreturn只适用于一旦你决定需要进行上下文切换的情况,而这种情况每次调用只会发生一次。优化调度程序是一件事,但这是为了处理许多任务的可扩展性。我还考虑了自愿抢占的用例,其中内核函数可能在执行某些缓慢的操作时调用调度程序。但如果调度程序可能只是返回(或在上下文切换到其他内容然后返回后返回),那么该函数实际上并不是noreturn - Peter Cordes
所以我同意,很难想象在哪种情况下它会有很大的性能差异。如果您有抛出异常的帮助函数,则可以帮助减小代码大小。但是,如果在热循环中存在异常抛出/捕获,那么您正在做错误的事情。 - Peter Cordes
显示剩余6条评论

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编译器资源管理器上进行Code和x86-64汇编。
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()会返回,否则它可以使用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
ARM32的Gcc实际上确实这样做,但仍会发出指令以否则干净地返回。 因此,在ARM32上实际返回的noreturn函数将破坏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

每个其他存在的指令都是一个被错过的优化机会。如果声明了extnoreturn,那么我们就得到了确切的结果。
任何以返回结尾的基本块都可以假定永远不会被执行到。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接