实际上在C

时间:2017-09-08 15:34:00

标签: c gcc undefined-behavior

我已经阅读了很多关于未定义行为(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引导到一般异常处理程序吗?或者我错过了什么?

4 个答案:

答案 0 :(得分:3)

实际上,大多数编译器都使用以下任一方式使用未定义的行为:

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

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

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

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

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

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

E)这是一个语法正确的程序,因此编译器肯定会产生一个&#34;工作&#34;二进制文件。从理论上讲,这个二进制文件可以与你在互联网上下载并运行的任何二进制文件具有相同的行为。最有可能的是,你得到一个直接退出的二进制文件,或者打印上述信息并立即退出。

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

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

答案 1 :(得分:2)

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

我喜欢编程,但我也喜欢完成一个程序,然后继续处理其他事情。我不喜欢不断修补我已编写的程序,让他们面对因硬件,编译器或其他情况不断变化而自发发展的错误。

所以当我编写一个程序时,它还不够工作。它必须以正确的理由工作。我必须知道它有效,并且它将在下周,下个月和明年继续工作。它似乎无法正常工作,对于迄今为止我运行它的必要有限的一组测试用例给出了明显正确的答案。

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

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

答案 2 :(得分: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 :(得分:0)

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

阅读这些帖子: