假定具有未定义行为的分支是否无法访问并优化为死代码?

usr*_*usr 88 c++ dead-code undefined-behavior language-lawyer unreachable-code

请考虑以下声明:

*((char*)NULL) = 0; //undefined behavior
Run Code Online (Sandbox Code Playgroud)

它明确地调用未定义的行为.在给定的程序中是否存在这样的语句意味着整个程序是未定义的,或者一旦控制流命中这个语句,该行为只会变得不确定?

如果用户从未输入数字,3是否可以明确定义以下程序?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}
Run Code Online (Sandbox Code Playgroud)

或者,无论用户输入什么,它都是完全未定义的行为?

此外,编译器是否可以假定在运行时永远不会执行未定义的行为?这样可以及时推理:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}
Run Code Online (Sandbox Code Playgroud)

在这里,编译器可以推断,以防num == 3我们总是调用未定义的行为.因此,这种情况必须是不可能的,并且不需要打印该号码.整个if声明可以优化.根据标准,是否允许这种向后推理?

Ste*_*sop 63

在给定的程序中是否存在这样的语句意味着整个程序是未定义的,或者一旦控制流命中这个语句,该行为只会变得不确定?

都不是.第一个条件太强,第二个条件太弱.

对象访问有时会被排序,但标准会在时间之外描述程序的行为.丹维尔已经引用:

如果任何此类执行包含未定义的操作,则此国际标准不要求使用该输入执行该程序的实现(甚至不考虑第一个未定义操作之前的操作)

这可以解释为:

如果程序的执行产生未定义的行为,则整个程序具有未定义的行为.

因此,UB无法访问的语句不会给程序UB.从未达到(由于输入值)的可达语句,不会给出程序UB.这就是为什么你的第一个条件太强了.

现在,编译器一般不能告诉UB有什么.因此,要允许优化器重新排序与潜在UB语句将被重新订购应他们的行为被定义,有必要允许UB为"达到时光倒流"和出问题之前,前面的序列点(或C中++ 11术语,用于UB影响在UB之前排序的事物).因此,你的第二个条件太弱了.

一个主要的例子是优化器依赖于严格的别名.严格别名规则的重点是允许编译器重新排序无法有效重新排序的操作,如果有问题的指针可能是同一个内存.因此,如果您使用非法别名指针,并且确实发生UB,那么它很容易影响UB语句"之前"的语句.就抽象机而言,UB语句尚未执行.就实际的目标代码而言,它已部分或完全执行.但是标准并没有试图详细说明优化器重新排序语句意味着什么,或者它对UB有什么影响.它只是让实施许可证尽快出错.

您可以将此视为"UB有时间机器".

专门回答你的例子:

  • 如果读取3,则行为仅为未定义.
  • 如果基本块包含某些未定义的操作,编译器可以并且确实消除了代码.在不是基本块但是所有分支都通向UB的情况下,它们被允许(我猜是这样).除非PrintToConsole(3)以某种方式确定返回,否则此示例不是候选者.它可能抛出异常或其他什么.

与你的第二个类似的例子是gcc选项-fdelete-null-pointer-checks,它可以采用这样的代码(我没有检查过这个具体的例子,认为它说明了一般的想法):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}
Run Code Online (Sandbox Code Playgroud)

并将其更改为:

*p = 3;
std::cout << "3\n";
Run Code Online (Sandbox Code Playgroud)

为什么?因为if p为null,所以代码仍然具有UB,因此编译器可能认为它不是null并且相应地进行优化.Linux内核绊倒这个(https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897),主要是因为它在取消对NULL指针的模式运行应该如果是UB,则预计会导致内核可以处理的已定义硬件异常.启用优化后,gcc需要使用-fno-delete-null-pointer-checks才能提供超标准保证.

PS对"未定义行为何时触发?"这个问题的实际答案?是"你计划离开这一天前10分钟".

  • 实际上,由于这个原因,过去存在很多安全问题.特别是,任何事后溢出检查都有可能因此而被优化掉.例如`void can_add(int x){if(x + 100 <x)complain(); 可以完全优化掉,因为如果`x + 100`*没有'*溢出没有任何反应,如果`x + 100`*确实*溢出,那就是根据标准的UB,所以没有*可能*发生. (3认同)
  • @fgp:对,这是一个优化,如果人们绊倒它,人们会抱怨它,因为它开始觉得编译器故意破坏你的代码来惩罚你."如果我想让你删除它,为什么我会这样写呢!" ;-)但我认为有时候,当操作更大的算术表达式时,优化器会有用,假设没有溢出并避免在这些情况下只需要的任何昂贵的东西. (3认同)
  • @usr:我相信这是正确的,是的.通过您的特定示例(以及对正在处理的数据的必然性做出一些假设),我认为实现原则上可以在缓冲的STDIN中查找"3",如果它想要的话,并且尽快打包回家当天它看到一个传入. (3认同)
  • PS的额外+1(如果可以的话) (3认同)
  • 如果用户从不输入3,程序没有未定义,但如果他在执行期间输入3,那么整个执行是否未定义是否正确?只要100%确定程序将调用未定义的行为(并且不早于此行为),行为就变成了任何东西.我的这些陈述是否100%正确? (2认同)

Dan*_*vil 10

标准规定为1.9/4

[注意:本国际标准对包含未定义行为的程序的行为没有要求. - 结束说明]

有趣的是,"包含"意味着什么.稍后在1.9/5它说:

但是,如果任何此类执行包含未定义的操作,则此国际标准不要求使用该输入执行该程序的实现(甚至不考虑第一个未定义操作之前的操作)

在这里,它特别提到"执行......用那个输入".我会解释为,在一个可能的分支中未定义的行为现在不执行不会影响当前的执行分支.

然而,另一个问题是基于代码生成期间的未定义行为的假设.有关详细信息,请参阅Steve Jessop的答案.

  • 我不认为问题是在实际到达代码之前UB是否可能出现*.正如我所理解的那样,问题是如果甚至不能达到代码,UB是否会出现.当然,答案是"不". (6认同)
  • 如果从字面上看,这就是对所有现存的真实程序的死刑宣判。 (2认同)

zwo*_*wol 5

一个有益的例子是

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

当前的GCC和当前的Clang都将优化(在x86上)

xorl %eax,%eax
ret
Run Code Online (Sandbox Code Playgroud)

因为他们推断出xif (x)控制路径中的UB 始终为零.海湾合作委员会甚至不会给你使用未初始化的价值警告!(因为应用上述逻辑的传递在生成未初始化值警告的传递之前运行)

  • 有趣的例子。启用优化会隐藏警告,这是相当令人讨厌的。这甚至没有记录 - GCC 文档只说启用优化会产生“更多”警告。 (2认同)
  • @supercat 2-5% 就这些而言是*巨大的*。我见过有人出汗率为 0.1%。 (2认同)