【问题标题】:Memory read without malloc没有malloc的内存读取
【发布时间】:2016-01-30 15:53:19
【问题描述】:

我写了一个C程序如下:

void foo(int *a) {
  if (a[1000] == a[1000]) {
    printf("Hello");
  } 
}

int main() {
  int *a;
  foo(a);
  return 0;
}

我预计这个程序会崩溃,因为我没有在 &a[1000] 处分配内存,但程序实际上并没有崩溃并打印“Hello”。我用命令编译了程序

gcc -O0 foo.c

这可能是什么原因?

【问题讨论】:

  • 未定义的行为是未定义的。
  • 仅仅因为它似乎工作并不意味着它实际上按预期工作。
  • 它甚至做内存访问吗? 'x==x' 对所有 x 都是正确的,那何必呢?
  • 我没有看两边就过马路,也没有被公共汽车碾过。为什么不?过马路总是安全的吗?
  • @MartinJames 正是如此。此代码中从未发生内存访问,即使在-O0 中,GCC 也会优化此条件。我发布了一个答案。

标签: c memory-management compiler-optimization undefined-behavior


【解决方案1】:

访问尚未分配的内存位置是未定义的行为。

现在,如果您正在访问的内存受限于您的程序,这可能会导致 seg fault

或者,就像您的情况一样,它不会有任何定义的效果。它可能会读取先前程序留下的垃圾值。这种行为称为未定义。

它可能在您的情况下在特定时间内有效,但绝对不会一直有效。

