c - GNU C 编译器破坏未定义的行为

标签 c gcc undefined-behavior

我有一个嵌入式项目,需要在某个时候写入地址 0。所以我很自然地尝试:

*(int*)0 = 0 ;
但是在优化级别 2 或更高级别时,gcc 编译器会搓手说,实际上,“这是未定义的行为!我可以做我喜欢做的事!哇哈哈!”并向代码流发出无效指令!
这是我的源文件:
void f (void)
  {
  *(int*)0 = 0 ;
  }
这是输出列表:
    .file   "bug.c"
    .text
    .p2align 4,,15
    .globl  _f
    .def    _f; .scl    2;  .type   32; .endef
_f:
LFB0:
    .cfi_startproc
    movl    $0, 0
    ud2                <-- Invalid instruction!
    .cfi_endproc
LFE0:
    .ident  "GCC: (i686-posix-dwarf-rev0, Built by MinGW-W64 project) 7.3.0"
我的问题是:为什么有人会这样做?像这样破坏代码可能会带来什么好处?当然,显而易见的做法是发出警告并继续编译?
我知道编译器可以这样做,我只是想知道编译器作者的动机。我花了两天时间和四个工程样本来追踪这个问题,所以我有点生气。
编辑添加:我已经通过使用汇编语言解决了这个问题。所以我不是在寻找解决方案。我只是好奇为什么有人会认为这种编译器行为是个好主意。

最佳答案

(免责声明:我不是 GCC 内部的专家,这更像是解释其行为的“事后”尝试。但也许它会有所帮助。)

the gcc compiler rubs its hands and says, in effect, "That is undefined behaviour! I can do what I like! Bwahaha!" and emits an invalid instruction to the code stream!


我不会否认在某些情况下 GCC 或多或少会这样做,但这里有更多的事情发生,并且有一些方法可以解决它的疯狂问题。
据我了解,GCC 并没有将空取消引用视为完全未定义的;它正在对其所做的事情做出一些假设。它对空引用的处理由一个名为 -fdelete-null-pointer-checks 的标志控制。 ,这可能在您打开优化时默认启用。来自 manual :

-fdelete-null-pointer-checks

Assume that programs cannot safely dereference null pointers, and that no code or data element resides at address zero. This option enables simple constant folding optimizations at all optimization levels. In addition, other optimization passes in GCC use this flag to control global dataflow analyses that eliminate useless checks for null pointers; these assume that a memory access to address zero always results in a trap, so that if a pointer is checked after it has already been dereferenced, it cannot be null.

Note however that in some environments this assumption is not true. Use -fno-delete-null-pointer-checks to disable this optimization for programs that depend on that behavior.

This option is enabled by default on most targets. On Nios II ELF, it defaults to off. On AVR, CR16, and MSP430, this option is completely disabled.

Passes that use the dataflow information are enabled independently at different optimization levels.


因此,如果您打算实际访问地址 0,或者由于某些其他原因您的代码将在取消引用后继续执行,那么您需要使用 -fno-delete-null-pointer-checks 禁用它。 .这将实现您想要的“继续编译”部分。然而,它不会给你警告,大概是在这种取消引用是有意的假设下。

但是在默认选项下,为什么你会看到你生成的代码,带有未定义的指令,为什么没有警告?我猜 GCC 的逻辑运行如下:
  • 因为 -fdelete-null-pointer-checks实际上,编译器假定执行不会继续超过空取消引用,而是会陷入困境。陷阱将如何处理,它不知道:可能是程序终止,可能是信号或异常处理程序,可能是 longjmp上堆栈。 null 取消引用本身是根据请求发出的,可能是在您有意执行陷阱处理程序的假设下。但无论哪种方式,空取消引用之后出现的任何代码现在都无法访问。
  • 所以现在它做了任何合理的优化编译器对无法访问的代码所做的事情:它不会发出它。在你的情况下,这只不过是一个 ret ,但不管它是什么,就 GCC 而言,它只会浪费内存字节,应该被省略。
    您可能认为您应该在这里收到警告,但 GCC 长期以来的设计决定是不对无法访问的代码发出警告,因为此类警告往往不一致,误报弊大于利。参见例如 https://gcc.gnu.org/legacy-ml/gcc-help/2011-05/msg00360.html .
  • 但是,作为一项安全功能,GCC 会发出一条未定义的指令(x86 上的 ud2)来代替省略的无法访问的代码。我相信,这个想法是,万一执行以某种方式继续超过空取消引用,最好让程序死亡,而不是陷入杂草并尝试执行接下来发生的任何内存内容。 (实际上,即使在取消映射零页的系统上也可能发生这种情况;例如,如果您执行 struct huge *p = NULL; p->x = 0; ,GCC 会将其理解为空取消引用,即使 p->x 可能根本不在零页上,并且可以想象位于一个可访问的地址。)

  • 有一个警告标志,-Wnull-dereference ,这将触发对您公然取消引用的警告。但是,它仅适用于 -fdelete-null-pointer-checks已启用。

    GCC 的行为什么时候有用?这是一个例子,可能是人为的,但它可能会传达这个想法。想象一下你的程序有一些可能会失败的分配函数:
    struct foo *p = get_foo();
    // do other stuff for a while
    if (!p) {
        // 5000 lines of elaborate backup plan in case we can't get a foo
    }
    frob(p->bar);
    
    现在想象你重新设计 get_foo()以免失败。您忘记取出“备份计划”代码,但您继续使用返回的对象:
    struct foo *p = get_foo();
    frob(p->bar);
    // do other stuff for a while
    if (!p) {
        // 5000 lines of elaborate backup plan in case we can't get a foo
    }
    
    编译器事先不知道 get_foo()将始终返回一个有效的指针。但是它可以看到您已取消引用它,因此可以假设只有在指针不为空的情况下才会继续执行该点。因此,它可以告诉你精心设计的备份计划是不可达的,应该被省略,这将为你的二进制文件节省大量的膨胀。

    顺便说一下clang的情况。尽管正如 Eric Postpischil 指出的那样,您确实收到了警告,但您没有得到的是来自地址 0 的实际负载:clang 忽略了它并只发出 ud2 .这就是“随心所欲”的真正样子,如果您希望使用页面零陷阱处理程序,那您就不走运了。

    关于c - GNU C 编译器破坏未定义的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64970018/

    相关文章:

    C While 循环表现得很有趣

    c - 对 `__lzcnt16 的 undefined reference ?

    c++ - 相同的代码由 clang 编译但在 gcc 中失败

    rust - 在没有未定义行为的情况下,如何在 Rust 中的对等点之间共享不安全状态?

    c - C中fgetc/fputc和fread/fwrite的速度比较

    c - 递增文件指针后打印文件中的字符

    c - 预增量和后增量

    c - 指向可变修改类型的指针的指针算法

    c - 在 C 中的运行之间重用/保存读取的数据数组

    macos - OS X上gprof的问题: [program] is not of the host architecture