目录 Table of Contents
过程调用与栈
基于栈的编程语言
基于栈的一个特征就是支持递归
- 支持递归
C, Pascal, Java
代码是可重入的, 也就是说同一个过程或者同一个函数, 它有多个实例在运行.
A 一直调用自身一直递归下去的话, 等同于 A 的若干实例都没有返回, 都没有返回说明在同时运行.
因此需要有一块区域来存储每个过程实例的数据, 包括参数, 局部变量, 返回地址
- 栈的工作规律
每个过程运行时间有限, 每个实例在运行栈里会占着一块地方, 这块区域的生命周期和你过程实例的生命周期是一样的. 从被调用开始, 到返回时结束.
一般情况下, 被调用者先于调用者返回
- 这样就需要每一个过程实例在栈中维护这一个过程自己私有的一个栈帧.
栈帧
每一个栈帧就是每一个函数在运行栈里面维护了一块内存, 来存放它私有的内容, 这个私有内容包括它的局部变量, 返回地址临时空间
栈帧的分配和释放 : Setup 代码就是分配栈帧空间, 后面 Finishi 代码释放栈帧空间
栈帧实际上就是栈顶一块连续的区域, 它的首地址和尾地址分别是由 esp 和 ebp 指定. esp 是栈顶, 栈顶是低地址; ebp 是高地址, ebp 是栈帧的开头, esp 是栈帧的结尾.
有些编译器不一样, 我们在这里认为编译器把 ebp 作为栈帧基址寄存器, 其它编译器可能不一样.
x86-32/Linux 下的栈帧
当前栈帧的内容, 也就是当前正在被执行的那个函数, 自栈顶向下, 它的栈帧里面放着子过程参数, 局部变量, 被保存的寄存器的值, 父过程栈帧起始地址......
实例
我们回到我们最开始的那个代码 swap()
void swap(int *xp, int *yp)
{
int t0 = *xp;
int t1 = *yp;
*xp = t1;
*yp = t0;
}
然后我们的父过程是这样调用 swap() 的 :
int zip1 = 15213;
int zip2 = 91125;
void call_swap()
{
swap(&zip1, &zip2);
}
call_swap:
......
pushl $zip2
pushl $zip1
call swap
......
注意这个参数它是从右往左压栈, 先压 zip2, 再压 zip1
函数内部取出来就是先取 zip1 再取 zip2 了.
call 之后就进入 swap()
swap:
; Setup
pushl %ebp
movl %esp. %ebp
pushl %ebx
; Body
movl 12(%ebp), %ecx
movl 8(%ebp), %edx
movl (%ecx), %eax
movl (%edx), %ebx
movl %eax, (%edx)
movl %ebx, (%ecx)
; Finish
movl -4(%ebp), %ebx
movl %ebp, %esp
pop %ebp
ret
Setup 建立栈帧, 让 ebp esp 指向栈帧的首尾, 这样我函数就要修改 ebp 了, 所以在修改之前, 我要把 ebp 原来的值压栈, esp 就指向旧的 ebp 了; 然后 movl %esp, %ebp
把这个 ebp 拉下来, 这时候 ebp 指向旧的 ebp 了.
现在 ebp, esp 都指向栈顶了, 然后 pushl %ebx
. 这是为什么呢, 就是我在我这个 swap() 里面修改了 ebx 寄存器, 所以我要把 ebx 原来的值保存下来, 以免和父过程产生冲突
|-----------|
12 | yp |
|-----------|
8 | xp |
|-----------|
4 | Rtn addr |
|-----------|
0 | Old %ebp | <--- %ebp
|-----------|
-4 | Old %ebx | <--- %esp
|-----------|
ebp + offset 上下浮动来访问栈帧的内容
然后 Finish 过程就是相反的了
这个图解释下, 就是假设 esp 最开始是 12, 然后函数调用传参数把 yp, xp, 返回地址压栈. 从上往下添加东西. 这个图与上一节图不一样, 原因是经过了
pushl %ebp
movl %esp, %ebp
这个操作.