栈帧浅析
什么是栈帧
引用百度百科中的解释:
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
函数的每次调用,都有它自己独立的栈帧。栈帧中维持着函数调用所需要的各种信息,包括函数的入参、函数的局部变量、函数执行完成后下一步要执行的指令地址、寄存器信息等。
运行时栈
栈帧使用了栈这一数据结构,达到了后进先出(First In Last Out )的内存管理原则。不管是插入数据还是删除数据,都是在栈顶进行的。
x86-64 的栈由高地址向低地址增长,寄存器rbp 指向当前栈帧的底部(高地址),寄存器rsp 指向当前栈帧的顶部(低地址)。数据压栈和出栈会修改rsp 的值。通过push 指令将数据存入栈中,同时64位系统中会对栈顶指针做减法操作rsp=rsp-8 。pop 指令是push 的逆操作,它将数据从栈中读取出来,同时64位系统中会对栈顶指针做加法操作rsp=rsp+8
当过程P调用过程Q时,
-
把实参压栈,cdecl 是gcc 的默认调用约定,实参压栈顺序为从右至左 -
把返回地址(即P调用Q后的下一条指令地址)压入栈中,表示当Q返回后,P程序下一步要从哪条指令开始运行 -
开始调用Q,首先将P的栈底rbp 压栈,然后栈顶rsp 赋值给rbp ,从而形成新的栈底地址。我们在看函数调用的汇编代码时经常看到的一段正是在做这个操作 push rbp
mov rbp, rsp
-
分配局部变量空间,开始具体执行Q函数的指令代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4UYEpKos-1638200341831)(C:\Users\86188\Desktop\1877984-20191225221512357-1441441103.png)]
转移控制
将控制从函数P转移到函数Q只需要将程序计数器(PC)设置为Q函数代码的起始位置。另外,稍后从Q返回时处理器还需要继续执行P的下一条指令A。在x86-64 中,这个过程是用执行call Q 指令来完成的。该指令将A的地址压栈,并将PC设置为Q的起始地址。
Q执行完成后弹出A地址这个过程是通过ret 指令完成的,它会弹出地址A,并把PC设置为A
以一段函数调用为例
#include<iostream>
#include<stdint.h>
int32_t Add(int32_t x,int32_t y)
{
int32_t z=0;
z=x+y;
return z;
}
int main()
{
int32_t a=10;
int32_t b=20;
int32_t ret=Add(a,b);
return 0;
}
函数很简单,我们在Compiler Explorer (godbolt.org)中编译,编译器版本为x86-64 GCC 11.2 优化等级为O0 ,查看这段程序的汇编代码:
#include<iostream>
#include<stdint.h>
int32_t Add(int32_t x,int32_t y) // push rbp
// mov rbp, rsp
// mov DWORD PTR [rbp-20], edi
// mov DWORD PTR [rbp-24], esi
{
int32_t z=0; // mov DWORD PTR [rbp-4], 0
z=x+y; // mov edx, DWORD PTR [rbp-20]
// mov eax, DWORD PTR [rbp-24]
// add eax, edx
// mov DWORD PTR [rbp-4], eax
return z; // mov eax, DWORD PTR [rbp-4]
} // pop rbp
// ret
int main()
{
// push rbp
// mov rbp, rsp
// sub rsp, 16
int32_t a=10; // mov DWORD PTR [rbp-4], 10
int32_t b=20; // mov DWORD PTR [rbp-8], 20
int32_t ret=Add(a,b); // mov edx, DWORD PTR [rbp-8]
// mov eax, DWORD PTR [rbp-4]
// mov esi, edx
// mov edi, eax
// call Add(int, int)
// mov DWORD PTR [rbp-12], eax
return 0;
}
分析上述汇编代码,当main 函数调用Add 时,执行了以下步骤(注意执行顺序):
Add(int, int):
// 5. 将main函数的栈帧底部地址入栈保存
push rbp
// 6. 将此时的栈帧顶部地址作为Add函数的栈帧底部地址
mov rbp, rsp
// 7. 获取形参x
mov DWORD PTR [rbp-20], edi
// 8. 获取形参y
mov DWORD PTR [rbp-24], esi
// 9. 初始化z=0
mov DWORD PTR [rbp-4], 0
// 10. 将x和y分别保存至寄存器edx和eax,然后相加,结果保存在寄存器eax
mov edx, DWORD PTR [rbp-20]
mov eax, DWORD PTR [rbp-24]
add eax, edx
// 11. 将寄存器eax中的x和y相加结果赋值给z
mov DWORD PTR [rbp-4], eax
// 12. 将Add函数结果z保存至寄存器eax
mov eax, DWORD PTR [rbp-4]
// 13. 将之前保存的main函数栈帧底部地址出栈并保存至rbp
pop rbp
// 14. 相当于pop eip 将之前保存的mov DWORD PTR [rbp-12], eax指令地址出栈并保存至eip
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
// 1. 初始化a=10
mov DWORD PTR [rbp-4], 10
// 2. 初始化b=20
mov DWORD PTR [rbp-8], 20
// 3. 分别将a和b保存至寄存器eax和edx,再分别拷贝至寄存器edi和dsi
mov edx, DWORD PTR [rbp-8]
mov eax, DWORD PTR [rbp-4]
mov esi, edx
mov edi, eax
// 4. 等价于两条命令
// ① push eip 将eip中存储的下一条指令地址压栈,即mov DWORD PTR [rbp-12], eax这条指令
// ② jmp Add(int,int) 跳转至Add(int,int)
call Add(int, int)
// 15. 将Add函数返回结果保存至ret
mov DWORD PTR [rbp-12], eax
|