C中未定义的行为实际上会发生什么

C中未定义的行为实际上会发生什么,c,gcc,undefined-behavior,C,Gcc,Undefined Behavior,我读过很多关于未定义行为的文章,但都是关于理论的。我想知道在实践中会发生什么,因为包含UB的程序实际上可能会运行 我的问题涉及类unix系统,而不是嵌入式系统 我知道人们不应该编写依赖于未定义行为的代码。请不要发送这样的答案: 一切都有可能发生 守护进程可以从你的鼻子里飞出来 电脑可能会跳起来着火 特别是对于第一个,这不是真的。显然,通过执行有符号整数溢出无法获得根。我问这个只是为了教育目的 问题A 实现定义的行为:未指定的行为,其中每个实现记录了如何做出选择 实现是编译器吗 问题B 要发生SE

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

我的问题涉及类unix系统,而不是嵌入式系统

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

一切都有可能发生 守护进程可以从你的鼻子里飞出来 电脑可能会跳起来着火 特别是对于第一个,这不是真的。显然,通过执行有符号整数溢出无法获得根。我问这个只是为了教育目的

问题A 实现定义的行为:未指定的行为,其中每个实现记录了如何做出选择

实现是编译器吗

问题B 要发生SEGFULT以外的事情,我需要我的系统被破坏吗?即使不可预测,实际会发生什么?第一个字节可以设置为零吗?还有什么,怎么做

问题C 这是因为参数的求值顺序未定义。正当但是,当程序运行时,谁来决定参数的求值顺序:是编译器、操作系统还是其他什么

问题D
PC不会被引导到通用异常处理程序吗?或者我遗漏了什么?

过去,您可以指望编译器做一些合理的事情。然而,越来越多的情况下,当您编写未定义的代码时,编译器确实在利用他们的许可证做一些奇怪的事情。在效率的名义下,这些编译器引入了非常奇怪的优化,这些优化并没有达到您可能想要的效果

阅读以下帖子:

Linus Torvalds描述了gcc利用未定义的行为的一个给定示例 关于未定义的行为三部分中的第一部分, John Regehr的另一篇文章也是三部分的第一部分:,
实际上,大多数编译器使用未定义行为的方式有以下两种:

在编译时打印警告,通知用户他可能犯了错误 推断变量值的属性,并使用这些属性简化代码 执行不安全的优化,只要它们只破坏未定义行为的预期语义 编译器通常不会被设计成恶意的。利用未定义行为的主要原因通常是为了从中获得一些性能优势。但有时,这可能涉及到完全消除死代码

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

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

通常,计算顺序由编译器决定。在这里,它可能会决定将其转换为i+=3或i=undef,如果这是愚蠢的。CPU可以在运行时对指令进行重新排序,但如果它破坏了指令集的语义,则会保留编译器选择的顺序。编译器通常无法进一步向下转发C语义。寄存器的增量不能与同一寄存器的其他增量进行转换或并行执行

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

它是一个语法正确的程序,因此编译器肯定会产生一个有效的二进制文件。从理论上讲,该二进制文件可能与您可以在internet上下载并运行的任何二进制文件具有相同的行为。最有可能的情况是,您得到一个直接退出的二进制文件,或者打印前面提到的消息并直接退出

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

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

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

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

因此,当我编写一个程序时,它还不足以工作。它必须为正确的理由而工作。我必须知道它是有效的,而且它将在下周、下个月和明年继续有效。它似乎不可能仅仅起作用,在我迄今为止运行它的测试用例集(必然是有限的)上给出了明显正确的答案

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

另请看第三部分,从开始到现在,如果你还和我在一起,还有一件事

显然,通过执行有符号整数溢出无法获得根

为什么不呢

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

操作系统有缺陷。除其他外,利用这些bug可以调用

假设您使用有符号整数算法将索引计算到数组中。如果计算溢出,您可能会意外地将某些任意内存块击到预期数组之外。这可能会导致您的程序执行任意的错误操作

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

也可以考虑这个简单的拟定程序:

#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");
    }
}
当使用gcc-O0或gcc-O1编译时,以及

获取根的代码 使用gcc-O2或gcc-O3

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


未定义的行为原则上可以使程序意外地执行以相同权限启动的程序可能有意执行的任何操作。除非您使用的是无缺陷的操作系统,否则可能包括权限升级、擦除硬盘驱动器或向上司发送恶意电子邮件。

要了解未定义行为的良好工作模式,请使用您最喜欢的鼻魔搜索引擎。简而言之,没有逻辑,没有保证,如果硬件具有自毁序列,则允许触发。如果是自动取款机,它可以让你大把花钱。从你提供的链接来看:真正的编译器会发出代码来咬你的磁盘吗?当然不是。。。更一般地说,编译器无法生成绕过操作系统安全策略的代码。如果可以,您可以编写一个执行相同操作的汇编程序。但是想象一下,如果在内核本身运行的代码中存在未定义的行为。因为内核实现了安全策略,所以它不受安全策略的约束,一个bug几乎可以导致任何事情?这是一个我不相信UB真的能改造我的桌子或者让恶魔从我鼻子里飞出来的地方。我希望说这些话的人承认他们是在夸大其词。或者是b我不相信UB有那么糟糕,所以如果我知道我在做什么,如果我有很好的理由这样做,我可以满怀信心地编写未定义的代码,相信不会发生太糟糕的事情。如果你问b,让我告诉你,未定义的行为确实会导致任意的坏结果,你真的想学习如何避免它。查看我发布的答案中的链接。你不能一次问多个问题。一次只回答一个问题。在发帖之前,我已经阅读了你的大部分链接的第一部分。它们对开发人员很有帮助,但我想知道从实际的角度来看会发生什么。关键是,我不期待任何事情,只想know@Bilow如果你只是从逻辑上思考编译器和操作系统是如何工作的,那么大多数现实的结果应该是非常明显的。@Bilow我理解你的好奇心。但正如这位黑衣人所说的,准备好失望吧。即使我们能给你一个很好的列表,列出今天可能发生的所有奇怪的事情,明天它也会过时,当优化编译器开始做一些更奇怪和不可理解的事情时。@SteveSummit我喜欢你的评论,它是脚踏实地的,我会更喜欢它
我很高兴阅读并理解这样一个列表,即使它很快就会被弃用。请注意,你不需要一个愚蠢的编译器,你所需要的只是一个指令集,其中溢出会导致陷阱,带有一个未初始化的处理程序,跳入无人区,其中恰好包含此函数调用。也许还有其他方法可以让它发生。哦,还有比“明天不一样的事情”更糟糕的事情。“经过10000名客户的测试、发布、交付和安装,他们现在要么想要拿回自己的钱,要么正在与他们的律师交谈,情况有所不同”:底层选民是否愿意发表评论?
int i = 0;
foo(i++, i++, i++);
$ 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
*"abc" = '\0';
#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");
    }
}