TL;DR
正如大家已经注意到的,访问越界内存是Undefined Behavior。 然而,在这种特殊情况下发生了一些非常有趣的事情,使您的程序根本无法访问内存。死代码已被删除!
不能保证,但大多数高质量的编译器会优化if(1) { ... } 或if(0){ ... }(这正是gcc 的情况),即使在-O0 中也是如此。检查this answer 和this 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 <+22>:mov (%rax),%edx。正是它试图访问超出范围的内存的位置。繁荣!这是你未定义的行为。