C语言(5.2 函数栈帧)
目录
一、函数栈帧
1. 函数栈帧概念
函数栈帧(stack frame):函数调用过程中在程序的调用栈所开辟的空间
- 函数栈帧用来存放:
- 函数参数和返回值
- 临时变量(包括函数的非静态局部变量,和编译器自动产生的其他临时变量)
- 保存上下文信息(包括函数调用前后需要保持不变的寄存器)
2. 栈stack
栈(stack):是一种特殊的容器,用户可以将数据压入栈中,也可以将已经压入栈中的数据弹出。栈的特点是先进后出,后进先出(详情请阅读数据结构,这里不做赘述)
计算机中的栈空间:由栈底到栈顶存储与计算机的高地址到低地址,因为在实际开发中计算机不能确定程序对堆内存使用多还是对栈内存使用多,所以不能将二者以一条分界线隔开从低地址向高地址使用,容易导致空间利用率降低。将栈从高地址向低地址增长增加了空间的利用率,更方便管理。
- 栈空间使用栈底寄存器和栈顶寄存器记录栈底指针和栈顶指针来维护栈空间
- 局部变量存储在栈中,函数调用结束,,栈空间被销毁,局部变量被释放
3. 相关寄存器和汇编指令
相关寄存器:
- eax:通用寄存器,保存临时数据,常用于返回值
- ebx:通用寄存器,保存临时数据
- ebp:栈底寄存器,用来存放栈底指针(Extended Base Pointer)
- esp:栈顶寄存器,用来存放栈顶指针(Extended Stack Pointer)
- eip:指令寄存器,保存当前指令的下一条指令的地址
相关汇编指令:
- mov:数据转移指令
- push:数据入栈,使esp栈顶寄存器也发生改变
- pop:数据弹出至指定位置,同时esp栈顶寄存器也发生改变
- sub:减法指令
- add:加法指令
- call:函数调用,1.压入返回地址,2.转入目标函数
- jump:修改eip,转入目标函数,进行调用
- ret:恢复返回地址,压入eip,类似pop eip指令
二、函数栈帧的创建和销毁
1. 函数调用堆栈过程
1.1 VS2019函数调用堆栈
在VS2019编译器中,main函数不是直接调用的,而是由编译器的函数进行调用
编译器的函数tmainCRTStartup
调用编译器中的函数__tmainCRTStartup
,再调用main
函数,程序正式启动
1.2 程序代码
我们用以下代码进行函数栈帧创建与销毁的的分析,由main
函数调用Add
函数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 3;
int b = 5;
int ret = Add(3, 5);
printf("%d", ret);
return 0;
}
1.3 反汇编分析
我们进入调试后,对程序进行反汇编,对这些汇编语言进行分析
- main函数:
int main()
{
//函数栈帧的创建
//开辟main函数空间
009B1810 push ebp //将当前函数地址(__tmainCRTStartup函数)压入栈中
009B1811 mov ebp,esp //栈底指针=栈顶指针
009B1813 sub esp,0E4h //栈顶指针-0E4h(栈顶指针上移0E4h个字节,开辟了0E4h个空间)
//每个函数开辟空间后会压入3个寄存器内容,作用不用深究
009B1819 push ebx
009B181A push esi
009B181B push edi
//初始化main函数空间
009B181C lea edi,[ebp-24h] //将main函数空间大小给 edi
009B181F mov ecx,9 //为ecx赋值为9,ecx用来存储初始化次数
009B1824 mov eax,0CCCCCCCCh //将0CCCCCCCCh赋值给eax寄存器,是初始化的内容
009B1829 rep stos dword ptr es:[edi] //进行初始化,将eax内容分ecx次赋值给edi大小的内存
//main中的代码
//创建2个变量a、b
int a = 3;
009B182B mov dword ptr [a],3
int b = 5;
009B1832 mov dword ptr [b],5
//调用函数Add
int ret = Add(3, 5);
009B1839 push 5 //将参数5压入栈中
009B183B push 3 //将参数3压入栈中
009B183D call _Add (09B10B4h) //下一条指令为_Add函数的地址(调用Add函数)
//从此处暂停阅读,移步至Add函数的反汇编过程
009B1842 add esp,8 //栈顶指针+8(弹出参数的大小)
009B1845 mov dword ptr [ret],eax //创建ret变量并得到eax寄存器中的值
//打印ret的值
printf("%d", ret);
009B1848 mov eax,dword ptr [ret]
009B184B push eax
009B184C push offset string "%d" (09B7B30h)
009B1851 call _printf (09B10D2h)
009B1856 add esp,8
return 0;
009B1859 xor eax,eax
}
//销毁main函数空间
//弹出3个指针
009B185B pop edi
009B185C pop esi
009B185D pop ebx
//释放main函数空间
009B185E add esp,0E4h //栈顶指针+0E4h(释放main空间)
009B1864 cmp ebp,esp //比较栈顶指针和栈底指针
009B1866 call __RTC_CheckEsp (09B1244h) //执行__RTC_CheckEsp函数
009B186B mov esp,ebp //栈顶指针=栈底指针
009B186D pop ebp //释放栈底指针
009B186E ret //恢复返回地址,继续执行调用之前的指令,程序运行结束
- Add函数:
int Add(int x, int y)
{
//开辟Add函数空间
009B1760 push ebp //将当前函数(main函数)栈底压栈
009B1761 mov ebp,esp //栈顶指针=栈底指针
009B1763 sub esp,0C0h //开辟0C0h个字节的空间
//压入3个寄存器内容
009B1769 push ebx
009B176A push esi
009B176B push edi
//计算x+y结果,并存入eax寄存器中
return x + y;
009B176C mov eax,dword ptr [x]
009B176F add eax,dword ptr [y]
}
//销毁Add函数空间
//弹出3个指针
009B1772 pop edi
009B1773 pop esi
009B1774 pop ebx
//销毁函数空间
009B1775 mov esp,ebp //栈顶指针 = 栈底指针(销毁Add函数空间)
009B1777 pop ebp //弹出栈底指针
009B1778 ret //恢复返回地址,继续执行调用之前的指令
2. 函数调用栈图
2.1 main函数栈空间示意图
2.2 Add函数栈空间示意图
3. 创建销毁过程总结
3.1 调前准备
- 复制参数并压入栈中(形式参数)
- 压入当前指令的下一条指令地址call(为了被调函数执行完时接着执行中断的下一条指令)
- 压入调用函数的栈底指针last-ebp(记录当前函数的栈底指针,被调函数执行完时能回到当前函数继续执行)
3.2 创建空间
- 开辟函数的空间
- 分别压入3个寄存器的内容(ebx、esi、edi)
- 初始化内存空间(只有main函数初始化,VS2019初始化为0xCCCCCCCCh,就是经常越界访问的”烫“)
3.3 执行函数
-
在栈空间内可以创建局部变量
-
可以局部变量和参数,以ebp栈底地址为界限,进行偏移量计算访问局部变量和参数
- 访问局部变量:ebp-n(栈底地址相对于局部变量地址高)
- 访问参数:ebp+n(栈底地址相对于局部变量地址低)
-
return
语句执行时,返回值存储在eax通用寄存器中
3.4 销毁空间
- 弹出3个寄存器的内容(ebx、esi、edi)
- 释放函数空间(
esp = ebp
) - 弹出栈底指针,并赋值给ebp栈底寄存器(上一个函数正式接手)
- 弹出call指令地址,接着执行
- 弹出参数