【讨论】:

    【解决方案2】:

    TL;DR

    正如大家已经注意到的,访问越界内存是Undefined Behavior然而,在这种特殊情况下发生了一些非常有趣的事情,使您的程序根本无法访问内存。死代码已被删除!

    不能保证,但大多数高质量的编译器会优化if(1) { ... }if(0){ ... }(这正是gcc 的情况),即使在-O0 中也是如此。检查this answerthis answer


    逻辑推理

    您的编译器正在基于简单的逻辑“优化”if 条件,这就是为什么即使使用-O0 标志它也始终有效的原因。这种内存访问永远不会发生。当您的编译器找到a[1000] == a[1000] 或真正的a[n] == a[n] 时,它知道这与说VAR == VAR 本质上是一样的,这对于任何变量都是相同的并且对于任何变量总是如此。这来自Formal Logic,被称为Principle of Identity,它表明任何元素A 都等于它自己。我不知道是否有特定的优化标志,但我认为没有(特别是因为它发生在-O0)。如果有人知道,请在 cmets 中告诉我。

    换句话说,您的编译器将您的if(a[1000] == a[1000]) 替换为if(1),这始终正确,因此它完全删除了if

    需要注意的是,访问越界内存总是未定义的行为,但是,在这种情况下,翻译后的代码永远不会访问任何内存。为了证明这一点,一些反汇编代码:

    您提供的代码,使用gcc -O0 -o foo foo.c 编译输出以下foo 函数:

    (gdb) disass foo
    Dump of assembler code for function foo:
       0x000000000040052d <+0>: push   %rbp
       0x000000000040052e <+1>: mov    %rsp,%rbp
       0x0000000000400531 <+4>: sub    $0x10,%rsp
       0x0000000000400535 <+8>: mov    %rdi,-0x8(%rbp)
       0x0000000000400539 <+12>:mov    $0x4005f4,%edi
       0x000000000040053e <+17>:mov    $0x0,%eax
       0x0000000000400543 <+22>:callq  0x400410 <printf@plt>
       0x0000000000400548 <+27>:leaveq 
       0x0000000000400549 <+28>:retq   
    End of assembler dump.
    

    注意指令mov %rdi,-0x8(%rbp)。这是将函数参数保存到堆栈中。那是你的指针。紧随其后,它将$0x4005f4 存储到edi(这可能是数据段中“Hello”字符串的地址)并将eax 设置为零,然后调用printf。让我们检查一下:

    (gdb) print (char*)0x4005f4
    $3 = 0x400614 "Hello"
    

    靶心!嗯,等等! if 在哪里?我在这里看不到任何cmp 指令,或任何其他类型的分支.... if 被“优化”了。这并不是 GCC 的真正优化选项,而是 逻辑 优化。 1 总是等于 1。编译器知道输出机器码之前,所以你的if 从来没有得到二进制文件,也没有完成内存访问。

    但是,如果您要执行 if(a[1000] == a[1001]) 并使用相同的 gcc -O0 -o foo foo.c 进行编译,您将得到这个 foo

    (gdb) disass foo
    Dump of assembler code for function foo:
       0x000000000040052d <+0>: push   %rbp
       0x000000000040052e <+1>: mov    %rsp,%rbp
       0x0000000000400531 <+4>: sub    $0x10,%rsp
       0x0000000000400535 <+8>: mov    %rdi,-0x8(%rbp)
       0x0000000000400539 <+12>:mov    -0x8(%rbp),%rax
       0x000000000040053d <+16>:add    $0xfa0,%rax
       0x0000000000400543 <+22>:mov    (%rax),%edx
       0x0000000000400545 <+24>:mov    -0x8(%rbp),%rax
       0x0000000000400549 <+28>:add    $0xfa4,%rax
       0x000000000040054f <+34>:mov    (%rax),%eax
       0x0000000000400551 <+36>:cmp    %eax,%edx
       0x0000000000400553 <+38>:jne    0x400564 <foo+55>
       0x0000000000400555 <+40>:mov    $0x400614,%edi
       0x000000000040055a <+45>:mov    $0x0,%eax
       0x000000000040055f <+50>:callq  0x400410 <printf@plt>
       0x0000000000400564 <+55>:leaveq 
       0x0000000000400565 <+56>:retq   
    End of assembler dump.
    

    哇,更长了!

    现在,通常的mov %rdi,-0x8(%rbp) 就在那里。这是将我们的参数保存到堆栈中。下一行mov -0x8(%rbp),%rax 将我们的指针加载到rax。然后,add $0xfa0,%rax 将我们的1000 * sizeof(int) 偏移量添加到rax 中。到现在为止,一切都很好。现在,mov (%rax),%edx 尝试访问 rax 指向的内容并将其存储在 edx 中。换句话说,这是真正的指针解引用。如果您在 GDB 上执行单步指令,您将在此指令中获得 SIGSEGV

    Breakpoint 1, 0x0000000000400531 in foo ()
    (gdb) stepi
    0x0000000000400535 in foo ()
    (gdb) stepi
    0x0000000000400539 in foo ()
    (gdb) stepi
    0x000000000040053d in foo ()
    (gdb) stepi
    0x0000000000400543 in foo ()
    (gdb) stepi
    
    Program received signal SIGSEGV, Segmentation fault.
    0x0000000000400543 in foo ()
    

    请注意,在它尝试执行400543 处的指令后,它会崩溃。 400543 中有什么内容? 0x0000000000400543 &lt;+22&gt;:mov (%rax),%edx。正是它试图访问超出范围的内存的位置。繁荣!这是你未定义的行为。

    【讨论】:

    • 很棒的答案:)。不过需要注意的是 - a[1000] == a[1000] 实际上可能不是真的,因为负载不是原子的。其他一些线程可能会改变它。然而,编译器认为类似 - “每次我运行时都相等是一个可能且有效的结果,因此我可以让它总是发生”,然后继续消除代码。
    • 感谢您的精彩回答!显然,这个问题不仅仅是未定义的行为,而且这是唯一详细识别和解释它的答案。
    • @Leeor 编译器认为不是volatile,所以允许缓存。
    【解决方案3】:

    未定义行为的副作用之一是预期输出。

    但这并不能证明定义了UB

    【讨论】:

      【解决方案4】:

      它没有崩溃的一个解释是编译器可能已经优化掉了a[1000] == a[1000],因为这个表达式总是正确的。

      尝试使用a[1000] != a[1001],也许你每次都会崩溃。

      但无论如何,这是未定义的行为。

      【讨论】:

        【解决方案5】:

        您的程序可能会崩溃(分段错误)或不崩溃。

        它不会崩溃的事实并不意味着它可以工作。实际上这是未定义的行为,这意味着任何事情都可能发生。它可以读取一些随机值,也可以由于分段错误而崩溃。因此,当您测试它时它现在可以工作的事实并不意味着它会一直工作。

        例如,您可以尝试运行您的程序几次,但您可能会遇到段错误。

        这是因为语言标准没有规定很多东西。

        【讨论】:

          【解决方案6】:

          这里的int *a 是一个堆栈变量,由于堆栈变量没有预先初始化它包含一些垃圾值。

          幸运的是,这个垃圾值在允许的地址或该程序内,因此程序没有恐慌。

          【讨论】:

            【解决方案7】:

            这里的行为是未定义的,在这里你试图访问一个你不知道的内存。这个随机内存位置可能保存一些关键数据,也可能只是一个可以使用的好位置。
            情况 1:您的程序将因“分段错误”而崩溃。
            案例 2:您的程序可以正常打印“Hello World”。
            由于模棱两可的程序不是一个很好的程序,因此我们避免这种做法。
            现在我们有了更好的操作系统,您只会遇到分段错误,否则在此程序可能会导致您的系统崩溃之前的几天。

            【讨论】:

            • 在这个程序可能导致您的系统崩溃之前的几天。真的吗?只是想知道这个小错误是否真的会导致系统崩溃。
            猜你喜欢
            • 2012-11-18
            • 2015-03-22
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-03-29
            • 2014-03-28
            • 2013-10-13
            • 2014-01-22
            相关资源
            最近更新 更多