C中未定义的行为实际上会发生啥[关闭]

Posted

技术标签:

【中文标题】C中未定义的行为实际上会发生啥[关闭]【英文标题】:What could happen practically on undefined behavior in C [closed]C中未定义的行为实际上会发生什么[关闭] 【发布时间】:2017-09-08 15:34:00 【问题描述】:

我读过很多关于未定义行为 (UB) 的文章,但都在谈论理论。我想知道在实践中会发生什么,因为包含 UB 的程序实际上可能会运行。

我的问题与类 unix 系统有关,而不是嵌入式系统。

我知道不应该编写依赖于未定义行为的代码。请不要发送这样的答案:

一切皆有可能发生 恶魔可以飞出你的鼻子 电脑可能会跳起来着火

尤其是第一个,这是不正确的。您显然无法通过执行有符号整数溢出来获得 root。我问这个只是为了教育目的。

问题 A)

Source

实现定义的行为:未指定的行为,每个实现都记录了如何做出选择

implementation 是编译器吗?

问题 B)

*"abc" = '\0';

如果发生段错误以外的其他事情,我是否需要破坏我的系统?即使无法预测,实际会发生什么?第一个字节可以设置为零吗?还有什么,以及如何?

问题 C)

int i = 0;
foo(i++, i++, i++);

这是 UB,因为评估参数的顺序是未定义的。对。但是,当程序运行时,谁来决定评估参数的顺序:是编译器、操作系统还是其他?

问题 D)

Source

$ cat test.c
int main (void)

    printf ("%d\n", (INT_MAX+1) < 0);
    return 0;

$ cc test.c -o test
$ ./test
Formatting root partition, chomp chomp

根据其他 SO 用户的说法,这是可能的。这怎么可能发生?我需要一个损坏的编译器吗?

问题 E)

使用与上面相同的代码。除了表达式(INT_MAX+1) 产生一个随机值之外,实际会发生什么?

问题 F)

GCC -fwrapv 选项是否定义了有符号整数溢出的行为,还是仅让 GCC 假设它会回绕,但实际上它可能不会在运行时回绕?

问题 G)

这一项涉及嵌入式系统。当然,如果 PC 跳到一个意想不到的地方,两个输出可能会连在一起并造成短路(例如)。

但是,当执行类似这样的代码时:

*"abc" = '\0';

PC 不会被引导到通用异常处理程序吗?或者我错过了什么?

【问题讨论】:

对于未定义行为的预期效果的良好工作模型,请使用您最喜欢的“鼻恶魔”搜索引擎。总之,没有逻辑,没有保证,如果硬件有自毁序列,就允许触发。如果是自动取款机,它可以给你钱。 来自您提供的链接:真正的编译器会发出代码来压缩您的磁盘吗?当然不是,... 更一般地说,编译器无法生成绕过操作系统安全策略的代码(如果可以,您可以编写一个汇编程序来做同样的事情)。 但是想象一下,如果您在内核本身中运行的代码中有未定义的行为。由于内核实现了安全策略,它不受它的约束,一个错误几乎可以导致任何事情。 你到底想问什么?是不是(a)“我不相信UB真的可以重新格式化我的办公桌或让恶魔从我的鼻子里飞出来。我希望那些说这话的人承认他们夸大了。”还是(b)“我不相信 UB 是 那么 坏的,所以如果我知道我在做什么,如果我有充分的理由,我可以用合理的方式编写未定义的代码相信不会发生任何糟糕的事情。”如果您要问 (b),让我告诉您,未定义的行为确实会导致任意糟糕的结果,您确实想学习如何避免它。请参阅我发布的答案中的链接。 您不能同时提出多个问题。一次只回答一个问题。 【参考方案1】:

在实践中,大多数编译器通过以下任一方式使用未定义行为:

在编译时打印警告,通知用户他可能犯了错误 推断变量值的属性并使用这些属性来简化代码 执行不安全的优化,只要它们只破坏未定义行为的预期语义

编译器通常不是为恶意而设计的。利用未定义行为的主要原因通常是从中获得一些性能优势。但有时这可能涉及彻底消除死代码。

A) 是的。编译器应该记录他选择的行为。但通常这很难预测或解释 UB 的后果。

B) 如果字符串实际在内存中实例化并且在可写页面中(默认情况下它将在只读页面中),那么它的第一个字符可能会变为空字符。最有可能的是,整个表达式将作为死代码被丢弃,因为它是一个从表达式中消失的临时值。

C) 通常,评估顺序由编译器决定。在这里,它可能决定将其转换为i += 3(或者如果它很傻,则为i = undef)。 CPU 可以在运行时重新排序指令,但如果它破坏了其指令集的语义,则保留编译器选择的顺序(编译器通常不能进一步向下转发 C 语义)。一个寄存器的增量不能与同一个寄存器的另一个增量交换或并行执行。

D) 你需要一个愚蠢的编译器,当它检测到未定义的行为时会打印“格式化根分区,chomp chomp”。最有可能的是,它会在编译时打印警告,用他选择的常量替换表达式并生成一个二进制文件,该二进制文件只需使用该常量执行打印。

E) 这是一个语法正确的程序,所以编译器肯定会产生一个“工作”的二进制文件。理论上,该二进制文件与您可以在互联网上下载并运行的任何二进制文件具有相同的行为。最有可能的是,您会得到一个立即退出的二进制文件,或者打印上述消息并立即退出。

