栈帧:理解函数如何调用
栈帧:理解函数如何调用
要理解栈帧,首先要理解两个比较关键的寄存器
- 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
↓
释放栈空间
虽然但是,参数怎么传入的?
评论