大纲
- 传统栈
- 程序栈
- X86体系栈
- 反向的原因
- 参考资料
如果要讲程序在系统层的运行,一个绕不开的名词就是“栈”。所以深入理解“栈”是这个系列重要的基础。本文也将深入浅出,只讲明白程序运行中使用的栈是什么。
传统栈
有计算机基础的同学都知道栈的特点:先进后出的有序结构。比如我们按顺序将1,2,3压入(push)栈,只能按3,2,1的顺序将数据从栈中弹出(pop)。
程序栈
但是对于运行在系统上的程序,操作系统并不会如上图物理性的搬运栈上元素,而是一开始就统一分配了一定大小的连续空间作为栈。
在我的开发环境Ubuntu22TLS上,默认的程序所拥有的栈大小是8192kbytes,即8MB。
我们可以通过下面的这个命令来查看
ulimit -a | grep stack
stack size (kbytes, -s) 8192
那这个连续的内存如何做到栈的特点,即有入栈和出栈的行为呢?
当一个问题不能解决的时候,我们就可以通过引入一个中间层来解决。
很容易想到,我们可以通过一个指向该连续空间中某个位置的指针来实现出入栈。
X86体系栈
以上只是理论性的讲解,而现实中,我们对栈的理解要“换个角度”。
怎么换“角度”?要反过来看。
在X86体系架构中,栈是反着的。如下图,栈的底部位于“上方”,栈的顶部位于“下方”。
那评判“上方”或者“下方”的标准是什么?是地址大小。
所以需要记住以下几点:
- 在X86体系下,栈是“反向”的。
- 栈的底部(上)地址值大,栈的顶部(下)地址值小。
- 栈在增长(Push),则其栈顶地址在减小。栈在回退(Pop),则其栈顶地址在增大。
反向的原因
如果不去看计算机发展的历史,很难想象为什么早期的计算机科学家会做出如此反常的设计。现在我们假想自己回到计算机早期设计阶段,会面对什么样的问题,以及如何解决。
我们知道,和“栈”对应的结构是“堆”。我们平时经常将“堆栈”放在一起来描述计算机一些原理。
堆栈就是用于动态保存程序运行时数据的结构。如果我们要在内存中给它们分配空间,肯定需要考虑一个问题:
- 如何尽量大的利用内存?
一种比较简单的想法就是:堆和栈用一块内存空间,即它们相连,但是又要有一个明显的间隔。这个间隔区分了堆和栈,以防止数据污染。
上述我们讲过,栈的大小是固定的(默认8MB)。这个大小就帮助“间隔”的产生。
于是存在下面两种可能:
- 栈在顶部,堆在底部。
- 栈在底部,堆在顶部。
这两个设计没有本质性的区别。但是最终早期的计算机科学家选择了栈在顶部、堆在底部的设计。
这样的设计,就意味着: - 栈的底部是高地址空间,栈的增长方向是向下(向低地址空间发展)。
- 堆的底部是低地址空间,堆的增长方向是向上(向高地址空间发展)。
如下图,是我们未来要讲解的一个例子。rbp是栈帧的栈底指针寄存器,rsp是栈帧的栈顶指针寄存器。可以看到rbp比rsp的值要大,即栈底的地址大于栈顶的地址。
参考资料
- https://stackoverflow.com/questions/4560720/why-does-the-stack-address-grow-towards-decreasing-memory-addresses