int main(int argc, char* argv[])
{
int arr[3];
arr[4] = 99;
}
这个缓冲区的空间是从哪里来的?这是分配给进程的堆栈吗?
以下是我不久前为了教育目的而编写的内容...
考虑下面这个 C 程序:
int q[200];
main(void) {
int i;
for(i=0;i<2000;i++) {
q[i]=i;
}
}
编译并执行后,会产生一个核心转储(core dump):
$ gcc -ggdb3 segfault.c
$ ulimit -c unlimited
$ ./a.out
Segmentation fault (core dumped)
现在使用gdb进行死后分析:
$ gdb -q ./a.out core
Program terminated with signal 11, Segmentation fault.
[New process 7221]
#0 0x080483b4 in main () at s.c:8
8 q[i]=i;
(gdb) p i
$1 = 1008
(gdb)
哦,当我写超出分配的200个项目时,程序没有出现段错误,而是在i=1008处崩溃了,为什么呢?
输入页面。
在UNIX/Linux上有几种方法可以确定页面大小,一种方法是使用系统函数sysconf(),像这样:
#include <stdio.h>
#include <unistd.h> // sysconf(3)
int main(void) {
printf("The page size for this system is %ld bytes.\n",
sysconf(_SC_PAGESIZE));
return 0;
}
这将会输出:
此系统的页面大小为4096字节。
或者可以使用命令行实用工具 getconf,如下所示:
$ getconf PAGESIZE
4096
死后分析
事实证明,段错误并不出现在i = 200处,而是在i = 1008处,让我们找出原因。启动gdb进行一些死后分析:
$gdb -q ./a.out core
Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
[New process 4605]
#0 0x080483b4 in main () at seg.c:6
6 q[i]=i;
(gdb) p i
$1 = 1008
(gdb) p &q
$2 = (int (*)[200]) 0x804a040
(gdb) p &q[199]
$3 = (int *) 0x804a35c
q[]以地址0x804a35c结束,或者说,q[199]的最后一个字节在该位置。正如我们之前看到的那样,页大小为4096字节,机器的32位字长将虚拟地址分解为20位页号和12位偏移量。
q[]结束时所在的虚拟页号为:
0x804a = 32842 偏移量:
0x35c = 860 因此,在分配q[]的内存页上仍有:
4096 - 864 = 3232 字节剩余空间。这个空间可以容纳:
3232 / 4 = 808 个整数,而代码则将其视为包含在q的第200到1008个位置的元素。
我们都知道这些元素是不存在的,编译器也没有抱怨,硬件也没有,因为我们对该页具有写权限。只有当i=1008时,q[]才引用了一个不具备写权限的不同页上的地址,虚拟内存硬件检测到了这一点,并触发了段错误。
一个整数占4个字节,这意味着这一页包含808(3236/4)个额外的虚假元素,这意味着仍然可以完全合法地从q[200]、q[201]一直访问到元素199+808=1007(q[1007]),而不会触发段错误。当访问q[1008]时,进入了一个具有不同权限的新页面。
由于您正在数组范围之外写入,因此您的代码行为是未定义的。
未定义行为的本质是任何事情都可能发生,包括缺少segfaults(编译器没有义务执行边界检查)。
您正在写入尚未分配但恰好存在且可能未被用于其他任何目的的内存。如果您对看似不相关的代码部分、操作系统、编译器、优化标志等进行更改,则您的代码可能会表现出不同的行为。
换句话说,一旦进入该领域,所有赌注都取消了。
关于本地变量缓冲区溢出在何时/何处崩溃取决于以下几个因素:
请记住,堆栈是向下增长的。也就是说,进程执行始于指针接近内存作为堆栈的“末尾”的位置。不过它并不会从最后一个映射的字开始,这是因为系统初始化代码可能会决定在进程创建时向进程传递某种“启动信息”,而通常是在堆栈上进行。
这是通常的故障模式——从包含溢出代码的函数返回时崩溃。
如果在堆栈上写入缓冲区的总数据量大于之前使用的堆栈空间的总量(由调用者/初始化代码/其他变量),那么你将在第一次运行超出堆栈顶部(开头)的内存访问时崩溃。崩溃地址将刚好在页面边界之后——由于访问了未映射的内存而导致SIGSEGV
。
如果该总量小于此时使用的堆栈的已用部分大小,则一切都可以正常工作,但会在稍后崩溃——实际上,在将返回地址存储在堆栈上的平台(如x86/x64)中,从函数返回时。这是因为CPU指令ret
实际上从堆栈中取出一个字(返回地址)并将执行重定向到那里。如果这个地址包含任何垃圾而不是预期的代码位置,则会发生异常并导致程序终止。
举个例子:当调用main()
时,在32位x86 UNIX程序上,堆栈看起来像这样:
[ esp ] <return addr to caller> (which exits/terminates process)
[ esp + 4 ] argc
[ esp + 8 ] argv
[ esp + 12 ] envp <third arg to main() on UNIX - environment variables>
[ ... ]
[ ... ] <other things - like actual strings in argv[], envp[]
[ END ] PAGE_SIZE-aligned stack top - unmapped beyond
当 main()
开始执行时,它会在栈上为各种目的分配空间,其中包括用于容纳即将溢出的数组。这将使其看起来像:
[ esp ] <current bottom end of stack>
[ ... ] <possibly local vars of main()>
[ esp + X ] arr[0]
[ esp + X + 4 ] arr[1]
[ esp + X + 8 ] arr[2]
[ esp + X + 12 ] <possibly other local vars of main()>
[ ... ] <possibly other things (saved regs)>
[ old esp ] <return addr to caller> (which exits/terminates process)
[ old esp + 4 ] argc
[ old esp + 8 ] argv
[ old esp + 12 ] envp <third arg to main() on UNIX - environment variables>
[ ... ]
[ ... ] <other things - like actual strings in argv[], envp[]
[ END ] PAGE_SIZE-aligned stack top - unmapped beyond
arr[2]
更远的位置。#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int i, arr[3];
for (i = 0; i < atoi(argv[1]); i++)
arr[i] = i;
do {
printf("argv[%d] = %s\n", argc, argv[argc]);
} while (--argc);
return 0;
}
尝试一下当你溢出缓冲区一点点(比如说10个bit)时和溢出栈末端时,崩溃会有多大差异。使用不同的优化级别和编译器进行尝试,这很形象地展示了行为不良(可能不会正确打印所有的argv[]
),以及各种地方的崩溃,甚至是无限循环(如果编译器将i
或argc
放入堆栈中并且代码在循环期间覆盖它)。
void main(int argc, char* argv[])
{
std::vector<int> arr(3);
arr.at(4) = 99;
}
如果出现异常,您将会收到一个异常提示。
C++提供了检查和未检查两种接口。选择使用哪一种取决于您自己。
这是未定义的行为——你简单地没有观察到任何问题。最有可能的原因是你覆盖了程序行为并不依赖之前的内存区域——那块内存在技术上是可写的(在大多数情况下,堆栈大小约为1兆字节),而你没有看到任何错误指示。你不应该依赖于这个。
你的代码存在未定义行为。这意味着它可以做任何事情或什么都不做。根据你的编译器和操作系统等,它可能会崩溃。
话虽如此,对于许多编译器,如果不是大多数编译器,你的代码甚至 无法编译。
这是因为你使用了 void main
,而 C 标准和 C++ 标准都要求使用 int main
。
唯一支持 void main
的编译器是微软的 Visual C++。
这是一个 编译器缺陷,但由于微软有很多示例文档甚至代码生成工具都会生成 void main
,他们可能永远不会修复它。然而,请考虑编写符合标准的 int main
比编写针对 Microsoft 的 void main
多输入一个字符。那么为什么不遵循标准呢?
祝好!
当进程试图覆盖它没有拥有的内存页时,会发生分段错误;除非您在缓冲区的末尾远离了很长一段路,否则不会触发分段错误。
堆栈位于应用程序拥有的内存块中的某个位置。在这种情况下,如果您没有覆盖重要内容,那么您只是幸运而已。您可能已经覆盖了一些未使用的内存。如果您再不走运一点,您可能已经覆盖了堆栈上另一个函数的堆栈帧。
显然,当您要求计算机在内存中分配一定数量的字节时,例如: char array[10] 它会给我们一些额外的字节,以避免出现段错误,但是仍然不安全使用这些字节,尝试访问更多的内存最终会导致程序崩溃。
sizeof(int)==4
,在这种情况下,您已经从堆栈中分配了可怜的12个字节。您的代码正在数组的末尾之外进行写入。这不是堆栈溢出,而是_未定义行为_。 - David Hammenarr[3]
的意思是“指定3个可用于我的使用的int
空间”,它并不意味着“从虚无中创建3个int
空间”,尽管如果这在物理上是可能的话,那将是一种合法的实现方式。你正在涂写arr
旁边(事实上是隔壁的下一个地址)的任何内存/地址,正如David所说的那样,这是UB。是的,它是你的堆栈的一部分(C和C++标准没有谈论堆栈,但在实践中,这就是自动变量的位置)。 - Steve Jessoparr[3] = 99;
已经足够。 - Peter - Reinstate Monica