大纲
- RBP,RSP
- 栈帧边界
- 总结
- 参考资料
在《从汇编层看64位程序运行——栈帧(Stack Frame)入门》中,我们简单介绍了栈帧的概念,以及它和函数调用之间的关系。如文中所述,栈帧是一种虚拟的概念,它表达了一个执行中的函数的栈空间区域。在这个区域中,该函数的局部非静态变量便被置于这个空间中,即我们常常说起的栈上变量。
那么这个区间是通过什么划分的?
RBP,RSP
这就需要引入寄存器这个概念。
寄存器(Register)是中央处理器内用来暂存指令、数据和地址的存储器。 寄存器的存贮容量有限,读写速度非常快。 在计算机体系结构里,寄存器存储在已知时间点所作计算的中间结果,通过快速地访问数据来加速计算机程序的执行。
CPU有非常多的寄存器,本节我们主要讲解与栈帧相关的两个寄存器:
- 栈底指针寄存器(Stack Base pointer register): 在16位系统中,有个寄存器叫bp;在32位系统中,这个寄存器叫ebp;在64位系统中,这个寄存器叫rbp。
- 栈顶指针寄存器(Stack pointer register): 在16位系统中,有个寄存器叫sp;在32位系统中,这个寄存器叫esp;在64位系统中,这个寄存器叫rsp。
需要注意的是,栈底指针寄存器(rbp)保存的并不是当前栈帧的栈底地址,而是保存了栈上变量分配的起始地址。
如下图,0x7fffffffdf20到0x7fffffffdf10是main函数栈帧的部分,它保存了函数调用需要用到的一些信息(我们在《从汇编层看64位程序运行——函数的调用和栈平衡》将会介绍)。从0x7fffffffdf10开始的内存,保存的是main函数局部非静态变量的值。
栈顶指针寄存器(rsp)不仅可以被直接设置,还会随着栈上Push和Pop的操作而改变。这个特性和rbp有很大不同,rbp只能直接被设置,栈上Push和Pop操作并不会直接导致rbp的值发生变动。
如果函数有局部非静态变量,则在函数开始时,编译器会计算好这个函数需要的局部变量空间,然后通过调整rsp的值(值变小,即栈增长),来声明当前函数的栈上变量空间。
比如下面代码的反汇编,在汇编代码的+8行,通过sub $0x10,%rsp,让当前函数的栈扩展了0x10个字节。
int main() {
int b = 16;
b = b + 16;
foo();
return 0;
}
栈帧边界
有了上述的铺垫,我们通过info stack和info frame来查看到程序运行到foo函数时的栈帧
可以看到一个函数的栈帧起始地址是上一个栈帧的rsp,栈帧结束地址该函数进入下一栈帧前的rsp。
需要注意的是“该函数进入下一栈帧前的rsp”并不一定等于局部变量空间的最后一个地址!
因为在局部变量空间之后,调用者函数可能还需要将一些参数Push到栈中,从而传递给调用者。这个时候rsp会继续扩张。于是栈帧的结束地址也在扩张。关于这块知识我们可以在《从汇编层看64位程序运行——参数传递的底层实现》中看到。
正因为存在需要使用栈来给不同函数传递参数的情况,让一个函数的栈帧的结束地址是持续变化的。比如一个函数调用的A,需要栈上传递1个参数;这个函数调用的B,需要栈上传递2个参数。那么相较于进入A函数,进入B函数时调用者函数的栈帧就要更大些(大1个参数空间)。
如下图main函数的栈帧在它调用不同函数时,其栈帧的结束地址是持续变化的。
总结
- 栈帧运行时虚拟的结构。
- 函数的栈帧起始地址是调用者调用本函数时RSP的值。
- 函数的栈帧的结束地址分为两种情况:
- 有调用其他函数时。结束地址是调用其他函数时RSP的值。
- 没有调用其他函数时。结束地址是栈上最后一个变量的结束地址。
- 栈帧的结束地址可能会随着其调用其他函数而不停改变。
参考资料
- https://www.eecg.utoronto.ca/~amza/www.mindsec.com/files/x86regs.html