没有 malloc 的内存读取
Memory read without malloc
我写了一个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
这可能是什么原因?
未定义行为的副作用之一是预期输出。
但这并不能证明UB被定义
访问未分配的内存位置是未定义的行为。
现在,这可能导致 seg fault
,如果您正在访问的内存对您的程序有限制。
或者,就像您的情况一样,它不会有任何明确的效果。它可能会读取先前程序留下的垃圾值。这种行为称为未定义。
它可能在特定时间对您的情况有效,但肯定不会一直有效。
您的程序可能会崩溃(分段错误)或不会崩溃。
它不崩溃的事实并不意味着它有效。实际上这是未定义的行为,意味着任何事情都可能发生。它要么读取一些随机值,要么由于分段错误而崩溃。因此,当您测试它时它现在有效并不意味着它会一直有效。
例如,您可以尝试 运行 您的程序几次,您可能会遇到段错误。
这是因为语言标准没有规定很多东西。
它没有崩溃的一个解释是编译器可能已经优化掉了 a[1000] == a[1000]
因为这个表达式总是正确的。
试试 a[1000] != a[1001]
也许每次都会崩溃。
但无论如何这是未定义的行为。
这里的行为是未定义的,你在这里试图访问你不知道的内存。这个随机内存位置可能保存着一些关键数据,也可能只是一个可以使用的好位置。
情况 1:您的程序将因 "segmentation fault".
而崩溃
在情况 2 中:您的程序可以很好地打印 "Hello World".
由于模棱两可的程序不是很好的程序,因此我们避免这种做法。
现在我们有更好的 OS,你只会遇到分段错误,否则在这个程序可能会导致你的系统崩溃之前的几天。
这里的int *a
是一个栈变量,由于栈变量没有被预初始化,它包含了一些垃圾值。
幸运的是这个垃圾值在允许的地址或那个程序之内,所以程序没有恐慌。
TL;DR
正如大家已经注意到的,越界访问内存是 Undefined Behavior。 但是,在这种特殊情况下发生了一些非常有趣的事情,使您的程序根本无法访问内存。死代码已删除!
不能保证,但大多数高质量的编译器会优化 if(1) { ... }
或 if(0){ ... }
(恰恰是 gcc
的情况),即使在 -O0
中也是如此。检查 this answer and this answer.
逻辑推理
您的编译器 "optimizing" if
条件基于简单的逻辑,这就是为什么即使使用 -O0
标志它也始终工作。这种内存访问永远不会发生。当你的编译器发现 a[1000] == a[1000]
,或者真的 a[n] == a[n]
时,它 知道 这与说 VAR == VAR
本质上是一样的,这对任何变量都是相同的 并且对任何变量都始终为真。这来自 Formal Logic and is called Principle of Identity,它指出任何元素 A
都等于其自身。我不知道是否有特定的优化标志,但我认为没有(特别是因为它发生在 -O0
中)。如果有人知道,请在评论中告诉我。
换句话说,您的编译器将 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 [=10=]x10,%rsp
0x0000000000400535 <+8>: mov %rdi,-0x8(%rbp)
0x0000000000400539 <+12>:mov [=10=]x4005f4,%edi
0x000000000040053e <+17>:mov [=10=]x0,%eax
0x0000000000400543 <+22>:callq 0x400410 <printf@plt>
0x0000000000400548 <+27>:leaveq
0x0000000000400549 <+28>:retq
End of assembler dump.
注意指令mov %rdi,-0x8(%rbp)
。这是将函数参数保存到堆栈中。那是你的指针。在它之后,它将 [=31=]x4005f4
存储到 edi
(这可能是数据段中 "Hello" 字符串的地址)并将 eax
设置为零,然后调用 printf
。让我们检查一下:
(gdb) print (char*)0x4005f4
= 0x400614 "Hello"
靶心!好吧,等等! if
在哪里?我在这里没有看到任何 cmp
说明,或任何其他类型的分支....if
得到了 "optimized"。它实际上不是 GCC 的优化选项,而是 logic 优化。 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 [=12=]x10,%rsp
0x0000000000400535 <+8>: mov %rdi,-0x8(%rbp)
0x0000000000400539 <+12>:mov -0x8(%rbp),%rax
0x000000000040053d <+16>:add [=12=]xfa0,%rax
0x0000000000400543 <+22>:mov (%rax),%edx
0x0000000000400545 <+24>:mov -0x8(%rbp),%rax
0x0000000000400549 <+28>:add [=12=]xfa4,%rax
0x000000000040054f <+34>:mov (%rax),%eax
0x0000000000400551 <+36>:cmp %eax,%edx
0x0000000000400553 <+38>:jne 0x400564 <foo+55>
0x0000000000400555 <+40>:mov [=12=]x400614,%edi
0x000000000040055a <+45>:mov [=12=]x0,%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 [=45=]xfa0,%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
。正是它试图访问越界内存的位置。繁荣!这是你未定义的行为。
我写了一个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
这可能是什么原因?
未定义行为的副作用之一是预期输出。
但这并不能证明UB被定义
访问未分配的内存位置是未定义的行为。
现在,这可能导致 seg fault
,如果您正在访问的内存对您的程序有限制。
或者,就像您的情况一样,它不会有任何明确的效果。它可能会读取先前程序留下的垃圾值。这种行为称为未定义。
它可能在特定时间对您的情况有效,但肯定不会一直有效。
您的程序可能会崩溃(分段错误)或不会崩溃。
它不崩溃的事实并不意味着它有效。实际上这是未定义的行为,意味着任何事情都可能发生。它要么读取一些随机值,要么由于分段错误而崩溃。因此,当您测试它时它现在有效并不意味着它会一直有效。
例如,您可以尝试 运行 您的程序几次,您可能会遇到段错误。
这是因为语言标准没有规定很多东西。
它没有崩溃的一个解释是编译器可能已经优化掉了 a[1000] == a[1000]
因为这个表达式总是正确的。
试试 a[1000] != a[1001]
也许每次都会崩溃。
但无论如何这是未定义的行为。
这里的行为是未定义的,你在这里试图访问你不知道的内存。这个随机内存位置可能保存着一些关键数据,也可能只是一个可以使用的好位置。
情况 1:您的程序将因 "segmentation fault".
而崩溃
在情况 2 中:您的程序可以很好地打印 "Hello World".
由于模棱两可的程序不是很好的程序,因此我们避免这种做法。
现在我们有更好的 OS,你只会遇到分段错误,否则在这个程序可能会导致你的系统崩溃之前的几天。
这里的int *a
是一个栈变量,由于栈变量没有被预初始化,它包含了一些垃圾值。
幸运的是这个垃圾值在允许的地址或那个程序之内,所以程序没有恐慌。
TL;DR
正如大家已经注意到的,越界访问内存是 Undefined Behavior。 但是,在这种特殊情况下发生了一些非常有趣的事情,使您的程序根本无法访问内存。死代码已删除!
不能保证,但大多数高质量的编译器会优化 if(1) { ... }
或 if(0){ ... }
(恰恰是 gcc
的情况),即使在 -O0
中也是如此。检查 this answer and this answer.
逻辑推理
您的编译器 "optimizing" if
条件基于简单的逻辑,这就是为什么即使使用 -O0
标志它也始终工作。这种内存访问永远不会发生。当你的编译器发现 a[1000] == a[1000]
,或者真的 a[n] == a[n]
时,它 知道 这与说 VAR == VAR
本质上是一样的,这对任何变量都是相同的 并且对任何变量都始终为真。这来自 Formal Logic and is called Principle of Identity,它指出任何元素 A
都等于其自身。我不知道是否有特定的优化标志,但我认为没有(特别是因为它发生在 -O0
中)。如果有人知道,请在评论中告诉我。
换句话说,您的编译器将 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 [=10=]x10,%rsp
0x0000000000400535 <+8>: mov %rdi,-0x8(%rbp)
0x0000000000400539 <+12>:mov [=10=]x4005f4,%edi
0x000000000040053e <+17>:mov [=10=]x0,%eax
0x0000000000400543 <+22>:callq 0x400410 <printf@plt>
0x0000000000400548 <+27>:leaveq
0x0000000000400549 <+28>:retq
End of assembler dump.
注意指令mov %rdi,-0x8(%rbp)
。这是将函数参数保存到堆栈中。那是你的指针。在它之后,它将 [=31=]x4005f4
存储到 edi
(这可能是数据段中 "Hello" 字符串的地址)并将 eax
设置为零,然后调用 printf
。让我们检查一下:
(gdb) print (char*)0x4005f4
= 0x400614 "Hello"
靶心!好吧,等等! if
在哪里?我在这里没有看到任何 cmp
说明,或任何其他类型的分支....if
得到了 "optimized"。它实际上不是 GCC 的优化选项,而是 logic 优化。 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 [=12=]x10,%rsp
0x0000000000400535 <+8>: mov %rdi,-0x8(%rbp)
0x0000000000400539 <+12>:mov -0x8(%rbp),%rax
0x000000000040053d <+16>:add [=12=]xfa0,%rax
0x0000000000400543 <+22>:mov (%rax),%edx
0x0000000000400545 <+24>:mov -0x8(%rbp),%rax
0x0000000000400549 <+28>:add [=12=]xfa4,%rax
0x000000000040054f <+34>:mov (%rax),%eax
0x0000000000400551 <+36>:cmp %eax,%edx
0x0000000000400553 <+38>:jne 0x400564 <foo+55>
0x0000000000400555 <+40>:mov [=12=]x400614,%edi
0x000000000040055a <+45>:mov [=12=]x0,%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 [=45=]xfa0,%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
。正是它试图访问越界内存的位置。繁荣!这是你未定义的行为。