【问题标题】:Is it legal for source code containing undefined behavior to crash the compiler?包含未定义行为的源代码使编译器崩溃是否合法?
【发布时间】:2019-12-30 08:14:43
【问题描述】:

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

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

【问题讨论】:

  • "UB 就是 UB。忍受它"... 不用等待。 “请发布 MCVE。” ……不用等。我喜欢这个问题,因为它不恰当地触发了所有的反应。 :-)
  • 真的没有限制,所以说UB可以召唤nasal demons。
  • UB 可以让作者在 SO 上发布 question。 :P
  • 不管 C++ 标准怎么说,如果我是编译器编写者,我肯定会将其视为编译器中的错误。因此,如果您看到这种情况,请提交缺陷报告。
  • @LeifWillerts 这可以追溯到 80 年代。我不记得确切的构造,但认为它取决于使用卷积变量类型。在我换上一个替代品后,我有一个“我在想什么 - 事情不是那样工作”的时刻。我没有责怪编译器拒绝构造,只是为了重新启动机器。我怀疑今天有人会遇到那个编译器。它是面向 68000 微处理器的 HP 64000 的 HP C 交叉编译器。

标签: c++ language-lawyer undefined-behavior


【解决方案1】:

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

[defns.undefined]

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

[ 注意:当这个 International 标准省略了任何明确的行为定义或程序何时 使用错误的构造或错误的数据。允许的未定义 行为范围从完全无视情况 不可预知的结果,在翻译或程序中表现 以文件化方式执行环境特征 (无论是否发出诊断消息),终止 翻译或执行(与诊断 信息)。许多错误的程序结构不会产生未定义 行为;他们需要被诊断出来。常数的评估 表达式从不表现出明确指定为未定义的行为。 — 尾注 ]

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

【讨论】:

  • 也就是说,如果您实际上可以让编译器在编译时执行任意代码,而无需任何沙盒,那么各种安全人员会非常 i> 有兴趣了解它。编译器的段错误也是如此。
  • 凯文所说的同上。作为之前职业中的 C/C++/etc 编译器工程师,我们的立场是,未定义的行为可能会导致 您的程序 崩溃、破坏您的输出数据、让您的房子着火等等。但是无论输入什么,编译器都不应崩溃。 (它可能不会给出有用的错误消息,但它应该产生某种诊断和退出,而不是仅仅尖叫 CTHULHU TAKE THE WHEEL 和段错误。)
  • @TiStrga 我打赌克苏鲁会成为一名出色的 F1 车手。
  • “如果某个实施窃取了您的密码,这并不违反标准中规定的任何合同。”不管代码是否有UB,这都是真的,不是吗?标准只规定了编译后的程序应该做什么——一个正确编译代码但在过程中窃取你的密码的编译器不会违反标准。
  • @Carmeister,哦,这是一个很好的观点,每当出现“UB 允许编译器启动核战争”的论点时,我都会确保提醒人们这一点。再次。
【解决方案2】:

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

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

ISO C++ 引用in @StoryTeller's answer 的翻译过程中的行为部分类似于 ISO C 标准中使用的语言。 C 在编译时不包含模板或constexpr 强制评估。

但有趣的事实:ISO C 在注释中说,如果终止翻译,则必须带有诊断消息。或“在翻译过程中表现......以有记录的方式”。我不认为“完全无视情况”可以理解为包括停止翻译。


旧答案,在我了解翻译时 UB 之前写的。不过,对于运行时 UB 来说确实如此,因此可能仍然有用。


在编译时没有发生这样的事情。它可以沿着特定的执行路径对编译器可见,但在 C++ 术语中,直到执行通过函数到达该执行路径时它才发生。 p>

程序中甚至无法编译的缺陷不是 UB,而是语法错误。这样的程序在 C++ 术语中是“格式不正确的”(如果我的标准正确的话)。程序可以是格式良好但包含 UB。 Difference between Undefined Behavior and Ill-formed, no diagnostic message required

除非我有什么误解,否则 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,则可以删除分支。

但是编译器仍然必须以理智和正确的方式编译所有else。所有没有遇到(或无法证明遇到)UB 的路径仍必须编译为 asm,该 asm 就像 C++ 抽象机正在运行它一样执行。


您可以争辩说main 中的无条件编译时可见 UB 是此规则的一个例外。 或者编译时可证明从 main 开始的执行实际上确实达到了保证 UB。

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


在分支内包含可能或可证明的 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。

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

【讨论】:

    【解决方案3】:

    这里的“合法”是什么意思?根据这些标准,任何与 C 标准或 C++ 标准不矛盾的东西都是合法的。如果您执行声明i = i++;,结果恐龙接管了世界,这并不违反标准。然而它确实与物理定律相矛盾,所以它不会发生:-)

    如果未定义的行为使您的编译器崩溃,这并不违反 C 或 C++ 标准。然而,这确实意味着编译器的质量可以(并且可能应该)得到改进。

    在 C 标准的早期版本中,存在错误或不依赖于未定义行为的语句:

    char* p = 1 / 0;
    

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

    【讨论】:

    • 确切地说:恐龙占领世界并不违反任何物理定律(例如侏罗纪公园的变化)。这是极不可能的。 :)
    【解决方案4】:

    如果遇到#include "'foo'",该标准不会对实现的行为施加任何要求。如果编译器编写者判断通过运行指示的程序并将其输出定向到临时文件然后表现为该文件的#include 来处理该形式的包含指令(在文件名中包含撇号)是有用的,那么尝试处理包含上述行的程序可能会运行程序foo,无论结果如何。

    因此,对于尝试翻译 C 程序可能发生的后果,通常没有限制,即使不努力运行它也是如此。

    【讨论】:

    • 人们可以对任何编程语言的任何翻译器或编译器说同样的话。或者,就此而言,任何程序。
    • @RobertHarvey:许多编程语言规范对这些事情都更加具体。如果语言规范说某个指令将从指定的操作系统路径的流中读取输入,并且操作系统在读取某个路径时做了一些奇怪的事情,那将超出语言规范的控制范围,但我没有认为大多数语言规范都会让实现全权委托以任意方式处理此类指令,而无需记录它,即使在以其他方式定义行为的平台上也是如此。
    猜你喜欢
    • 2012-05-19
    • 2012-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-11
    相关资源
    最近更新 更多