包含未定义行为的源代码会使编译器崩溃合法吗?

Jer*_*ner 83 c++ undefined-behavior language-lawyer

假设我去编译一些编写不佳的C ++源代码,这些源代码会调用未定义的行为,因此(正如他们所说)“任何事情都可能发生”。

从C ++语言规范在“合格”编译器中认为可接受的角度来看,这种情况下的“任何情况”是否包括编译器崩溃(或窃取我的密码,或者在编译时出现异常或错误),或者未定义行为的范围专门限于生成的可执行文件运行时会发生什么?

Sto*_*ica 70

未定义行为的规范定义如下:

[defns.undefined]

本国际标准没有规定的行为

[注:当本国际标准省略行为的任何明确定义或程序使用错误的构造或错误的数据时,可能会出现未定义的行为。允许的不确定行为包括:完全忽略具有无法预测结果的情况,以环境特征的书面形式在翻译或程序执行期间的行为(带有或不带有诊断消息),终止翻译或执行(带有发行)诊断消息)。许多错误的程序构造不会引起未定义的行为。他们需要被诊断。常量表达式的求值永远不会表现出明确指定为undefined的行为。?—尾注?]

尽管该说明本身不是规范性的,但它确实描述了已知表现出的一系列行为。因此,根据该说明,使编译器崩溃(翻译突然终止)是合法的。但是实际上,正如规范性文本所言,该标准对执行或翻译没有任何限制。如果某个实现窃取了您的密码,则不违反该标准中规定的任何合同。

  • 凯文说的也一样。作为前一个职业的C / C ++ / etc编译器工程师,我们的立场是,未定义的行为可能会崩溃_您的程序_,破坏您的输出数据,让您的房子着火。但是,无论输入什么内容,编译器都绝不会崩溃。(它可能不会提供有用的错误消息,但是它应该产生某种诊断并退出,而不是仅仅尖叫着CTHULHU TAKE WHEEL和segfaulting。) (66认同)
  • 就是说,如果您可以*实际上*使编译器在编译时执行任意代码而无需任何沙箱操作,那么各种安全人员将*非常*会对此感兴趣。对编译器进行段错误也是如此。 (41认同)
  • “如果实施方案窃取了您的密码,则不违反该标准中规定的任何合同。” 不管代码是否具有UB,这都是事实,不是吗?该标准仅指示已编译程序应执行的操作-可以正确编译代码但在此过程中窃取密码的编译器不会违反该标准。 (35认同)
  • @TiStrga,我敢相信克苏鲁会成为一名了不起的F1赛车手。 (8认同)
  • @Carmeister,哦,这是一个好主意,我一定要提醒人们,每当出现“ UB授予编译器许可进行核战争的权限”的论点时。再次。 (8认同)

Pet*_*des 8

我们通常担心的大多数UB,例如NULL-deref或被零除,都是运行时 UB。编译一个函数,如果执行该函数将导致运行时UB 不能导致编译器崩溃。 除非可能证明该功能(以及该功能的路径)一定由程序执行。

(第二个想法:也许我没有考虑在编译时需要对template / constexpr进行评估。在此期间,UB可能被允许在翻译过程中引起任意怪异,即使从未调用生成的函数也是如此。)

@StoryTeller的答案中 ISO C ++报价的翻译过程中行为类似于ISO C标准中使用的语言。C 在编译时不包含模板或强制评估。constexpr

有趣的事实是:ISO C在注释中表示,如果翻译终止,则必须带有诊断消息。或“在翻译过程中……以书面形式表现”。我不认为“完全忽略情况”可以理解为包括停止翻译。


旧答案,写于我了解翻译时UB之前。 但是,对于runtime-UB确实如此,因此可能仍然有用。


UB 不会在编译时发生。沿着特定的执行路径,编译器可以看到它,但是用C ++术语来说,直到执行通过函数到达该执行路径时,它才发生

程序中甚至无法编译的缺陷不是UB,它们是语法错误。这样的程序在C ++术语中“格式不正确”(如果我的标准语言正确的话)。程序的格式可以正确,但包含UB。 未定义行为和格式错误的区别,无需诊断消息

除非我有误解,否则ISO C ++ 要求此程序正确编译和执行,因为执行永远不会达到零。(在实践中(Godbolt),好的编译器只能使可执行的可执行文件。gcc / clang会发出警告,x / 0但不会发出警告,即使在优化时也不会发出警告。但是无论如何,我们试图告诉我们ISO C ++的允许实现的质量如何。因此请检查gcc / clang除了确认我正确编写了程序外,几乎不是一个有用的测试。)

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}
Run Code Online (Sandbox Code Playgroud)

一个用例可能涉及C预处理器,或constexpr变量以及在这些变量上的分支,这会导致在某些路径中胡说八道,而对于那些常量选择,这些路径是永远不会达到的。

可以假定导致编译时可见的UB的执行路径是永不采用的,例如x86的编译器可能会发出ud2(引起非法指令异常)作为的定义cause_UB()。或在函数内,如果if()引线的一侧导致可证明的 UB,则可以删除分支。

但是编译器仍然必须以理智而正确的方式编译其他所有内容。如果C ++抽象机正在运行UB ,所有遇到(或无法证明遇到)UB的路径仍必须编译为asm才能执行。


您可能会争辩说,无条件的编译时可见的UB in main是该规则的例外。 否则,编译时可证明,从开始执行main实际上就可以达到保证的UB。

我仍然认为合法的编译器行为包括产生手榴弹,如果运行,它就会爆炸。或更合理的说,它的定义main由一条非法指令组成。 我认为如果您从不运行该程序,那么还没有UB。 IMO不允许编译器本身爆炸。


分支内部包含可能或可证明的UB的函数

沿任何给定执行路径的UB都会向后退,以“污染”所有先前的代码。但是实际上,编译器只有在可以真正证明执行路径导致编译时可见的UB 时,才能利用该规则。例如

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}
Run Code Online (Sandbox Code Playgroud)

编译器必须使asm适用于x除3之外的所有其他组件,直到x * 5导致有符号溢出的UB达到INT_MIN和INT_MAX的程度。如果从不使用调用此函数x==3,则该程序当然不包含UB,并且必须按编写的方式工作。

我们可能还用if(x == 3) __builtin_unreachable();GNU C 编写来告诉编译器x肯定不是3。

实际上,普通程序中到处都有“雷区”代码。例如,任何整数除法都会向编译器保证其非零。任何指针deref都会向编译器保证它不是NULL。