包含未定义行为的源代码是否可以使编译器崩溃?这是否合法?

85

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

从C++语言规范认为符合标准的编译器的角度来看,在这种情况下,“任何事情”是否包括编译器崩溃(或窃取我的密码,或者以其他方式在编译时出现错误或异常行为),或者未定义行为的范围仅限于生成的可执行文件运行时可能发生的事情?


22
“UB就是UB,接受它吧”……等等,“请发布一个MCVE。”……等等。我喜欢这个问题,它不合适地触发了我的许多反射动作。 :-) - Yunnosch
14
没有真正的限制,这也是为什么人们说UB可以召唤鼻妖的原因。 - Some programmer dude
15
UB可以促使作者在 Stack Overflow 上发布一个问题。 :P - Tanveer Badar
46
不论C++标准怎么说,如果我是编译器的开发者,我肯定会认为这是我的编译器中的一个bug。所以,如果你看到了这个问题,请提交一个缺陷报告。 - john
9
这是在80年代发生的事情。我记不得具体的构造方式,但认为它依赖于使用一个复杂的变量类型。在我放置了替代品之后,我有了一个“我当时在想什么——事情不会那样工作”的瞬间。我没有责怪编译器拒绝这种结构,只是因为重新启动机器而感到沮丧。我怀疑今天是否还有人会遇到那个编译器。这是针对68000微处理器的HP 64000的HP C交叉编译器。 - Avi Berger
显示剩余7条评论
4个回答

75
“未定义行为”的规范定义如下: [defns.undefined] 本国际标准对其不强制执行任何要求的行为
【注:“未定义行为”可能在本国际标准省略任何明确的行为定义或程序使用错误结构或错误数据时出现。允许的未定义行为范围从完全忽略情况并产生不可预测的结果,到以文件环境特征的记录方式在翻译或程序执行期间表现出来(无论是否发出诊断消息),再到终止翻译或执行(带有诊断消息的发布)。许多错误的程序结构不会引起未定义的行为;它们需要被诊断。常量表达式的评估从未表现出明确定义的行为。——注释结束】
虽然注释本身不是规范性的,但它确实描述了实现已知会表现出的一系列行为。因此,按照该注释,崩溃编译器(即翻译突然终止)是合法的。但事实上,正如规范文本所说,标准不对执行或翻译设置任何限制。如果一个实现窃取了您的密码,那么这并不违反标准所规定的任何契约。

44
如果你能实际上让编译器在编译时执行任意代码而没有任何沙盒保护,那么各种安全人员将非常有兴趣了解它。同样的情况也适用于导致编译器出现段错误。 - Kevin
67
对于Kevin所说的内容也是同样的观点。在我之前从事C/C++/等编译器工程师的职业中,我们的立场是未定义行为可能会导致程序崩溃,破坏输出数据,引起火灾等等问题。但无论输入是什么,编译器都不应该崩溃。(它可能无法给出有用的错误消息,但至少应该生成某种诊断信息并退出,而不是尖叫着让CTHULHU 掌握控制权然后段错误。) - Ti Strga
8
我猜克苏鲁会成为一个很棒的F1车手。 - zeta-band
35
即使代码存在未定义行为,如果实现程序窃取了您的密码,这并不违反标准中规定的任何合同。标准只规定编译后的程序应该做什么 - 即使编译器在正确编译代码的同时窃取了您的密码,也不违反标准。 - Carmeister
8
@Carmeister,哦,这是一个好观点,每当那些“UB让编译器有权发动核战争”的争论再次出现时,我会确保提醒人们注意这一点。 - ilkkachu
显示剩余13条评论

7

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

(第二个想法:也许我没有考虑到模板/constexpr在编译时需要评估。即使从未调用结果函数,可能允许在翻译期间引起任意怪异行为。)

ISO C++引用中“翻译期间的行为”部分在@StoryTeller的答案中类似于ISO C标准中使用的语言。C不包括模板或constexpr强制在编译时进行评估。

