操作系统是否为堆栈或其他内容保留固定数量的有效虚拟空间?我可以仅通过使用大的局部变量来产生堆栈溢出吗?
我写了一个小C
程序来测试我的假设。它在 X86-64 CentOS 6.5 上运行。
#include <string.h>
#include <stdio.h>
int main()
{
int n = 10240 * 1024;
char a[n];
memset(a, 'x', n);
printf("%x\n%x\n", &a[0], &a[n-1]);
getchar();
return 0;
}
运行程序给出&a[0] = f0ceabe0
和&a[n-1] = f16eabdf
proc 映射显示了堆栈:7ffff0cea000-7ffff16ec000. (10248 * 1024B)
然后我尝试增加n = 11240 * 1024
运行程序给出&a[0] = b6b36690
和&a[n-1] = b763068f
proc 映射显示了堆栈:7fffb6b35000-7fffb7633000. (11256 * 1024B)
ulimit -s
在我的电脑上打印10240
。
正如您所看到的,在这两种情况下,堆栈大小都大于ulimit -s
给出的大小。并且堆栈随着局部变量的增大而增长。堆栈顶部不知何故少了 3-5kB &a[0]
(据我所知,红色区域是 128B)。
那么这个堆栈映射是如何分配的呢?
答案1
看来堆栈内存限制未分配(无论如何,它不能无限堆栈)。https://www.kernel.org/doc/Documentation/vm/overcommit-accounting说:
C 语言堆栈的增长会进行隐式的 mremap。如果您想要绝对的保证并接近边缘运行,则必须将堆栈映射为您认为需要的最大大小。对于典型的堆栈使用来说,这并不重要,但如果你真的很关心的话,这就是一个极端的情况
然而,映射堆栈将是编译器的目标(如果它有一个选项)。
编辑:在 x84_64 Debian 机器上进行一些测试后,我发现堆栈在没有任何系统调用的情况下增长(根据strace
)。因此,这意味着内核会自动增长它(这就是上面“隐式”的意思),即没有显式mmap
/mremap
来自进程。
很难找到证实这一点的详细信息。我建议了解 Linux 虚拟内存管理器梅尔·戈尔曼着。我想答案在第 4.6.1 节中处理页面错误,例外情况是“区域无效,但位于可扩展区域(如堆栈)旁边”以及相应的操作“扩展区域并分配页面”。另见 D.5.2扩展堆栈。
关于 Linux 内存管理的其他参考资料(但几乎没有关于堆栈的内容):
- 内存常见问题解答
- 每个程序员都应该了解的内存知识作者:乌尔里希·德雷珀
编辑2:此实现有一个缺点:在极端情况下,即使堆栈大于限制,也可能无法检测到堆栈堆冲突!原因是,对堆栈中变量的写入可能最终会进入分配的堆内存,在这种情况下,不会出现页面错误,并且内核无法知道堆栈需要扩展。请参阅我在讨论中的示例GNU/Linux 下静默的栈堆冲突我从 gcc-help 列表开始。为了避免这种情况,编译器需要在函数调用时添加一些代码;这可以通过 GCC 完成-fstack-check
(有关详细信息,请参阅 Ian Lance Taylor 的回复和 GCC 手册页)。
答案2
Linux内核4.2
- 毫米/mmap.c#acct_stack_growth决定是否会出现段错误。它使用
rlim[RLIMIT_STACK]
与 POSIX 相对应的gerlimit(RLIMIT_STACK)
- arch/x86/mm/fault.c#do_page_fault是启动一个链的中断处理程序,该链最终调用
acct_stack_growth
- 拱门/x86/entry/entry_64.S设置页面错误处理程序。您需要了解一些有关分页的知识才能理解该部分:x86 分页如何工作? |堆栈溢出
最小测试程序
然后我们可以使用最小的 NASM 64 位程序对其进行测试:
global _start
_start:
sub rsp, 0x7FF000
mov [rsp], rax
mov rax, 60
mov rdi, 0
syscall
确保关闭 ASLR 并删除环境变量,因为这些变量将进入堆栈并占用空间:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
env -i ./main.out
该限制略低于我的限制ulimit -s
(对我来说是 8MiB)。看起来这是因为除了环境之外,最初放入堆栈的额外的 System V 指定数据:汇编中的 Linux 64 命令行参数堆栈溢出
如果你认真对待这个问题,TODO制作最小的 initrd 映像从栈顶开始写入并向下写入,然后使用 QEMU + GDB 运行它。dprintf
在循环中放置一个打印堆栈地址,并在 处放置一个断点acct_stack_growth
。这将是辉煌的。
有关的:
答案3
默认情况下,最大堆栈大小配置为每个进程 8MB,
但可以使用以下命令进行更改ulimit
:
显示默认值(以 kB 为单位):
$ ulimit -s
8192
设置为无限制:
ulimit -s unlimited
影响当前 shell 和子 shell 及其子进程。
(ulimit
是 shell 内置命令)
您可以使用以下命令显示正在使用的实际堆栈地址范围:
cat /proc/$PID/maps | grep -F '[stack]'
在 Linux 上。