包含未定义行为的源代码使编译器崩溃是否合法?
Is it legal for source code containing undefined behavior to crash the compiler?
假设我要编译一些编写不当的 C++ 源代码,这些代码会调用未定义的行为,因此(正如他们所说)"anything can happen"。
从 C++ 语言规范认为在 "conformant" 编译器中可以接受的角度来看,"anything" 在这种情况下是否包括编译器崩溃(或窃取我的密码,或其他行为不当或错误-在编译时输出),或者未定义行为的范围是否专门限于生成的可执行文件运行时可能发生的情况?
未定义行为的规范定义如下:
behavior for which this International Standard imposes no requirements
[ Note: Undefined behavior may be expected when this International
Standard omits any explicit definition of behavior or when a program
uses an erroneous construct or erroneous data. Permissible undefined
behavior ranges from ignoring the situation completely with
unpredictable results, to behaving during translation or program
execution in a documented manner characteristic of the environment
(with or without the issuance of a diagnostic message), to terminating
a translation or execution (with the issuance of a diagnostic
message). Many erroneous program constructs do not engender undefined
behavior; they are required to be diagnosed. Evaluation of a constant
expression never exhibits behavior explicitly specified as undefined.
— end note ]
虽然注释本身不是规范性的,但它确实描述了一系列已知的行为实现。因此,根据该注释,使编译器崩溃(翻译突然终止)是合法的。但实际上,正如规范文本所说,该标准对执行或翻译没有任何限制。如果实施窃取了您的密码,则不违反标准中规定的任何合同。
这里的"legal"是什么意思?根据这些标准,任何不与 C 标准或 C++ 标准相抵触的内容都是合法的。如果您执行语句 i = i++;
,结果恐龙统治了世界,这并不违反标准。然而,它确实与物理定律相矛盾,所以它不会发生:-)
如果未定义的行为使您的编译器崩溃,这并不违反 C 或 C++ 标准。然而,它确实意味着编译器的质量可以(并且可能应该)得到改进。
在以前版本的 C 标准中,存在错误或不依赖于未定义行为的语句:
char* p = 1 / 0;
允许将常量 0 分配给 char*。允许非零常量不是。由于 1 / 0 的值是未定义的行为,因此无论编译器是否应该接受此语句,都是未定义的行为。 (如今,1 / 0 不再符合 "integer constant expression" 的定义)。
我们通常担心的大多数类型的 UB,如 NULL-deref 或除以零,都是 运行time UB。编译一个会导致 运行time UB if executed 的函数一定不能导致编译器崩溃。 除非它可以证明函数(和那个路径)通过函数)肯定会被程序执行。
(第二个想法:也许我没有考虑过在编译时需要对模板/constexpr 进行评估。可能在此期间的 UB 允许在翻译过程中造成任意的怪异,即使从未调用结果函数。)
ISO C++ 引用 的 翻译期间的行为 部分类似于 ISO C 标准中使用的语言。 C 在编译时不包含模板或 constexpr
强制求值。
但是有趣的事实:ISO C 在注释中说如果翻译终止,它必须带有诊断消息。或 "behaving during translation ... in a documented manner"。我不认为 "ignoring the situation completely" 可以理解为包括停止翻译。
旧答案,在我了解翻译时间 UB 之前写的。 不过 运行time-UB 是正确的,因此可能仍然有用。
在编译时 发生 的 UB 是不存在的。它可以对编译器可见沿着特定的执行路径,但在 C++ 术语中它没有发生直到执行到达该执行路径通过函数。
程序中导致无法编译的缺陷不是 UB,它们是语法错误。这样的程序在 C++ 术语中是 "not well-formed"(如果我的标准语正确的话)。一个程序可以是良构的但包含 UB。 Difference between Undefined Behavior and Ill-formed, no diagnostic message required
除非我误解了什么,否则 ISO C++ 要求 这个程序才能正确编译和执行,因为执行永远不会达到零除。 (在实践中(Godbolt),好的编译器只会生成工作的可执行文件。gcc/clang 警告 x / 0
但不是这个,即使在优化时也是如此。但是无论如何,我们试图告诉 low 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()
的一侧导致 provable UB,则可以删除该分支。
但是编译器仍然必须以理智和正确的方式编译所有东西else。 不遇到(或无法证明遇到)UB的所有路径仍必须编译为执行的asm,就好像C++抽象机是运行ning一样它。
您可能会争辩说 main
中的无条件编译时可见 UB 是此规则的例外。 或者编译时可证明的执行开始于main
实际上确实达到了有保证的 UB。
我仍然认为合法的编译器行为包括产生一枚会爆炸的手榴弹 if 运行。或者更合理地说,main
的定义由一条非法指令组成。 我认为如果你从来没有运行这个程序,那么还没有任何 UB。编译器本身是'不允许爆炸,IMO。
在分支中包含可能或可证明的 UB 的函数
UB 沿着任何给定的执行路径及时向后到达 "contaminate" 所有以前的代码。但在实践中,编译器只有在实际 证明 执行路径导致编译时可见的 UB 时才能利用该规则。例如
int minefield(int x) {
if (x == 3) {
*(char*)nullptr = x/0;
}
return x * 5;
}
编译器必须使 asm 适用于除 3 以外的所有 x
,直至 x * 5
在 INT_MIN 和 [=119= 处导致有符号溢出 UB 的点].如果从未使用 x==3
调用此函数,则程序当然不包含 UB,并且必须按照编写的方式运行。
我们不妨在 GNU C 中编写 if(x == 3) __builtin_unreachable();
来告诉编译器 x
绝对不是 3。
实际上,在普通程序中到处都是 "minefield" 代码。例如任何除以整数都会向编译器保证它是非零的。任何指针 deref 都向编译器承诺它是非 NULL 的。
如果遇到 #include "'foo'"
,标准将不会对实现的行为强加任何要求。如果编译器编写者判断通过 运行 将指定程序的输出定向到一个临时文件然后表现为 #include
的文件,然后尝试处理包含上述行的程序可能 运行 程序 foo
,无论结果如何。
因此,对于尝试翻译 C 程序可能发生的结果通常没有任何限制,即使不努力 运行 它也是如此。
假设我要编译一些编写不当的 C++ 源代码,这些代码会调用未定义的行为,因此(正如他们所说)"anything can happen"。
从 C++ 语言规范认为在 "conformant" 编译器中可以接受的角度来看,"anything" 在这种情况下是否包括编译器崩溃(或窃取我的密码,或其他行为不当或错误-在编译时输出),或者未定义行为的范围是否专门限于生成的可执行文件运行时可能发生的情况?
未定义行为的规范定义如下:
behavior for which this International Standard imposes no requirements
[ Note: Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed. Evaluation of a constant expression never exhibits behavior explicitly specified as undefined. — end note ]
虽然注释本身不是规范性的,但它确实描述了一系列已知的行为实现。因此,根据该注释,使编译器崩溃(翻译突然终止)是合法的。但实际上,正如规范文本所说,该标准对执行或翻译没有任何限制。如果实施窃取了您的密码,则不违反标准中规定的任何合同。
这里的"legal"是什么意思?根据这些标准,任何不与 C 标准或 C++ 标准相抵触的内容都是合法的。如果您执行语句 i = i++;
,结果恐龙统治了世界,这并不违反标准。然而,它确实与物理定律相矛盾,所以它不会发生:-)
如果未定义的行为使您的编译器崩溃,这并不违反 C 或 C++ 标准。然而,它确实意味着编译器的质量可以(并且可能应该)得到改进。
在以前版本的 C 标准中,存在错误或不依赖于未定义行为的语句:
char* p = 1 / 0;
允许将常量 0 分配给 char*。允许非零常量不是。由于 1 / 0 的值是未定义的行为,因此无论编译器是否应该接受此语句,都是未定义的行为。 (如今,1 / 0 不再符合 "integer constant expression" 的定义)。
我们通常担心的大多数类型的 UB,如 NULL-deref 或除以零,都是 运行time UB。编译一个会导致 运行time UB if executed 的函数一定不能导致编译器崩溃。 除非它可以证明函数(和那个路径)通过函数)肯定会被程序执行。
(第二个想法:也许我没有考虑过在编译时需要对模板/constexpr 进行评估。可能在此期间的 UB 允许在翻译过程中造成任意的怪异,即使从未调用结果函数。)
ISO C++ 引用 constexpr
强制求值。
但是有趣的事实:ISO C 在注释中说如果翻译终止,它必须带有诊断消息。或 "behaving during translation ... in a documented manner"。我不认为 "ignoring the situation completely" 可以理解为包括停止翻译。
旧答案,在我了解翻译时间 UB 之前写的。 不过 运行time-UB 是正确的,因此可能仍然有用。
在编译时 发生 的 UB 是不存在的。它可以对编译器可见沿着特定的执行路径,但在 C++ 术语中它没有发生直到执行到达该执行路径通过函数。
程序中导致无法编译的缺陷不是 UB,它们是语法错误。这样的程序在 C++ 术语中是 "not well-formed"(如果我的标准语正确的话)。一个程序可以是良构的但包含 UB。 Difference between Undefined Behavior and Ill-formed, no diagnostic message required
除非我误解了什么,否则 ISO C++ 要求 这个程序才能正确编译和执行,因为执行永远不会达到零除。 (在实践中(Godbolt),好的编译器只会生成工作的可执行文件。gcc/clang 警告 x / 0
但不是这个,即使在优化时也是如此。但是无论如何,我们试图告诉 low 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()
的一侧导致 provable UB,则可以删除该分支。
但是编译器仍然必须以理智和正确的方式编译所有东西else。 不遇到(或无法证明遇到)UB的所有路径仍必须编译为执行的asm,就好像C++抽象机是运行ning一样它。
您可能会争辩说 main
中的无条件编译时可见 UB 是此规则的例外。 或者编译时可证明的执行开始于main
实际上确实达到了有保证的 UB。
我仍然认为合法的编译器行为包括产生一枚会爆炸的手榴弹 if 运行。或者更合理地说,main
的定义由一条非法指令组成。 我认为如果你从来没有运行这个程序,那么还没有任何 UB。编译器本身是'不允许爆炸,IMO。
在分支中包含可能或可证明的 UB 的函数
UB 沿着任何给定的执行路径及时向后到达 "contaminate" 所有以前的代码。但在实践中,编译器只有在实际 证明 执行路径导致编译时可见的 UB 时才能利用该规则。例如
int minefield(int x) {
if (x == 3) {
*(char*)nullptr = x/0;
}
return x * 5;
}
编译器必须使 asm 适用于除 3 以外的所有 x
,直至 x * 5
在 INT_MIN 和 [=119= 处导致有符号溢出 UB 的点].如果从未使用 x==3
调用此函数,则程序当然不包含 UB,并且必须按照编写的方式运行。
我们不妨在 GNU C 中编写 if(x == 3) __builtin_unreachable();
来告诉编译器 x
绝对不是 3。
实际上,在普通程序中到处都是 "minefield" 代码。例如任何除以整数都会向编译器保证它是非零的。任何指针 deref 都向编译器承诺它是非 NULL 的。
如果遇到 #include "'foo'"
,标准将不会对实现的行为强加任何要求。如果编译器编写者判断通过 运行 将指定程序的输出定向到一个临时文件然后表现为 #include
的文件,然后尝试处理包含上述行的程序可能 运行 程序 foo
,无论结果如何。
因此,对于尝试翻译 C 程序可能发生的结果通常没有任何限制,即使不努力 运行 它也是如此。