【问题标题】:memcpy behaves differently with optimization flags compared to without与没有优化标志相比,memcpy 的行为不同
【发布时间】:2021-03-25 10:30:41
【问题描述】:

考虑这个演示程序:

#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)

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

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

【问题讨论】:

  • 编译器不需要为未定义的行为生成诊断。同样,带有 UB 的程序可能会“正常”运行或产生运行时错误或其他问题。
  • 缓冲区溢出导致未定义行为,未定义行为未定义。
  • 一个具有未定义行为的程序不需要每次都做同样的事情:那将是定义的行为。并且不同的代码不需要具有相同的未定义效果。
  • @jumbot 您的问题并不愚蠢,只是“为什么在 X 情况下与 Y 情况下存在不同的未定义行为”有很多变体。例如,我记得一个问题,有人问他们为什么在尝试写入字符串文字而不是 segfualt 后会出现总线错误。这个问题听起来可能不同,但每个问题的答案都是一样的:未定义的行为是未定义的。你可以在运行这个程序时让龙侵入你的房子,它不会违反 C 标准,因为它是未定义的行为,任何事情都被允许
  • 这是一个很好的问题。不过,我不是那种“你的猫会变绿”的学校,这很愚蠢。

标签: c gcc memcpy buffer-overflow


【解决方案1】:

..我无法使用演示程序重现此问题,有时根本没有警告...

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

为什么会有两种不同的行为和不同的构建标志?为什么在构建为调试构建时有时执行时没有错误,但在构建优化时总是出错?

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

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 你的最后两个字应该改成crashes faster
    • @tofro 这根本不是真的。它也可能使程序稳定,因为未定义的行为已从二进制文件中删除。
    【解决方案3】:

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

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

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

    其次,运行第一个程序会产生: ... 而第二个会产生

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

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

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

    实际上启用了优化后,它甚至不需要调用 memcpy 和朋友,因为没有副作用并且不使用变量,因此可以安全地从可执行文件中删除它们。

    【讨论】:

    • “而不是试图理解未定义的结果” - 我只是好奇......这是不允许的吗? :)
    • @jumbot 就像您之前的数百万新手一样,每个人都提出了必须调查的相同“革命奥秘”。不过,您可以从中学到的唯一东西是编译器如何为给定的系统/ABI 生成机器代码的点点滴滴。如果您真的想学习一些有价值的东西,那么您实际上将学习给定系统的 ABI、调用约定、堆栈帧格式等,而不是您最终通过 UB 获得的那些随机位和片段。这些都不是小事要研究。然而,这正是理解错误所必需的。
    • 从检查结果中不可能学到任何东西。我完全不同意。假设您的产品功能出现故障,并且您在代码的不相关部分中发现了错误。故障消失。您如何确保您没有移动未发现的另一个错误的副作用?您通过检查实际发生的情况来做到这一点,以确保您发现的错误 对在其他地方观察到的错误负责。这对于将作为固件分发到世界各地的代码尤其重要。
    • @jumbot 我不记得曾经告诉过你不要问这个问题或走开。在那种情况下,我不会写答案,请参阅。另外,我没有投反对票。不过,您必须意识到,有些问题之前已经被问过 很多次 次,因此当被问到第 100,000 次时,人们最终会感到厌烦。与流行的看法相反,SO 实际上不是交互式初学者教程。尽管碰巧的是,如果您翻阅 SO 档案,您可以找到无数重复的旧初学者问题。
    • @WeatherVane 我的意思是,您可以教人们如何避免创建此类错误。或者更好的是,教他们解决错误。如:好吧,你得到了“堆栈粉碎”,让我们启动一个调试器,看看我们是否能找到原因。缩小问题所在的范围。这是非常有用的知识!
    猜你喜欢
    • 2019-04-10
    • 1970-01-01
    • 2019-04-28
    • 1970-01-01
    • 1970-01-01
    • 2013-06-18
    • 1970-01-01
    • 2021-04-19
    • 2014-11-13
    相关资源
    最近更新 更多