之前 CSAPP 的课程让我们建立了一个栈顶向下生长、堆向上生长的心智模型:

3 questions on Assembly - meaning of code, Decompilation in Linux, Higher  level perspective, - Reverse Engineering Stack Exchange

我们自然会认为,分配个一个进程的内存是有限的。栈增长遇到堆就会栈溢出,堆增长遇到栈就会分配失败。真的是这样吗?

写一个程序测试一下:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
  uint64_t *heap = new uint64_t;
  uint64_t stackvar;

  uint64_t diff = &stackvar - heap;
  float distance = diff / (1024 * 1024 * 1024);
  printf("distance = %.2f GB\n", distance);
  free(heap);

  return 0;
}

运行:

ubuntu@uclassol:~/leet/2021-12-03$ cr distance.cpp 
distance = 5445.00 G

离谱了?这只是一台 8G 内存的虚拟机,测出来栈与堆的距离竟然长达 5445GB。

其实这是因为进程所使用的是虚拟空间。虽然中间间隔这么多,但进程在未使用的时候,内存是未分配的。

那么既然如此,为何还会发生栈溢出呢?就算用 1GB 来存放栈,也不应该调用几千层就溢出呀。

为什么会发生栈溢出?

栈溢出并非是因为内存不够,而是因为程序超过了编译器规定的栈大小。例如 Visual Studio 默认栈大小是 1MB。而 Linux 上很多程序使用环境变量控制栈大小,可以用 ulimit -s NEW_SIZE 去设定。

为什么要限制栈的大小?

原因是多方面的,例如:

  • 栈内存是连续分配的,而堆内存可以分散在物理内存各处。因此栈内存不能轻易地分配和回收。
  • 每个线程的栈都是独立的,限制栈的大小能够让操作系统运行更多线程
  • 另外这也是一种错误检测机制,能够一定程度避免程序的糟糕设计。有限的栈空间很容易被 CPU 缓存,减少访存的速度。这也是程序员感觉栈似乎比堆内存更快的原因。一旦栈的大小超过了 CPU 缓存的容量,栈的速度会变得和堆一样,函数调用等操作的开销会变大。
  • 参考

一个进程的虚拟空间真的有 6000 G 吗?

实际上,在 x86-64 情况下,每个 Windows 进程的虚拟空间高达 8TB,具体可以参考 虚拟地址空间 - Windows drivers | Microsoft Docs。而 x86-32 的虚拟空间有大约 4GB. Linux 的情况比较复杂,可以参考 25.3. Memory Management — The Linux Kernel documentation Process Address Space (kernel.org)

堆内存会溢出吗?

如果堆内存不够,会引发 malloc 错误,返回空指针(但也不一定,比如在 Linux 上,malloc 永远不会失败,因为 malloc 只是宣称占用,只有真正使用的时候,才会检查到底能不能用,不能用,则首先 OOM Killer 杀死用户空间的其它进程,还是不够就抛出 Out of memeory)。

像 Java 这类跑在虚拟机内的程序,虚拟机会限制其堆内存大小(或者通过 -Xmx 参数指定)。则堆内存会溢出。

堆和栈会相遇吗?

显然,虚拟空间足够大,它们的地址几乎不可能相遇。

参考

C/C++ maximum stack size of program - Stack Overflow

c++ - why is stack memory size so limited? - Stack Overflow