函数栈帧

当一个函数在运行时,需要为它在堆栈中创建一个栈帧(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++中销毁栈帧并不会清空被销毁栈帧中的数据。

results matching ""

    No results matching ""