为什么“不归还”功能归还?


73

我读 这个问题关于noreturn属性,它被用于那些不返回到调用函数。

然后我用C编写了一个程序。

并使用生成的代码汇编

为什么func()提供noreturn属性后函数会返回?


22
请原谅我问,但是,“ noreturn”功能对任何人远程有用吗?它用于/误用了什么?
马丁·詹姆斯

15
@MartinJames,您的问题看起来像是这样:P
ForceBru

17
“我阅读了有关noreturnC语言中属性的问题”。您链接的问题与“ noreturnC ++中的属性”有关,这是另一种语言。
Gerhardh

12
@MartinJames noreturn允许编译器进行一些优化。最重要的是,以结尾exit或等效的错误处理不会将叶函数转换为非叶函数。因此,您可以assert在性能至关重要的代码的最深层中拥有一个或类似的控件,并且其花费不超过正确预测的分支(而不是必须设置框架,保存寄存器,避免调用者保存的寄存器等功能)。 。
艺术

5
“这里的函数返回值。” 其实没有;没有。x86ret指令仅意味着将执行转移回调用方。对于返回值为的函数,您会看到同样的事情void。换句话说,它返回control;没有价值。现在,所有x86调用约定都在EAX寄存器中返回值,但是调用者和被调用者必须对此达成一致,因此,如果函数返回void或什么都不返回,则EAX仅包含垃圾内容。再次,正如答案所说,根据C语言标准,行为是不确定的,但这就是asm的含义。
科迪·格雷

Answers:


122

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

首先,_Noreturn函数说明符(或,noreturn使用<stdnoreturn.h>)向编译器提示程序员在理论上承诺该函数将永不返回。基于此承诺,编译器可以做出某些决定,并对代码生成执行一些优化。

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

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

行为是不明确的。你一定不要能从函数中返回。

为了清楚起见,使用noreturn函数说明符不会停止函数窗体返回其调用方。程序员对编译器的承诺是允许编译器具有更大的自由度来生成优化的代码。

现在,以防万一,您早晚做出承诺,选择违反此承诺,结果就是UB。鼓励但不要求编译器在发生以下情况时发出警告:_Noreturn函数似乎可以返回其调用者。

根据第6.7.4章C11第8段

_Noreturn功能说明符声明的功能不得返回其调用者。

