内存读取没有malloc

use*_*417 -3 c memory-management compiler-optimization undefined-behavior

我写了一个C程序如下:

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

int main() {
  int *a;
  foo(a);
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

我期待这个程序崩溃,因为我没有在&a [1000]分配内存,但程序实际上没有崩溃并打印"Hello".我用命令编译了程序

gcc -O0 foo.c
Run Code Online (Sandbox Code Playgroud)

可能是什么原因?

Har*_*ris 8

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

现在,seg fault如果您访问的内存仅限于您的程序,则可能导致这两种情况.

或者,就像你的情况一样,它没有任何明确的效果.它可能会读取前面程序留下的垃圾值.这种行为称为undefined.

它可能在你的情况下工作一段时间,但它肯定不会一直有效.


Enz*_*ber 6

TL; DR

正如大家已经指出的那样,访问越界内存是未定义的行为.但是,在这种特殊情况下会发生一些非常有趣的事情,使您的程序根本无法访问内存.死代码被删除了!

它并不能保证,但大多数高质量的编译器都会优化if(1) { ... }if(0){ ... }(gcc即使是这样)-O0.检查这个答案这个答案.


逻辑推理

您的编译器正在if基于简单的逻辑"优化"该条件,这就是为什么它始终与-O0标志一起工作.这种内存访问永远不会发生.当你的编译器发现a[1000] == a[1000],或者a[n] == a[n]它确实知道它与说VAR == VAR任何变量的相同时基本相同,并且对于任何变量总是如此.这来自形式逻辑,被称为身份原则,它表明任何元素A都等于自身.我不知道是否有特定的优化标志,但我认为没有(特别是因为它发生在-O0).如果有人知道,请在评论中告诉我.

换句话说,你的编译器交换你的if(a[1000] == a[1000])for 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.
Run Code Online (Sandbox Code Playgroud)

注意说明mov %rdi,-0x8(%rbp).这会将函数参数保存到堆栈中.那是你的指针.在它之后,它存储$0x4005f4edi(可能是数据段中"Hello"字符串的地址)并设置eax为零,然后调用printf.让我们检查:

(gdb) print (char*)0x4005f4
$3 = 0x400614 "Hello"
Run Code Online (Sandbox Code Playgroud)

靶心!好吧,等等!那是哪里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.
Run Code Online (Sandbox Code Playgroud)

哇,那更长!

现在,平常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 ()
Run Code Online (Sandbox Code Playgroud)

请注意,在尝试执行指令后400543,它会崩溃.什么在4005430x0000000000400543 <+22>:mov (%rax),%edx.确切地说,它试图访问一个出界的内存.繁荣!有你未定义的行为.

  • 很棒的答案:).但需要注意的是 - "a [1000] == a [1000]`实际上可能不是真的,因为负载不是原子的.其他一些线程可能会改变它.然而,编译器认为类似的事情 - "每次运行时都是相等的,这是一种可能有效的结果,因此我可以让它始终发生",并继续消除代码. (2认同)