函数调用栈和栈帧

在计算机科学中,Callstack 是指存放某个程序的正在运行的函数的信息的栈。Call stack 由 stack frames 组成,每个 stack frame 对应于一个未完成运行的函数。

在当今流行的计算机体系架构中,大部分计算机的参数传递,局部变量的分配和释放都是通过操纵程序栈来实现的。栈用来传递函数参数,存储返回值信息,保存寄存器以供恢复调用前处理机状态。每次调用一个函数,都要为该次调用的函数实例分配栈空间。为单个函数分配的那部分栈空间就叫做 stack frame,也就是说,stack frame 这个说法主要是为了描述函数调用关系的。

Stack frame 组织方式的重要性和作用体现在两个方面:第一,它使调用者和被调用者达成某种约定。这个约定定义了函数调用时函数参数的传递方式,函数返回值的返回方式,寄存器如何在调用者和被调用者之间进行共享;第二,它定义了被调用者如何使用它自己的 stack frame 来完成局部变量的存储和使用。


|------------------|<--high address
|     ......       | Previous
|     ......       |  frames
|------------------|<-----------
| registers and    |
| local variables  |
|------------------|  Caller's
|   argument 6     |
|   argument 5     |  frame
|   argument 4     |
|   argument 3     |
|   argument 2     |
|   argument 1     |
|------------------|<--previous sp
|  return address  |(n-4)sp
|------------------|(n-8)sp
| saved registers  |
|     ......       | Current
|------------------|  frame
| local  variables |(Callee's
|     ......       |  frame )
|------------------|
|   next calling   |
|  arguments list  |
|------------------|<--sp
|     ......       |
|__________________|<--low address

图片链接:http://solrex.googlepages.com/frame.jpg

上图描述的是一种典型的(MIPS O32)嵌入式芯片的 stack frame 组织方式。在这张图中,计算机的栈空间采用的是向下增长的方式,SP(stack pointer) 就是当前函数的栈指针,它指向的是栈底的位置。Current Frame 所示即为当前函数(被调用者)的 frame ,Caller’s Frame 是当前函数的调用者的 frame 。每个 frame 中所存放的内容和存放顺序,则由目标体系架构的调用约定(calling convention)定义。如图所示,MIPS O32调用约定规定了所占空间不大于4 个字节的参数应该放在从 $4到 $8 的寄存器中,剩下的参数应该依次放到调用者 stack frame 的参数域中,并且在参数域中需要为前四个参数保留栈空间;如果被调用者需要使用 $16 到 $23 这些保留寄存器(saved register),就必须先将这些保留寄存器的值保存在被调用者 stack frame 的保留寄存器域中,当被调用者返回时恢复这些寄存器值;当被调用者不是叶子函数时,即被调用者中存在对其它函数的调用,需要将 RA(return address) 寄存器 ($31) 值保存到被调用者 stack frame 的返回值域中;被调用者所需要使用的局部变量,应保存在被调用者 stack frame 的本地变量域中。

在没有 BP(base pointer) 寄存器的目标架构中,进入一个函数时需要将当前栈指针向下移动 n 字节,这个大小为 n 字节的存储空间就是此函数的 stack frame 的存储区域。此后栈指针便不再移动,只能在函数返回时再将栈指针加上这个偏移量恢复栈现场。由于不能随便移动栈指针,所以寄存器压栈和出栈都必须指定偏移量,这与 x86 架构的计算机对栈的使用方式有着明显的不同。

在 RISC 计算机中主要参与计算的是寄存器,saved registers 就是指在进入一个函数后,如果某个保存原函数信息的寄存器会在当前函数中被使用,就应该将此寄存器保存到堆栈上,当函数返回时恢复此寄存器值。而且由于 RISC 计算机大部分采用定长指令或者定变长指令,一般指令长度不会超过32个位。而现代计算机的内存地址范围已经扩展到 32 位,这样在一条指令里就不足以包含有效的内存地址,所以RISC计算机一般借助于一个返回地址寄存器 RA(return address) 来实现函数的返回。几乎在每个函数调用中都会使用到这个寄存器,所以在很多情况下 RA 寄存器会被保存在堆栈上以避免被后面的函数调用修改,当函数需要返回时,从堆栈上取回 RA 然后跳转。移动 SP 和保存寄存器的动作一般处在函数的开头,叫做 function prologue;恢复这些寄存器状态的动作一般放在函数的最后,叫做 function epilogue。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注