原因
计算机内存分了代码段(.text
段)、初始化的数据段(.data
段)、未初始化的数据段(.bss
段)、堆空间(heap
)、栈空间(stack
)和命令行参数和环境变量区域。
程序计数器(Program Counter
,简称PC
)的缺省指向0地址,计算机开机后从程序计数器指向的地址开始执行程序,每执行完一条指令后, 程序计数器自动加1。
因此很自然的,代码段从低地址区间开始加载,向高地址区间扩展;
heap从低地址向高地址扩展,做内存管理相对要简单些,为了避免栈空间和代码段冲突,最大利用地址空间,很自然的,我们会选择把栈底设置在高地址区间,然后让栈向下增长。
这是来自apue里一张经典的c程序内存分布图,着重看一下heap和stack的内存分布。
栈由高地址向低地址扩展的优点
stack
从高地址向低地址扩展,这样栈空间的起始位置就能确定下来。动态的调整栈空间大小也不需要移动栈内的数据,如果是从低地址到高地址的扩展,结尾的地址是固定的,如果要扩大或缩小,则需要移动整个栈的数据。
并且这样设计可以使得堆和栈能够充分利用空闲的地址空间。如果栈向上涨的话,我们就必须得指定栈和堆的一个严格分界线,但这个分界线怎么确定呢?平均分?但是有的程序使用的堆空间比较多,而有的程序使用的栈空间比较多。
所以就可能出现这种情况:一个程序因为栈溢出而崩溃的时候,其实它还有大量闲置的堆空间呢,但是我们却无法使用这些闲置的堆空间。所以呢,最好的办法就是让堆和栈一个向上涨,一个向下涨,这样它们就可以最大程度地共用这块剩余的地址空间,达到利用率的最大化!
现在 CPU 指令集的设计
大部分CPU指令集设计了函数调用架构,定义了专用的调用/返回指令,并在指令中隐含规定栈的方向。
- 主流1:向低地址扩展:x86,MIPS
- 主流2:自由选择:Arm(但个别指令仅支持向低)
- 罕见:向高地址扩展:PA-RISC,操作系统Multics
- 非主流:System z,栈是个链表[2]
如果CPU同时支持向上和向下,例如arm,那么编译器需要指定程序的调用方向,一般还是选择向下。比较罕见的极端的案例是Multics操作系统,这是Unix的巨无霸前身,设计者刻意选用向高地址扩展,因为该架构有助于防御缓冲区溢出攻击[3]。
引用
[1] What is the logical explanation for stacks typically growing downward and heaps growing upward?
[2] 列出了8种架构的栈增长方向 What is the direction of stack growth in most modern systems?
[3] 罕见的栈增长方向 Why does the stack grow downward?