并且,第12段,(请注意注释!


对于C++,其行为非常相似。引用第7.6.4章C++14第2段(重点是我的内容

如果ff先前使用noreturn属性声明过的位置调用了一个函数并f最终返回,则该行为是不确定的。 [注意:该函数可能会因引发异常而终止。—尾注]

[注意:如果标记的功能[[noreturn]]可能会返回,鼓励实现者发出警告。—尾注]

3 [示例:

—末例]


1
@MartinJames:我对gcc pure/ constattributes的感觉是一样的,它们基本上只是声明开发人员保证会创建一个不会产生任何副作用的函数,或者nonnull保证不会将null指针传递给该函数,因此编译器(允许它删除null您可能在函数内部执行的所有检查)。
Groo

20
@MartinJames-此功能对静态分析工具(特别是执行最坏情况的堆栈深度分析的工具)有极大的帮助。如果没有noreturn真正不能返回的函数的属性,则此类工具的结果将是错误的。它可以同时对优化有帮助(节省最多几个字节,每一个这样的函数被调用时),因为编译器不必须考虑函数返回; 调用函数中的控制流仅在该点停止。
phonetagger

36
tl; dr:noreturn并不意味着“此函数将不返回”;它的意思是“告诉编译器它可以假定此函数将不会返回”。它不是使您的工作更轻松,而是使编译器的工作更轻松。
Hong Ooi

15
@MartinJames:这样考虑:弄清楚一个函数是否会返回实际上是停顿的问题。但是,对于正确性和优化而言,它也是一个有趣的属性。那么,当我们拥有要确定的属性但实际上陈规定型的不确定属性时,我们怎么办?我们必须问程序员。请注意,具有比C语言更具表现力的类型系统的语言通常也具有这种类型。例如,Scala的类型Unit为不返回任何内容的函数,而类型Nothing为不返回的函数。
约尔格W¯¯米塔格

4
@MauganRa这就是我建议ud2IMO的原因(取消该程序,在某些其他情况下也可以通过静态证明为未定义行为的相同操作),比返回经过优化以假定它的调用方要“可笑”惯于。
Random832 '17

50

为什么在提供noreturn属性后函数func()返回?

因为您编写了告诉它的代码。

如果你不想让你的函数返回,呼叫exit()abort()或相似的,所以它不会返回。

什么别的将你的函数做的比其他的回报,它已被称为后printf()

C标准6.7.4功能说明符,段落12具体包括的一个例子noreturn,实际上可以返回功能-和标签的行为作为未定义

例子2

总之,noreturn是一个限制放置你的代码-它告诉编译器“我的代码永远不会返回”。如果您违反了该限制,那么一切就在您身上。


6
@phonetagger对于包含未定义行为的程序而言,及时发送回信号可以杀死编译器,然后再生成程序,这是完全合法的。(这可能是物理学不允许的,但是该标准对此没有问题。)
Ray

7
@phonetagger实际上,随着时间的流逝,您可以说“是的,严格来说,这是不确定的,但实际上不会发生任何不好的情况”的次数越来越少。编译器越聪明,如果您给他们任何借口,他们似乎就有更多的余地让他们自己做真正疯狂的事情。
史蒂夫·萨米特

3
@phonetagger:这是令人惊讶的UB的真实示例:godbolt.org/g/uZibdL最近的clang ++编译了非空函数,这些函数以return无穷循环结束,显然是有意帮助您在错过警告时捕获错误(或者也许是但由于不确定是否会采用此路径而未发出警告的情况)。它不会对C执行此操作,因为如果调用者使用result,则它仅是C99 / C11中的UB
彼得·科德斯

4
@phonetagger我们不使用幻想的(和令人难忘的)示例,因为我们实际上认为UB将导致时间流逝的紫色巨龙将鼻恶魔追赶到爆炸的冰箱中。我们这样做是为了防止人们认为UB必须以某种方式工作,因为所有其他选择都是荒谬的。 我们想要传达的想法是“甚至可笑的结果都是可能的,没有任何假设是安全的”。某些UB表现是如此的怪异,以至没有人期望它们。(我什至没有写这个评论;我只是运行了一个包含i = i++;它的程序,它出现了。)
Ray

2
相比之下,安德鲁在开场白中直接回答了OP的问题。答案不是“既然是UB,它就可以做任何事情;圣牛,它有随机的机会,它回来了!真幸运!” 答案是,它返回是因为OP的代码要求这样做。当然,任何答案都不会被提及,而无需提及UB,Andrew也这样做。但按目前的情况来看,Sourav的编辑答案现在是更好的答案。
phonetagger

27

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

函数如何不返回?一个例子是,如果调用它exit()

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

  1. 对您“好”并找出使函数无论如何都能正确返回的方法。
  2. 发出代码,当函数不正确返回时,它将崩溃或以任意不可预测的方式运行。
  3. 给您警告或错误消息,指出您违反了诺言。

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

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

与现实生活中的编程一样,最重要的是:不要做出无法兑现的承诺。可能有人根据您的诺言做出了决定,如果您违背诺言,可能会发生坏事。


1
换句话说,不要对编译器撒谎。
詹斯(Jens)

16

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

如果确实从此类函数返回,则行为是不确定的,但这并不意味着明智的编译器将允许您通过删除ret语句来完全弄乱应用程序的状态,尤其是因为编译器通常甚至可以推断出该状态确实有可能返回。

但是,如果您编写此代码:

那么对于编译器来说,some_other_func完全删除是完全合理的。


关于弄乱应用程序的状态,在某个时候,我编写了一个程序,其中主函数没有return语句,并且关闭它的唯一方法是注销(它无法自行关闭,我不能X它出来(由于某种原因有一个空白的图形窗口),我无法通过任务管理器或终端强制退出它)。顺便说一句,这是在Windows上,不确定是什么编译器。
所罗门·乌科

11

正如其他人提到的,这是经典的未定义行为。你承诺了func不会返回,但无论如何都让它返回。发生故障时,您可以拾起碎片。

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

您可以在装配清单中看到这一点:编译器承担,中main,那func将不会返回。因此,它从字面上删除了所有代码call func(在https://godbolt.org/g/8hW6ZR上查看)。程序集清单不会被截断,它实际上只是在结束之后,call func因为编译器会假定之后的任何代码都无法访问。因此,当func确实返回时,main将开始执行该main函数之后的任何废话-填充,立即常量或大量00字节。再次-非常不确定的行为。

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


嗯,这很奇怪。 noreturn似乎禁用了尾调用优化(以及在noreturn函数调用printf的情况下停止内联)。您甚至可以通过-O3以下代码看到此文件:godbolt.org/g/vFWp8E。Clang 4.0和gcc 7.2相同。
彼得·科德斯

嗯,这是因为gcc的noreturn处理专门禁用了tailcall优化以获得更好的abort()回溯:gcc.gnu.org/bugzilla/show_bug.cgi?id=55747#c4
彼得·科德斯

8

根据这个

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

确保此函数永不返回是程序员的责任,例如,函数末尾的exit(1)。


6

ret仅仅意味着该函数将控制权返回给调用者。因此,main确实call func,CPU执行的功能,然后用ret,CPU继续执行main

编辑

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

另外,考虑到我已经读过的关于该说明符的内容,为了确保该函数不会返回其调用点,应该在其中调用另一个 noreturn函数并确保后者始终处于运行状态(为了(以避免未定义的行为),并且不会导致UB本身。


1
没错,但是您应该读这篇
kocica '17

@FilipKočica,这正是您发布此评论时正在阅读的内容
ForceBru

3
@ user694733我相信问题的核心在于,提问者希望noreturn做一些阻止他的代码返回的事情,而不是noreturn实际上限制了他所编写的代码在标准下的允许行为
安德鲁·亨利

的链noreturn必须在某处结束,例如引发异常,调用longjmp,进行诸如之类的系统调用_exit(2)或使用嵌入式asm进行一些骇人听闻的操作。但是,是的,除非您执行了其他不返回的操作,否则您应该调用另一个noreturn函数(包括exit(3)or abort(3)。)
Peter Cordes

6

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

参见以下示例:https : //godbolt.org/g/2N3THC并找出差异


实际上,唯一可以优化的noreturn是可执行文件大小的几(!)个字节:noreturn在任何理智的代码中,函数绝不能成为性能关键路径的一部分。整个构造更多的是关于使不必要的警告静音而不是优化。
cmaster-恢复莫妮卡

@cmaster没有属性使代码理智,只有编码者的知识。
P__J为波兰的妇女提供支持

我不是那个意思 我的意思是:如果代码至少是一半理智,则意味着的性能影响为零noreturn。从您似乎理解的角度来看,这与推理相反。我永远不会说使用noreturnmake会使代码或多或少地理智。
cmaster-恢复莫妮卡

That's not what I said.你对我来说太复杂了。我没
听懂

我只是遇到一个问题:编写“noreturn函数永远不能成为性能关键路径的一部分”是错误的:完全有可能编写一个包含noreturn性能关键路径中的调用的程序(通过longjmp())。但是,这些程序将不可避免地陷入大脑。条件“至少是一半的理智的代码”简单地排除了那些。希望能澄清一些事情。(对不起,我的语言很混乱。)
cmaster-恢复莫妮卡的时间

2

TL:DR:gcc错过了优化


noreturn向编译器保证该函数不会返回。这可以进行优化,并且在编译器难以证明循环永远不会退出或难以证明没有返回函数路径的情况下特别有用。

main即使func()返回,GCC仍已进行优化以使函数结束(即使使用的默认值-O0(最低优化级别)也是如此)。

func()本身的输出可以认为是错过的优化;它可能会在函数调用之后忽略所有内容(因为不返回调用是函数本身唯一的方法noreturn)。这不是一个很好的例子,因为printf它是已知可以正常返回的标准C函数(除非您setvbuf给出stdout出现段错误的缓冲区?)

让我们使用编译器不知道的其他函数。

Godbolt编译器资源管理器上的代码+ x86-64 asm 。

gcc7.2的输出bar()很有趣。它内联func(),并消除了foo=3无效存储,只剩下:

Gcc仍然假定该ext()函数将返回,否则它可能只是ext()用with尾部调用jmp ext。但是gcc不会尾noreturn调用函数,因为那样会丢失回溯信息,例如abort()。显然,内联它们是可以的。

Gcc可以通过在此mov之后省略商店来进行优化call。如果ext返回,则说明该程序已完成,因此没有必要生成任何代码。Clang确实在bar()/中进行了优化main()


func本身更有趣,并且错过了更大的优化

gcc和clang都发出几乎相同的东西:

该函数可以假定它不返回,并且使用rbx并且rbp不保存/恢复它们。

Gcc for ARM32确实可以做到这一点,但是仍然发出指令以其他方式返回。因此noreturn,实际上在ARM32上返回的函数将破坏ABI,并在调用者或更高版本中引起难以调试的问题。(未定义的行为允许这样做,但这至少是实现质量问题:https : //gcc.gnu.org/bugzilla/show_bug.cgi?id=82158。)

这在gcc无法证明函数是否返回的情况下非常有用。(不过,当函数仅简单返回时,这显然是有害的。Gcc在确定noreturn函数确实会返回时发出警告。)其他gcc目标体系结构则不这样做。这也是错过的优化。

但是gcc还远远不够:优化返回指令(或用非法指令替换)将节省代码大小,并保证嘈杂的失败,而不是无声的破坏。

而且,如果您要优化,则ret仅在函数返回时才需要优化所有内容。

因此,func()可以编译为

出现的所有其他指令都是错过的优化。如果ext声明了noreturn,那正是我们得到的。

任何以返回值结尾的基本块都可以认为永远不会到达。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.