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


85

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

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


22
“ UB是UB。与它同住”……不用等待。“请发布MCVE。” ...别等 我很喜欢这个问题,因为它不恰当地触发了所有反应。:-)
Yunnosch '19

14
确实没有限制,这就是据说UB可以召唤鼻恶魔的原因
一位程序员伙计,

15
UB可以使作者发布有关SO的问题。:P
Tanveer Badar

45
无论C ++标准怎么说,如果我是一名编译器作者,我肯定会将其视为编译器中的错误。因此,如果您看到此情况,请提交缺陷报告。
约翰,

9
@LeifWillerts这是上世纪80年代。我不记得确切的构造,但认为它取决于使用卷积变量类型。更换后,我有一个“我在想什么-事情不会那样做”的时刻。我并没有责怪编译器拒绝该构造,而只是重启机器。我怀疑今天有人会遇到该编译器。它是针对68000微处理器的HP 64000的HP C交叉编译器。
阿维·伯杰

Answers:


71

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

[defns.undefined]

本国际标准对此没有要求的行为

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

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


43
就是说,如果您实际上可以让编译器在编译时执行任意代码而没有任何沙箱操作,那么各种安全人员将非常有兴趣了解它。对编译器进行段错误也是如此。
凯文(Kevin)

67
凯文说的也一样。作为前一个职业的C / C ++ / etc编译器工程师,我们的立场是未定义的行为可能会使您的程序崩溃,破坏您的输出数据,让您大火。但是,无论输入什么内容,编译器都绝不会崩溃。(它可能不会给出有用的错误消息,但它应该产生某种诊断并退出,而不仅仅是尖叫CTHULHU TAKE WHEEL和segfaulting。)
Ti Strga,

8
@TiStrga,我敢打赌克苏鲁会成为一名出色的F1赛车手。
zeta乐队

35
“如果实施方案窃取了您的密码,则不违反该标准中规定的任何合同。” 不管代码是否具有UB,这都是事实,不是吗?该标准仅指示已编译程序应执行的操作-可以正确编译代码但在此过程中窃取密码的编译器不会违反该标准。
卡梅斯特,

8
@Carmeister,哦,这是一个好主意,我一定要提醒人们,每当出现“ UB授予编译器许可进行核战争的权限”的论点时。再次。
ilkkachu

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 ++的允许实现质量如何。 / 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,则可以删除分支。

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


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

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


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

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

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

    return x * 5;
}

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

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

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


3

“合法”在这里是什么意思?根据这些标准,任何与C标准或C ++标准不矛盾的东西都是合法的。如果您执行一条语句i = i++;,结果恐龙占领了世界,那么这与标准并没有矛盾。但是它确实违背了物理定律,所以它不会发生:-)

如果未定义的行为使您的编译器崩溃,则不会违反C或C ++标准。但是,这确实意味着可以(可能应该)提高编译器的质量。

在以前的C标准版本中,有些语句是错误的或不依赖于未定义的行为:

char* p = 1 / 0;

允许将常数0分配给char *。不允许非零常量。由于1/0的值是未定义的行为,因此编译器是否应接受此语句是未定义的行为。(现在,1/0不再满足“整数常量表达式”的定义)。


3
确切地说:霸占世界的恐龙与任何物理定律(例如侏罗纪公园的变异)都不矛盾。这是极不可能的。:)
怪异的
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.