堆栈,在 C 等语言中使用,不是典型的 LIFO。之所以称为堆栈,是因为它的使用方式类似于 LIFO:当调用一个过程时,会将一个新帧推入堆栈。框架通常包含局部变量和簿记信息,例如返回的位置。类似地,当一个过程返回时,它的帧从堆栈中弹出。
这并没有什么神奇之处。编译器(不是操作系统)分配一个寄存器用作 stack pointer - 我们称之为SP。按照惯例,SP 指向下一个空闲堆栈字的内存位置:
+----------------+ (high address)
| argument 0 |
+----------------+
| argument 1 |
+----------------+
| return address |
+----------------+
| local 0 |
+----------------+
| local 1 |
+----------------+ +----+
| free slot | <-------------- | SP |
+----------------+ (low address) +----+
要将值压入堆栈,我们执行以下操作(在伪汇编中):
STORE [SP], 42 ; store the value 42 at the address where SP points
SUB SP, 1 ; move down (the stack grows down!) to the next stack location
符号[SP] 被读作“SP 指向的内存单元的内容”。一些体系结构,尤其是 x86,提供了一个push 指令,它同时进行存储和减法。要弹出(并丢弃)堆栈中的 n 个顶部值,我们只需将 n 添加到 SP*。
现在,假设我们要访问上面的 local 0 字段。如果我们的 CPU 有base+offset 寻址模式,那就很容易了!假设SP 指向上图中的空闲槽。
LOAD R0, [SP+2] ; load "local 0" into register R0
注意我们如何不需要首先将local 0从堆栈中弹出,因为我们可以使用它与堆栈指针的偏移量来引用任何字段。
根据编译器和机器体系结构,可能有另一个寄存器指向局部变量和参数之间(或附近)的区域。这个寄存器,通常称为frame pointer,在堆栈指针移动时保持固定。
我想强调一个事实,通常情况下,操作系统根本不参与堆栈操作。内核分配初始堆栈,并可能监视其增长,但将值的推送和弹出留给用户程序。
*为简单起见,我假设机器字长为 1 字节,这就是我们从 SP 中减去 1 的原因。在 32 位机器上,将一个字压入堆栈意味着减去(至少)四个字节。