但是有趣的是:ISO C在注释中说,如果终止翻译,必须附带诊断消息。或者“在文档化的方式下翻译时的行为”。我认为,“完全忽略这种情况”不能被解读为包括停止翻译。

旧答案,是在我了解翻译时UB之前写的。虽然对于运行时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();
}

这可能涉及到C预处理器或constexpr变量以及在这些变量上进行分支,这会导致某些路径上的无意义,这些路径对于这些常量的选择永远不会被触发。

可以假定导致编译时可见UB的执行路径永远不会被采取,例如,x86编译器可以将ud2(导致非法指令异常)作为cause_UB()的定义。或者在函数内部,如果if()的一侧导致了可证明的UB,则可以删除该分支。

但编译器仍然必须以明智和正确的方式编译所有其他内容。所有没有遇到(或不能证明遇到)UB的路径仍必须编译为asm,就好像C++抽象机正在运行它一样。


你可以认为在main中无条件的编译时可见UB是这个规则的一个例外。或者编译时可以证明从main开始执行确实会达到保证的UB。

我仍然认为合法的编译器行为包括生成一个只有在运行时才会爆炸的手榴弹。或者更有可能的是,一个由单个非法指令组成的main定义。我认为如果你从来没有运行过程序,那么就还没有发生任何UB。在我看来,编译器本身不允许爆炸。


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

在执行路径上的任何给定位置,UB 都会向后“污染”所有先前的代码。但在实践中,编译器只能在它们可以实际证明执行路径导致编译时可见的 UB 时利用该规则。例如:

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

    return x * 5;
}

编译器必须生成适用于除3以外的所有x的asm,直到x * 5在INT_MIN和INT_MAX处导致有符号溢出UB。如果从未使用x == 3调用此函数,则程序当然不包含UB并且必须按原样工作。
我们可以在GNU C中写入if(x == 3) __builtin_unreachable();告诉编译器x绝对不是3。
实际上,在正常程序中到处都是“雷区”代码。例如,任何整数除法都承诺编译器它是非零的。任何指针解引用都承诺编译器它是非NULL的。

4
在这里,“legal”是什么意思?根据C标准或C++标准,任何不与之矛盾的内容都是合法的。如果您执行语句i = i ++; 并导致恐龙占领了世界,这不违反标准。但这确实违反了物理定律,因此不会发生 :-)
如果未定义行为导致编译器崩溃,那也不违反C或C++标准。但这意味着编译器的质量可能需要改进。
在早期版本的C标准中,存在错误语句或不依赖于未定义行为的语句:
char* p = 1 / 0;

将常数0赋值给char*是被允许的。但不允许使用非零常数。因为1/0的值是未定义的行为,所以编译器是否应该接受这个语句也是未定义的行为。(现在,1/0不再满足“整数常量表达式”的定义)。

4
准确地说:恐龙占领世界并不违反任何物理定律(例如《侏罗纪公园》那样)。只是这种情况高度不太可能发生。 :) - freakish

-1

如果编译器遇到 #include "'foo'",标准不会对实现的行为施加任何要求。 如果编译器作者判断以运行指定程序并将其输出重定向到临时文件,然后像处理一个#include那样处理包含文件名中带有单引号的这种形式的include指令将是有用的,那么尝试处理包含上述行的程序可能会运行程序foo,并导致任何结果。

因此,即使没有努力运行它,试图翻译C程序可能会导致任何可能的后果。


任何编程语言中的任何翻译器或编译器都可以说同样的话。 或者,换句话说,任何程序都是如此。 - Robert Harvey
@RobertHarvey:许多编程语言规范对这些事情更加具体。如果语言规范指定某个指令将从指定的 OS 路径读取输入流,并且当读取某个路径时,操作系统会出现一些奇怪的问题,那么这将超出语言规范的控制范围,但我认为大多数语言规范不会给予实现在平台上任意处理这些指令的自由,而无需记录它,即使在其他情况下定义了行为。 - supercat

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