与没有优化标志相比,memcpy 的行为有所不同

memcpy behaves differently with optimization flags compared to without

考虑这个演示程序:

#include <string.h>
#include <unistd.h>

typedef struct {
    int a;
    int b;
    int c;
} mystruct;

int main() {
    int TOO_BIG = getpagesize();
    int SIZE = sizeof(mystruct);
    mystruct foo = {
        123, 323, 232
    };

    mystruct bar;
    memset(&bar, 0, SIZE);
    memcpy(&bar, &foo, TOO_BIG);
}

我用两种方式编译:

  1. gcc -O2 -o buffer -Wall buffer.c
  2. gcc -g -o buffer_debug -Wall buffer.c

即第一次启用优化,第二次启用调试标志但没有优化。

首先要注意的是编译时没有警告,尽管 getpagesize 返回一个会导致缓冲区溢出的值 memcpy

其次,运行第一个程序产生:

*** buffer overflow detected ***: terminated
Aborted (core dumped)

而第二个产生

*** stack smashing detected ***: terminated
Aborted (core dumped)

或者,你必须在这里相信我,因为我无法用演示程序重现这一点,有时 根本没有警告。该程序甚至没有中断,它正常运行。这是我在一些更复杂的代码中遇到的行为,这使得调试变得困难,直到我意识到发生了缓冲区溢出。

我的问题是:为什么会有两种具有不同构建标志的不同行为?为什么在作为调试版本构建时有时执行没有错误,但在优化构建时总是出错?

访问分配后的内存是未定义的行为,这意味着允许编译器做任何事情。当没有优化时,编译器可能会尝试猜测并做一些合理的事情。启用优化后,编译器可能会利用允许任何行为来执行某些运行速度更快的事实。

..I can't reproduce this with the demo program, sometimes no warning at all...

undefined behavior 指令非常广泛,编译器不需要为表现出这种行为的程序发出任何警告:

why are there two different behaviours with different build flags? And why does this sometimes execute with no errors when built as a debug build, but always errors when built with optimizations?

编译器优化倾向于优化掉未使用的变量,如果 I compile your code 启用了优化我没有遇到分段错误,查看程序集(上面的 link),你会注意到有问题的变量被优化掉了,并且 memcpy 没有被调用,所以没有理由不编译成功,程序以成功代码 0 退出,而如果不优化它,未定义的行为显示出来,程序以代码 139 退出,经典的分段错误退出代码。

如您所见,这些结果与您的不同,这是未定义行为的特征之一,不同的编译器、系统甚至编译器版本的行为可能完全不同方式。

The first thing to notice is that there are no warnings when compiling, despite getpagesize returning a value that will cause buffer overflow with memcpy.

这是程序员的责任,而不是编译器。如果编译器设法为您找到潜在的缓冲区溢出,您将非常幸运。它的工作是检查您的代码是否有效,然后将其转换为机器代码。

如果您想要一个捕捉错误的工具,它们被称为静态分析器,这是一种不同类型的程序。在某种程度上,静态分析可能作为一项功能集成到编译器中。有一个用于 clang,但大多数静态分析器是商业工具而不是开源工具。

Secondly, running the first programme produces: ... whereas the second produces

未定义的行为仅仅意味着没有定义的行为。 What is undefined behavior and how does it work?。这意味着不可能从检查结果中学到任何东西,也没有什么有趣的谜团需要解开。在一种情况下,它显然访问了禁止的内存,在另一种情况下,它破坏了一个可怜的小“堆栈金丝雀”。差异将与不同的内存布局有关。谁在乎 - 错误就是错误。专注于为什么 错误发生(你已经知道了!),而不是试图理解未定义的结果。

现在,当我 运行 你的代码 实际上 真正启用时(gcc -O2 在 x86 Linux 上),编译器给出我

main:
        subq    , %rsp
        call    getpagesize
        xorl    %eax, %eax
        addq    , %rsp
        ret

实际启用优化后,它甚至不必费心调用 memcpy & friends,因为没有副作用并且不使用变量,因此可以安全地从可执行文件中删除它们。