栈帧 Stack Frame

栈帧 Stack Frame

前言

最近在 Coursera 上学习 Maryland 大学的 「Software Security」 课程。第一周讲的是有关 Memory-based attacks 的介绍,在第二个视频 Memory Layout 中,涉及到了 Stack Frame 的知识点,有所疑惑,特此写一篇博客,尽可能清晰地解释 Stack Frame 的作用。

基础知识

栈、调用栈、栈帧

在当今多数计算机体系架构中,函数的参数传递、局部变量的分配和释放都是通过操纵栈来实现的。栈还用来存储返回值信息、保存寄存器以供恢复调用前处理机状态。

每次调用一个函数,都要为该次调用的函数实例分配栈空间。为单个函数分配的那部分栈空间就叫做栈帧( Stack Frame )。调用栈( Call Stack )就是正在使用的栈空间,由多个嵌套调用函数所使用的栈帧组成。具体来说,Call Stack 就是指存放某个程序的正在运行的函数的信息的栈。Call Stack 由 Stack Frames 组成,每个 Stack Frame 对应于一个未完成运行的函数。

需要注意的是,在内存中,栈是从高地址向低地址延伸的,即栈底对应高地址,栈顶对应低地址。

寄存器

在计算机领域,寄存器是CPU内部的元件,它是有限存贮容量的高速存贮部件,可用来暂存指令、数据和地址。

寄存器分为通用寄存器和特殊寄存器。通用寄存器有 ax/bx/cx/dx/di/si,在大多数指令中可以任意选用,但也有一些规定某些指令只能用某个特定的「通用」寄存器;特殊寄存器有 bp/sp/ip 等,特殊寄存器均有特定用途。

在 Stack Frame 中,涉及到三种重要的特殊寄存器:

  • bp ( base pointer ) 寄存器
  • sp ( stack poinger ) 寄存器
  • ip ( instruction pointer ) 寄存器

需要注意的是,不同架构的CPU,寄存器名称会添加不同的前缀来表示寄存器的大小。例如对于x86架构,字母「e」用作名称前缀,表示寄存器大小为32位;对于x86_64架构,字母「r」用作名称前缀,表示寄存器大小为64位。

栈帧

栈帧的定义

在维基百科上,并没有 Stack Frame 的词条,但在 Call Stack 的词条中,有关于 Stack Frame 的解释:

……
A call stack is composed of stack frames (also called activation records or activation frames). These are machine dependent and ABI-dependent data structures containing subroutine state information. This data is sometimes referred to as CFI (Call Frame Information). Each stack frame corresponds to a call to a subroutine which has not yet terminated with a return. For example, if a subroutine named DrawLine is currently running, having been called by a subroutine DrawSquare, the top part of the call stack might be laid out like in the picture on the right.
……
The stack frame at the top of the stack is for the currently executing routine. The stack frame usually includes at least the following items (in push order):

  • the arguments (parameter values) passed to the routine (if any);
  • the return address back to the routine’s caller (e.g. in the DrawLine stack frame, an address into DrawSquare’s code);
  • space for the local variables of the routine (if any).

其中,它举了一个例子,内容为一个子程序 DrawSquare 函数调用另一个子程序 DrawLine 函数,在这个过程中,调用栈的布局图( Call Stack Layout )如下:

然而,在参考网上不少资料后,我发现这一幅图的描述并不太准确,原因是它的 Frame Pointer 指向了返回地址与传递给被调函数的参数之间,而且在图中的栈帧并未有存放前一个栈帧 Frame Pointer 的区域。

因此,我想引用另一幅较为准确的调用栈布局图来说明栈帧的定义「参考于 Intel x86 Function-call Conventions - Assembly View,写得非常好的一篇文章!强烈推荐!」,图如下所示:

由图可得:

  • 栈帧按顺序地保存有传递给被调函数( Callee )的参数,返回调用函数( Caller )的地址(即图中 old %EIP),调用函数( Caller )Frame Pointer 地址( 即图中的 old %EBP ),被调函数( Callee )的局部变量,以及在被调函数( Callee )中会使用到的保存寄存器的旧值( 即图中的 saved %reg )。 注意的是,并不是在任何情况下,栈帧都会有以上所有的数据,因为有可能没有参数传递,有可能没有局部变量,也有可能不会使用保存寄存器( 指 saved %reg )。

涉及的特殊寄存器有如下特点( 在x86的环境下 ):

  • ESP 寄存器为 Stack Pointer ,它始终指向栈顶的位置。
  • EIP 寄存器为返回地址,它是调用函数( Caller )在执行完 Call 指令后的下一条指令的地址。
  • EBP 寄存器为 Frame Pointer( 亦称 Base Pointer ),它被用作在当前的栈帧中寻址所有的函数参数以及局部变量。

栈帧的创建过程

  1. 按照从右至左的顺序,依次把传递的参数压入栈。( Push parameters onto the stack, from right to left. )
  2. 把返回地址压入栈。( Push the return address. )
  3. 跳转到被调用函数的地址。( Jump to the function’s address. )
  4. 把存放在 EBP 寄存器的 Frame Pointer 地址压入栈。( Push the old frame pointer onto the stack. )
  5. 更新 EBP 寄存器为当前栈顶的地址。( Set %ebp to where the end of the stack is right now. )
  6. 把局部变量压入栈。( Push local variables onto the stack. )
  7. 把保存寄存器里的值压入栈。( Save CPU registers used for temporaries. )

栈帧的寻址

在当前栈帧中,所有数据都是根据与 EBP 寄存器里 Frame Pointer 的偏移量来寻址的。如下图所示:

栈帧的销毁过程

  1. 恢复保存寄存器的旧值。( Restore saved registers. )
  2. 恢复 Frame Pointer 的旧值。( Restore the old frame pointer. )
  3. 返回调用函数( Caller )。( Return from the function. )

参考资料

授权声明

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值