Skip to content

栈与函数调用

做 PA4 的时候,栈的方向给我搞晕了,就顺道整理一下。下面主要用 x86 来讲解,别的汇编语言也是同样的道理。

首先是介绍一下栈指针寄存器(stack pointer): sp 寄存器。它永远保存栈顶元素的地址。程序的栈放在内存中的某个区域并向下增长。也就是说,每 push 一个元素,sp 的地址减少,pop 则让 sp 的地址增加。

pushq %rbp 等价于

sub $8,rsp
movq %rbp,(%rsp)

popq %rax 等价于

movq (%rsp),%rax
addq $8,(%rsp)

利用栈,我们就可以实现函数了。将栈指针减少一个适当的量可以分配空间,这段分配的空间叫做栈帧(stack frame)。分配空间后赋值本质上是和将需要的值一个个 push 进来是一样的。反过来,增加栈指针相当于释放空间。

在调用一个函数时,首先 call 指令会将返回地址压入栈中,然后进入调用的函数。在新函数中,首先要减少栈指针的值分配空间,然后依次保存寄存器的值,分配局部变量空间,分配传入的参数所需的空间。这些都完成后,再执行之后的过程。执行完毕后,将栈指针加回原来的值释放空间,ret 指令将栈顶的地址 pop 给 pc,就从函数返回了。下面是一个函数 P 调用函数 Q 的局部栈图。

看这张图片已经很直观了,栈底的地址高,栈顶的地址低。

值得一提的是,C 语言参数是从右往左压入堆栈的,所以假定 (%rsp) 是第一个元素,那么 4(%rsp) 是第二个,8(%rsp) 是第三个元素,以此类推。如果传入的是数组或者结构体,方向也是一样的。可以想象,倒过来将元素压入堆栈导致顺序符合内存增长的方向。