堆栈分配在Linux中如何工作?


18

操作系统是否为堆栈或其他内容保留了固定数量的有效虚拟空间?仅使用大局部变量就能产生堆栈溢出吗?

我写了一个小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 -s10240在我的电脑上打印。

如您所见,在两种情况下,堆栈大小都大于堆栈大小ulimit -s。并且堆栈随着更大的局部变量而增长。堆栈的顶部以某种方式会降低3-5kB &a[0](AFAIK,红色区域是128B)。

那么如何分配此堆栈映射?

Answers:


14

似乎未分配堆栈内存限制(无论如何,堆栈数量不受限制)。https://www.kernel.org/doc/Documentation/vm/overcommit-accounting说:

C语言堆栈增长执行隐式mremap。如果您需要绝对的保证并靠近边缘,则必须将堆栈映射为您认为需要的最大尺寸。对于典型的堆栈使用而言,这并不重要,但是如果您真的很在意,那将是一个极端的情况

但是,映射堆栈将是编译器的目标(如果有此功能的话)。

编辑:在x84_64 Debian机器上进行了一些测试之后,我发现堆栈在没有任何系统调用的情况下增长了(根据strace)。因此,这意味着内核会自动增长它(这就是上面的“隐式”的意思),即没有显式的mmap/ mremap来自进程。

很难找到证实这一点的详细信息。我建议由Mel Gorman 理解《 Linux虚拟内存管理器》。我想答案是在第4.6.1节“ 处理页面错误”中,例外情况是“区域无效,但在可扩展区域(如堆栈)旁边”和相应的动作“扩展区域并分配页面”。另请参见D.5.2 扩展堆栈

有关Linux内存管理的其他参考(但几乎没有关于堆栈的信息):

编辑2:此实现有一个缺点:在角落情况下,即使在堆栈大于限制的情况下,也可能无法检测到堆栈堆冲突!原因是对堆栈中变量的写操作可能最终会在分配的堆内存中结束,在这种情况下,不会出现页面错误,并且内核无法知道需要扩展堆栈。请参阅我在gcc-help列表中开始的GNU / Linux下的无声堆栈堆冲突讨论中的示例。为了避免这种情况,编译器需要在函数调用时添加一些代码。-fstack-check对于GCC,可以这样做(有关详细信息,请参见Ian Lance Taylor的答复和GCC手册页)。


这似乎是我问题的正确答案。但这让我更加困惑。什么时候会触发mremap调用?它将是内置在程序中的syscall吗?
阿莫斯(Amos)2014年

@amos我假设如果需要在函数调用时或在调用alloca()时将触发mremap调用。
vinc17 2014年

对于不了解的人,最好提一下mmap是什么。
Faheem Mitha 2014年

@FaheemMitha我添加了一些信息。对于那些不知道mmap是什么的人,请参阅上面提到的内存常见问题解答。在这里,对于堆栈来说,它应该是“匿名映射”,以便未使用的空间不会占用任何物理内存,但是正如梅尔·戈尔曼(Mel Gorman)所解释的那样,内核会同时进行映射(虚拟内存)和物理分配。
vinc17 2014年

1
@max我已经尝试在OP的程序中ulimit -s给出10240,就像在OP的条件下一样,并且得到了预期的SIGSEGV(这是POSIX所要求的:“如果超出此限制,则将为线程生成SIGSEGV。 ”)。我怀疑OP的内核中存在错误。
vinc17

6

Linux内核4.2

最少的测试程序

然后,我们可以使用最小的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指定的数据放到了堆栈上:Assembly | 堆栈溢出

如果您对此很认真,那么TODO会制作一个最小的initrd映像,该映像从堆栈顶部开始写入然后向下移动,然后使用QEMU + GDB运行它。把一个dprintf环路上打印栈地址,并在断点处acct_stack_growth。这将是光荣的。

有关:


2

默认情况下,每个进程的最大堆栈大小配置为8MB,
但可以使用ulimit以下命令进行更改:

在kB中显示默认值:

$ ulimit -s
8192

设置为无限:

ulimit -s unlimited

影响当前的shell和子shell及其子进程。
ulimit是一个shell内置命令)

您可以显示
cat /proc/$PID/maps | grep -F '[stack]'
在Linux上使用的实际堆栈地址范围:


因此,当当前外壳加载程序时,OS将使ulimit -sKB 内存段对该程序有效。在我的情况下为10240KB。但是,当我声明一个本地数组char a[10240*1024]并设置时a[0]=1,程序将正确退出。为什么?
阿莫斯(Amos)2014年

尝试设置最后一个元素。并确保没有对其进行优化。
vinc17

@amos我认为vinc17的意思是您命名了一个不适合程序堆栈的内存区域,但是由于您实际上并未在不适合的部分中访问它,因此机器永远不会注意到- 它不会甚至得到那个信息
Volker Siegel

@amos尝试int n = 10240*1024; char a[n]; memset(a,'x',n);...段错误。
goldilocks 2014年

2
@amos因此,如您所见,a[]尚未在10MB堆栈中分配。编译器可能已经看到不可能进行递归调用,并且已经进行了特殊分配,或者发生了诸如不连续堆栈或某种间接调用之类的事情。
vinc17 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.