F) 它告诉 GCC 假设有符号整数使用 2 的补码语义在 C 语义中环绕。因此,它必须生成一个在运行时环绕的二进制文件。这相当容易,因为大多数架构无论如何都具有这种语义。 C 拥有 UB 的原因是编译器可以假设 a + 1 &gt; a 这对于证明循环终止和/或预测分支至关重要。这就是为什么使用有符号整数作为循环归纳变量可以导致更快的代码,即使它被映射到硬件中完全相同的指令。

G) 未定义的行为是未定义的行为。生成的二进制文件确实可以运行任何指令,包括跳转到未指定的位置……或者完全触发中断。最有可能的是,您的编译器会摆脱这种不必要的操作。

【讨论】:

关于 (D),你不需要一个愚蠢的编译器,你所需要的只是一个指令集,其中溢出导致陷阱,以及一个未初始化的处理程序,它会跳转到无人区,它恰好包含此函数调用。可能还有其他方法可以实现它。【参考方案2】:

你显然不能通过有符号整数溢出来获得root。

为什么不呢?

如果您假设有符号整数溢出只能产生一些特定值,那么您不太可能以这种方式获得 root。但是关于未定义行为的问题是优化编译器可以假设它不会发生,并根据该假设生成代码。

操作系统存在错误。利用这些漏洞可以调用privilege escalation。

假设您使用有符号整数运算来计算数组的索引。如果计算溢出,您可能会意外破坏预期数组之外的一些任意内存块。这可能会导致您的程序做任意坏事。

如果一个漏洞可以被故意利用(并且恶意软件的存在清楚地表明这是可能的),那么它至少有可能被意外利用。

另外,考虑这个简单的人为程序:

#include <stdio.h>
#include <limits.h>
int main(void) 
    int x = INT_MAX;
    if (x < x + 1) 
        puts("Code that gets root");
    
    else 
        puts("Code that doesn't get root");
    

在我的系统上,它会打印

Code that doesn't get root

当使用gcc -O0gcc -O1 编译时,并且

Code that gets root

使用gcc -O2gcc -O3

我没有触发安全漏洞的有符号整数溢出的具体示例(如果我有的话,我不会发布这样的示例),但这显然是可能的。

原则上,未定义的行为可能会使您的程序意外地执行以相同权限启动的程序可能故意执行的任何操作。除非您使用的是没有错误的操作系统,否则可能包括权限提升、擦除硬盘驱动器或向您的老板发送令人讨厌的电子邮件。

【讨论】:

投反对票的人愿意发表评论吗?【参考方案3】:

在我看来,面对未定义的行为可能发生的最糟糕的事情是明天会有所不同

我喜欢编程,但我也喜欢完成一个程序,然后继续从事其他工作。我不喜欢不断修补我已经编写的程序,以使它们在面对硬件、编译器或其他环境不断变化时自发产生的错误时继续工作。

所以当我写一个程序时,它是不够的。它必须出于正确的原因而发挥作用。我必须知道它有效,并且它将在下周、下个月和明年继续有效。到目前为止,在我运行它的一组测试用例上给出明显正确的答案似乎是行不通的。

这就是为什么未定义的行为如此有害的原因:它今天可能会做一些非常好的事情,然后明天会做一些完全不同的事情,当我不在身边为它辩护时。行为可能会发生变化,因为有人在稍微不同的机器上运行它,或者内存或多或少,或者在一组非常不同的输入上,或者在使用不同的编译器重新编译它之后。

另见this other answer 的第三部分(以“现在,还有一件事,如果你还在我身边的话”开头的部分)。

【讨论】:

哦 - 比“明天有所不同”更糟糕。 “在测试、发布、交付和安装后,有 10,000 名客户现在要么想要退款,要么正在与他们的律师交谈”:(【参考方案4】:

过去,您可以指望编译器做一些“合理的”事情。然而,当你编写未定义的代码时,编译器越来越多地真正利用他们的许可来做一些奇怪的事情。以效率的名义,这些编译器引入了非常奇怪的优化,它们并没有做任何接近你可能想要的事情。

阅读这些帖子:

Linus Torvalds 描述了一个 kernel bug that was much worse than it could have been,因为 gcc 利用了未定义的行为 LLVM blog post 关于未定义的行为(三部分中的第一部分,还有 two、three) John Regehr 的另一个 great blog post(也是三部分中的第一部分:two、three)

【讨论】:

在发布之前,我已经阅读了您的大部分链接(第一部分)。它们对开发人员很有帮助,但我想从实际的角度知道会发生什么。重点是,我什么都不期待,只想知道 @Bilow 如果您只是从逻辑上思考编译器和操作系统的工作原理,那么大多数现实后果应该是非常明显的。 @Bilow 我理解你的好奇心。但正如黑衣人的名言所说,“准备好失望吧。”即使我们可以给你一个很好的清单,列出今天可能发生的所有奇怪的事情,但明天它就会过时,因为优化编译器开始做的事情变得更加离奇和难以理解。 @SteveSummit 我喜欢你的评论,它是脚踏实地的,我很乐意阅读这样的列表并理解它,即使它很快就会被弃用

以上是关于C中未定义的行为实际上会发生啥[关闭]的主要内容,如果未能解决你的问题,请参考以下文章

在多线程 C++11 程序中未处理异常时会发生啥?

C ++中未初始化的内存分配

UINavigationcontroller 中未使用的视图会发生啥?

关闭 C++ 控制台应用程序时会发生啥

当连接关闭时,未提交的事务会发生啥?

手机屏幕关闭时会发生啥?