离久的小站

关于函数调用栈

2019/04/24 Share

函数调用栈原理

指令指针

  • 指令指针IP:指令寄存器存储,指向处理器下条等待执行的指令地址(代码内的偏移量),每次执行完 IP会增加
  • 堆栈栈顶指针SP:堆栈指令寄存器存储,系统栈的栈顶地址
  • 栈帧指针FP:栈帧基址指令寄存器存储,每个栈帧都有一个对应的栈帧基地址,局部变量和函数参数都可以通过FP确定,因为它们到FP的距离不会受到压栈和出栈操作影响。

为了访问函数局部变量,必须能定位每个变量。
局部变量相对于堆栈指针SP的位置在进入函数时就已确定,理论上变量可用SP加偏移量来引用,但SP会在函数执行期随变量的压栈和出栈而变动。
尽管某些情况下编译器能跟踪栈中的变量操作以修正偏移量,但要引入可观的管理开销。
而且在有些机器上(如Intel处理器),用SP加偏移量来访问一个变量需要多条指令才能实现,由此设计了栈帧指针FP,FP两侧分别记录函数参数,及局部变量。

函数调用栈内部布局

栈帧:函数(运行中且未完成)占用的一块独立的连续内存区域。
函数调用通常是嵌套的,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。
栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。

编译器利用栈帧,使得函数参数和函数中局部变量的分配与释放对程序员透明。
编译器将控制权移交函数本身之前,插入特定代码将函数参数压入栈帧中,并分配足够的内存空间用于存放函数中的局部变量。
使用栈帧的一个好处是使得递归变为可能,因为对函数的每次递归调用,都会分配给该函数一个新的栈帧,这样就巧妙地隔离当前调用与上次调用。

栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。
EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;
ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。
因此函数中对大部分数据的访问都基于EBP进行。

函数出入栈过程

  • BP栈帧指针地址:间隔被调用函数(局部变量内存空间)和调用函数(被调函数参数,调用函数地址,指令指针)
  • BP栈帧指针值:上一个栈帧的地址值,便于被调函数释放后,回到调用函数
  • BP栈帧入栈时机:函数被调用,申请内存空间来存储前一个栈帧的地址值

从图中可以看出,函数调用时入栈顺序为:
实参N-1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1-N 。
注意:内存地址降序

函数定义

  • caller(主调函数,紫色)
  • callee(被调函数,蓝色)

入栈过程

  1. caller未调用callee,内存分布如下:
    EBP:caller EBP
    ESP:caller的LocalVariables

  2. caller调用callee
    callee函数的参数入栈(由caller提供)
    caller的函数地址(vm_add), EIP入栈(代码偏移量offset)。
    备注:代码位置 = vm_add + offset,即真实地址 = 虚拟地址 + 偏移量

  3. callee栈帧指针入栈
    申请栈帧指针空间
    存储caller的栈帧指针地址

  4. 申请callee局部变量空间
    为局部变量申请足够的内存空间
    Local Variable#1,Local Variable#2,Local Variable#3…Local Variable#n
    EBP:callee的EBP
    ESP:Local Variable#n

出栈过程

  1. callee调用完毕
    callee局部变量空间释放
    EBP:callee ebp -> caller ebb
    ESP:caller ebp

  2. caller函数执行复原

  3. 代码执行复原:ip+return address = 代码位置
    callee函数空间释放:Argumne #1,Argumne #2,…,Argumne #1n
    EBP:caller ebb
    ESP:caller Load Variables

CATALOG
  1. 1. 函数调用栈原理
    1. 1.1. 函数调用栈内部布局
  2. 2. 函数出入栈过程
    1. 2.1. 函数定义
    2. 2.2. 入栈过程
    3. 2.3. 出栈过程