无论是面向过程还是面向对象编程,其中都少不了大大小小的函数和方法,A调用B,B调用C。那么有一个问题就是,A调用B的过程中,B执行结束,机器是如何知道该回到哪里继续执行的呢?这便引出了栈空间和寄存器的作用。
本文主要是记录一下我对函数调用过程中的大致流程,至于具体细节,可以参考以下博客。
有必要介绍一下几个特殊的寄存器:
- ax(accumulator):用于存放函数返回值
- bp(base pointer):用于存放执行中的函数对应的栈底地址
- sp(stack pointer):用于存放执行中的函数对应的栈顶地址
- ip(instuction pointer):指向当前指令的下一条指令
下面讲一下结合上面博客,我个人的理解:
- 函数A被调用时,汇编调用call指令
- 然后执行pushl(pushq) %rbp,将上一帧(即A函数调用方)的栈底地址压栈,pushl表示将4字节长度数据压栈,而pushq则是8字节,这个应该具体和CPU字长相关(即32位4字节,64位8字节)
- 根据第二步中栈底地址所占长度,修正寄存器rsp(保存了栈顶地址)值,32位减4,64位减8
- 然后执行movl %rsp, %rbp,将此刻的栈顶地址保存到寄存器rbp中,意味着此时rbp保存了A函数所在帧的栈底地址。从这一刻开始,栈内存开始为A函数所用,后续进行一些参数压栈,指令操作不再赘述。
- A函数执行结束时,调用汇编指令leave, leave等价于以下两条指令:movl %rbp, %rsp; pop %rbp(仔细看可以发现就是函数调用开始时的逆操作)。movl %rbp, %rsp表明函数A结束,需要回到调用前的位置,因此将栈顶地址修正为开始的位置。popl %rbp将栈底地址出栈,然后根据存储地址的数据长度进行计算(popl -4; popq -8),就能获取到上一帧栈底地址存放的内存地址,从而取得上一帧栈底地址,然后根据rip寄存器中的值,计算得出下一条指令的地址,恢复执行。
了解函数调用过程中栈,寄存器的操作,以及函数参数传递,对于调试定位程序有明显的帮助。希望大家有所收获。