函数栈帧的创建与销毁
当一个函数在运行时,需要为它在
堆栈
中创建一个栈帧
(stack frame)用来记录运行时产生的相关信息,因此每个函数在执行前都会创建一个栈帧,在它返回时会销毁该栈帧。
本节的示意图我们通过颜色区分调用者与被调用者的栈帧,蓝色表示被调用者,绿色表示调用者。
创建栈帧
通常用一个叫做栈基址
(bp)的寄存器来保存正在运行函数栈帧的开始地址
,由于栈指针
(sp)始终保存的是栈顶的地址,所以栈指针
保存的也就是正在运行函数栈帧的结束地址
。
每次发生函数调用时都要修改
栈基址
(bp)使它保存新栈帧的开始地址
,这将导致它被覆盖。因此我们可以利用之前讲到的寄存器的保存与恢复
使用栈来对栈基址
进行保存与恢复。
在一开始栈基址
和栈指针
都分别指向调用者栈帧的开始地址
和结束地址
,创建时首先将调用者栈帧的开始地址
也就是此时的栈基址
压栈保存,由于栈基址
是被调用者保存
寄存器,所以它存放在被调用的栈帧中。
栈变化过程:
随后将栈基址
(bp)修改成此时栈指针
(sp)的值,使这它们都指向同一个位置(下面左图),如果被调用函数还需要栈空间,那么它可以继续把栈指针
(sp)向低地址移动来分配空间(下面右图),最终栈基址
和栈指针
又分别指向了被调用者
栈帧的开始地址
和结束地址
。
栈变化过程:
栈帧中存储了函数参数、返回地址、保存的寄存器、局部变量,因此完整的栈结构可能像下面这样:
图中各个部分说明:
函数参数(arguments)
在
X64
中如果函数参数超过6个,前6个通过寄存器进行传递,其余参数则通过栈来进行参数传递,当少于等于6个或没有参数时,这个时候该栈帧部分可以忽略。在需要通过栈来传递参数时,
调用函数
需要先将参数压入自己的栈帧中,然后被调用函数
从调用函数
的栈帧中对参数进行访问。所以图中参数部分在调用函数的栈帧中。返回地址(ret addr)
将函数参数压栈之后,需要把调用位置处的下一条指令地址压栈,以便被调用函数执行完之后可以回到原来的位置继续执行,这个地址就是返回地址。
保存的寄存器(saved regs)
这里存放的是需要
被调用者
来保存的寄存器,例如旧的栈基址
(old bp)旧保存在其中。局部变量(local vars)
这个部分是存储在栈中而不是寄存器中的局部变量,如果函数没有局部变量或局部变量都存储在寄存器中,那么该栈帧部分可以忽略。
如果再次发生函数调用,那就重复整个创建栈帧的过程,因此对于递归函数来说和普通函数也没什么区别。
销毁栈帧
在函数返回时会把之前给这个函数创建的栈帧
销毁
,以释放空间。
销毁时先把栈指针
(sp)移动到此时栈基址
(bp)的位置,此时栈指针
和栈基址
都指向同样的位置。
栈变化过程:
现在栈顶刚好是我们在创建栈帧时保存的调用者栈帧
的栈基址
,现在把它出栈至栈基址
(bp),得到下图中的栈结构:
到目前位置被调用者
的栈帧已经被销毁空间得到释放,但是函数的返回步骤并没有完,调用者
的栈帧中还保存者返回地址
,此时需要把返回地址
出栈至程序计数器
(PC)以恢复到原来的位置继续执行,返回后的栈帧:
C/C++中销毁栈帧并不会清空被销毁栈帧中的数据。