本文撰写的起因在于学习 Pydbg 源码的时候,其中有一个函数 stack_unwind 对函数调用过程进行了解析,并涉及到了栈帧。另外,Linux 0.11 的汇编部分也多次涉及用汇编实现函数栈帧。
栈帧结构
栈帧结构
栈帧指的是系统为某个过程(如函数)单独分配的部分栈。对于其结构,《深入理解计算机系统》一书有一个比较简明的图示:
图片来源:《深入理解计算机系统》
从上图,可以得到以下几个比较重要的结论:
- 调用者与被调用者的栈帧是互相独立的;
- 被调用者的实参来自调用者压栈的数据(Argument 1 ~ n);
- 调用者最后压栈的数据是被调用者返回(ret)后的执行地址(Return address),即在调用者中调用被调用者处的
下一条指令
的地址; - 被调用者栈帧压栈的第一个值是调用者的帧指针(ebp),以便调用返回时恢复调用者的帧指针。
栈帧样例
另外,《深入理解计算机系统》作者还举了一个例子来说明在调用(call)和返回(ret)时刻栈帧的变化,其中汇编代码如下所示:
1 | Beginning of function sum |
用图可表示如下:
样例过程可描述如下:
- 在刚调用 sum 函数时,eip (0x080483dc)指向 main 函数中 call sum 的指令,esp (0xff9bc960)指向 main 函数栈帧顶部。
- 调用 sum 函数后,将 sum 函数返回地址(0x080483e1)、也即 main 函数中 call sum 的下一条指令的地址压到 main 函数的栈顶;eip (0x08048394)指向 sum 函数;将 main 函数的帧指针 ebp 压到 sum 函数的栈帧中;esp 向下增长递减 4 个字节(0xff9bc95c = 0xff9bc960 - 4)。这里,需要特别注意的是 esp 跟 ebp 并没有什么关系,esp 的偏移并不基于 ebp 而是整个栈的底部(Stack Bottom)。
- 函数返回时,sum 函数自动清理自己建立的栈帧,恢复 main 函数的帧指针 ebp;eip 指向 main 函数中 call sum 的下一条指令;esp 自动递增到原来的值(0xff9bc960)。
esp 和 ebp
在前头我们提到 esp 跟 ebp 并没有什么关系,这跟实模式下的段机制不一样。实际上,这也是跟 esp 是 32 位长有关系,因为 32 位长度允许 esp 定位到内存的任意位置,这样 ebp 自然用不到。有时候,ebp 可能不做帧指针寄存器用,而当做一般寄存器使用。
一般来说,我们通常是在调用函数后,将 esp 的值赋予 ebp,从此,在被调用函数栈帧中,ebp 保持不变,esp 根据实际情况变化。一个例子如下:
1 | int add(int x, int y) |
编译汇编及反汇编:
最终结果:
Pydbg 中的 stack_unwind 函数
stack_unwind 函数定义如下:
1 | def stack_unwind (self, context=None): |