栈帧:理解函数如何调用

汇编yulabu2026-09-03 34 次阅读

栈帧:理解函数如何调用

要理解栈帧,首先要理解两个比较关键的寄存器

  • ESP(Extended Stack Pointer):32位 x86 的栈指针寄存器
  • EBP(Extended Base Pointer):32位 x86 的基址指针寄存器

在 x86-64 中,对应的是:

  • RSP
  • RBP

什么是栈帧?

可以理解为

栈帧是函数调用过程中,在栈上划分出来的一块用于保存调用相关数据的区域

根据编译器、架构和调用约定不同,其中可能包含:

  • 返回地址
  • 保存的寄存器
  • 局部变量
  • 临时数据
  • 其他调用相关信息

返回地址位于函数调用相关的栈数据中,而传统栈帧通常会围绕它组织其他数据

并不是所有函数都会使用传统的 EBP/RBP 栈帧

ESP(x86)/RSP(x86-64)

栈指针寄存器,通常指向当前栈顶附近的位置。

每次 push 会调整栈指针,并将数据保存到新的栈顶位置;pop 则从栈顶取出数据并相应调整栈指针

ESP/RSP 保存当前栈顶的位置,CPU执行 push、pop 等栈操作时会根据它确定操作位置

在采用传统栈帧结构的函数中,EBP/RBP通常作为栈帧指针,在函数执行期间保持相对稳定,以便通过固定偏移访问局部变量和参数。

注意:这是比较经典的栈帧建立方式,还有省略帧指针(frame pointer omission),可以直接使用 ESP/RSP 加偏移访问局部变量

一个经典函数调用

c 复制代码
#include<stdio.h>
int func2(int a, int b)
{
    return a + b;
}

int func1(int a, int b)
{
    return func2(a, b);
}

假设

复制代码
main()
  ↓
func1()
  ↓
func2()

main调用func1时,cpu执行类似:

asm 复制代码
call func1

执行call时,cpu会:

  • 将返回地址压入栈
  • 跳转到func1

进入func1后,经典的 32 位 x86 函数可能建立栈帧

asm 复制代码
push ebp
mov  ebp, esp
sub  esp, xxx

其中:

asm 复制代码
push ebp

保存调用者的EBP入栈

asm 复制代码
mov ebp,esp

让 EBP 记录当前栈帧建立时的栈指针位置,作为后续访问栈帧数据的基准

asm 复制代码
sub  esp, xxx

调整栈指针,为当前函数需要的局部变量、临时数据等预留栈空间

可以得到如下类似结构

复制代码
高地址
┌──────────────┐
│    参数       │
├──────────────┤
│    返回地址   │
├──────────────┤
│ 保存的 EBP    │
├──────────────┤ ← EBP
│              │
│   局部变量    │
│              │
└──────────────┘ ← ESP
低地址

在 x86/x86-64 中,栈通常向低地址方向增长。

接下来就正常执行这个函数的任务知道要返回的位置,过程保持平栈

平栈,也可以说是栈平衡,是指函数执行过程中对栈指针的调整,在需要返回调用者时,应恢复到符合调用约定要求的状态

人话理解:函数使用栈空间后,在返回调用者之前,需要把栈指针恢复到符合调用约定要求的位置

简单情况下,可以理解为 push/pop 和 sub/add 成对出现

函数返回

经典函数可能通过:

asm 复制代码
mov esp, ebp
pop ebp

函数收尾时,esp指针回到当前函数最开始的ebp,把调用者的基址重新从栈中取出

asm 复制代码
ret

处理最开始call存入的返回地址,实现返回

这就是一个完整的函数调用流程

小结

以下为传统32位x86调用方式下的简化栈帧示意,实际布局取决于目标架构、ABI、调用约定和编译器

复制代码
RSP / ESP
    ↓
当前栈顶位置

RBP / EBP
    ↓
传统栈帧中的基准位置

call
    ↓
保存返回地址 + 跳转

ret
    ↓
取出返回地址 + 跳转

sub rsp/esp
    ↓
分配栈空间

add rsp/esp
    ↓
释放栈空间

虽然但是,参数怎么传入的?

评论

正在加载评论区…