函数栈帧的创建和销毁

函数栈帧的创建和销毁

1.什么是函数栈帧?

C语言程序是以函数为基本单位的,会把一个独立的功能抽象出来,编写为单独的函数。

既然是函数,就需要考虑到如何调用?返回值又如何对待?参数如何传递?等等一系列的问题都和函数栈帧有关。

函数栈帧(stack frame)就是函数在调用过程中在程序的调用栈
(call stack)所开辟的空间,这些空间是用来存放:

 1. 函数参数和函数返回值
 2. 临时变量(保存函数的非静态的局部变量以及编译器自动生产的其他
 临时变量
 3.保存上下文信息(包括在函数调用前后需要保存不变的寄存器)

2.函数栈帧能解决什么问题?

理解函数栈帧的创建和销毁之后,以下问题就能够很好地理解

1.局部变量是如何创建的?
2.为什么局部变量不初始化内容是随机的?
3.函数调用时参数是如何传递的?传参的顺序是怎么样的?
4.函数的形参和实参分别是如何实例化(创建变量)的?
5.函数的返回值是如何返回到 main函数中的?

3.解析函数栈帧的创建和销毁

3.1栈的概念

在经典的计算机科学中,栈(stack)被定义为一种特殊的容器,用户可以将数据压入栈中(入栈, push),也可以将已经压入栈中的数据弹出(出栈, pop),但是必须遵守一条规则:先入栈的数据后出栈

在计算机系统中,栈是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出,压栈操作使得栈增大,弹出操作使得栈减小。

在经典操作系统中,栈总是向下增长(由低地址向高地址)的。

在常见的x86或者x64环境下,栈顶由 esp寄存器进行定位。

3.2掌握相关寄存器和汇编指令

相关寄存器

eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址

汇编指令

mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生变化
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生变化
sub:减法命令
add:加法命令
call:函数调用   1.压入返回地址;2.转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
3.3解析函数栈帧的创建和销毁
3.3.1前言

首先需要一些预备知识才能帮助我们理解函数栈帧的创建和销毁

1.每一次的函数调用,都要为本次函数调用开辟空间,就是开辟函数栈帧的空间
2.这块空间的维护是使用了2个寄存器: espebpebp记录的是栈底的地址, esp记录的是栈顶的地址。

在这里插入图片描述

3.函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异。

3.3.2函数的调用堆栈
#include<stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 3;
	int b = 2;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

在vs2022编译器下,这段代码,调试进入Add函数后,就可以观察函数的调用堆栈,如下图

在这里插入图片描述

函数调用堆栈是反应函数调用逻辑的,可以清楚地观察到,先是调用mian函数,然后通过mian函数来调用Add函数。

接下来就从main函数的栈帧创建开始分析

3.3.3转到反汇编

调试到main函数开始执行的第一行,右击鼠标转到反汇编

int main()
{
//函数栈帧的创建
009118B0  push        ebp  
009118B1  mov         ebp,esp  
009118B3  sub         esp,0E4h  
009118B9  push        ebx  
009118BA  push        esi  
009118BB  push        edi  
009118BC  lea         edi,[ebp-24h]  
009118BF  mov         ecx,9  
009118C4  mov         eax,0CCCCCCCCh  
009118C9  rep stos    dword ptr es:[edi]  
009118CB  mov         ecx,offset _352C9E5E_test@c (091C008h)  
009118D0  call        @__CheckForDebuggerJustMyCode@4 (091131Bh)  
//main函数中的核心代码
	int a = 3;
00EE18D5  mov         dword ptr [ebp-8],3  
	int b = 2;
00EE18DC  mov         dword ptr [ebp-14h],2  
	int ret = 0;
00EE18E3  mov         dword ptr [ebp-20h],0  
	ret = Add(a, b);
00EE18EA  mov         eax,dword ptr [ebp-14h]  
00EE18ED  push        eax  
00EE18EE  mov         ecx,dword ptr [ebp-8]  
00EE18F1  push        ecx  
00EE18F2  call        00EE13B6  
00EE18F7  add         esp,8  
00EE18FA  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", ret);
009118FD  mov         eax,dword ptr [ebp-20h]  
00911900  push        eax  
00911901  push        offset string "%d\n" (0917B30h)  
00911906  call        _printf (09110D2h)  
0091190B  add         esp,8  
	return 0;
0091190E  xor         eax,eax  
}
00911910  pop         edi  
00911911  pop         esi  
00911912  pop         ebx  
00911913  add         esp,0E4h  
00911919  cmp         ebp,esp  
0091191B  call        __RTC_CheckEsp (0911244h)  
00911920  mov         esp,ebp  
00911922  pop         ebp  
00911923  ret  
3.3.4函数栈帧的创建

通过上面的汇编代码,接下来一行一行地拆解汇编代码

009118B0  push        ebp  
//把ebp寄存器中的值进行压栈
009118B1  mov         ebp,esp  
//move指令会把esp的值存放到ebp中,此时就产生了mian函数的ebp
009118B3  sub         esp,0E4h  
//sub的作用是使esp中的地址减去一个十六进制的数字0xE4,产生新的esp
//此时esp是main函数栈帧的esp,与上面的ebp之间维护了一块栈空间,
//这块空间就是为main函数开辟的栈空间,这块空间将存储main函数中的
//局部变量,以及调试信息等。
009118B9  push        ebx  
//将寄存器ebx的值压栈,esp-4
009118BA  push        esi  
//将寄存器esi的值压栈,esp-4
009118BB  push        edi  
//将寄存器edi的值压栈,esp-4

//上面三条指令将三个寄存器的值保存在栈区,因为这三个寄存器在函数
//随后的执行过程中可能被修改,如此便避免其中的值被修改,也方便退
//出函数时恢复其中的值


//以下操作便是在初始化main函数的栈帧空间
//1.先把 ebp-0x24h 的地址,放到edi中
//2.把 9 放入ecx
//3.把0CCCCCCCC放在 eax 中
//4.将从 ebp-0x24h 到 ebp 这一段内存的每个字节都初始化为0CCCCCCCC
009118BC  lea         edi,[ebp-24h]  
009118BF  mov         ecx,9  
009118C4  mov         eax,0CCCCCCCCh  
009118C9  rep stos    dword ptr es:[edi]  

画图展示如下
在这里插入图片描述
平常在写代码没有初始化变量直接打印会出现一连串的烫烫烫,其实打印的就是0xCCCCCCCC

例如

在这里插入图片描述

接着再分析main函数的核心代码

    int a = 3;
00EE18D5  mov         dword ptr [ebp-8],3  
//将3存储到 ebp-8 的地址处,也就是变量a的地址
	int b = 2;
00EE18DC  mov         dword ptr [ebp-14h],2  
//将2存储到 ebp-14h 的地址处,也就是变量b的地址
	int ret = 0;
00EE18E3  mov         dword ptr [ebp-20h],0  
//将0存储到 ebp-20h 的地址处,就是变量 ret 的地址

//以上汇编代码的本质就是变量 a,b,ret 的创建和初始化,就是局部变量
//的创建和初始化
//局部变量是在局部变量所在的函数的栈帧空间中所创建的

画图展示如下
在这里插入图片描述

Add函数的传参

//调用Add函数
	ret = Add(a, b);
//调用Add函数时的传参,是把参数压栈到栈帧空间
00EE18EA  mov         eax,dword ptr [ebp-14h]  寄存器中
//传递b,将 ebp-14h 地址处的2存放在 eax 
00EE18ED  push        eax  
//将 eax 的值进行压栈,esp-4
00EE18EE  mov         ecx,dword ptr [ebp-8]  
//传递a,将 ebp-8 地址处的3存放在 ecx 寄存器中
00EE18F1  push        ecx  
//将 ecx 的值进行压栈,esp-4

//跳转调用函数
00EE18F2  call        00EE13B6  
00EE18F7  add         esp,8  
00EE18FA  mov         dword ptr [ebp-20h],eax  

在这里插入图片描述

00EE18F2  call        00EE13B6  
00EE18F7  add         esp,8  
00EE18FA  mov         dword ptr [ebp-20h],eax  

call指令是要执行函数调用逻辑的,在执行 call指令之前会先把 call指令下一条指令的地址进行压栈操作,此操作是为了解决当函数调用结束后要回到 call指令的下一条指令的地址处,继续执行程序。

在这里插入图片描述

当跳转到Add函数时,就要开始观察Add函数的反汇编代码

int Add(int x, int y)
{
00EE1830  push        ebp  
//保存 main函数 栈帧的 ebp ,esp-4
00EE1831  mov         ebp,esp  
//将main函数的 esp 赋值给新的 ebp,ebp就变成 Add 函数的 ebp
00EE1833  sub         esp,0CCh  
//sub的作用是使esp中的地址减去一个十六进制的数字0CCh,产生新的esp
//此时esp是Add函数栈帧的esp,与上面的ebp之间维护了一块栈空间,
//这块空间就是为Add函数开辟的栈空间,这块空间将存储Add函数中的
//局部变量,以及调试信息等
00EE1839  push        ebx  
//将 ebx 的值进行压栈,esp-4
00EE183A  push        esi  
//将 esi 的值进行压栈,esp-4
00EE183B  push        edi  
//将 edi 的值进行压栈,esp-4
00EE183C  lea         edi,[ebp-0Ch]  
00EE183F  mov         ecx,3  
00EE1844  mov         eax,0CCCCCCCCh  
00EE1849  rep stos    dword ptr es:[edi]  
00EE184B  mov         ecx,0EEC008h  
00EE1850  call        00EE131B  
	int z = 0;
00EE1855  mov         dword ptr [ebp-8],0  
//将0存放在 ebp-8 的地址处,其实就是 z 的地址处
	z = x + y;
00EE185C  mov         eax,dword ptr [ebp+8]  
//将 ebp+8 的地址处的数字存储到 eax 中
00EE185F  add         eax,dword ptr [ebp+0Ch]  
//将 ebp+0xC地址处的数字加到 eax 中
00EE1862  mov         dword ptr [ebp-8],eax  
//将 eax 的结果保存到 ebp-8 的地址处,也就是 z 的地址处
	return z;
00EE1865  mov         eax,dword ptr [ebp-8]  
//将 ebp-8 地址处的值存放在 eax 中,本质上就是将 z 的值存储到
//eax 中,通过 eax 带回计算的结果,作为函数的返回值
}
00EE1868  pop         edi  
00EE1869  pop         esi  
00EE186A  pop         ebx  
00EE186B  add         esp,0CCh  
00EE1871  cmp         ebp,esp  
00EE1873  call        00EE1244  
00EE1878  mov         esp,ebp  
00EE187A  pop         ebp  
00EE187B  ret  

代码执行到Add函数时,就需要开始创建Add函数的栈帧空间
Add函数中创建栈帧的方法与在main函数相似。

创建Add函数栈帧的整体思路

 1. 将mian函数的 ebp 压栈
 2. 计算新的 ebp 和 esp
 3. 将 ebx,esi,edi 寄存器的值保存
 4. 计算求和,在计算的过程中,通过 ebp 的地址访问函数调用前压栈
 进去的参数,也就是形参访问
 5.将求出的和放在 eax 寄存器中带回

在这里插入图片描述
在这里插入图片描述
上图中的a'b'其实是Add函数的形参x,y。图中就很好地说明函数在传参过程中,以及函数在进行传值调用时,形参就是实参的一份临时拷贝,对形参的修改不会影响实参。

3.3.5函数栈帧的销毁

当函数调用即将结束时,前面创建的函数栈帧也即将开始销毁
接下来,通过反汇编代码来具体了解是怎么销毁的

00EE1868  pop         edi  
//在栈顶弹出一个值,存放到 edi 中,esp+4
00EE1869  pop         esi  
//在栈顶弹出一个值,存放到 esi 中,esp+4
00EE186A  pop         ebx  
//在栈顶弹出一个值,存放到 ebx 中,esp+4
00EE186B  add         esp,0CCh  
00EE1871  cmp         ebp,esp  
//再次将Add函数的 ebp 的值赋值给 esp ,相当于回收了Add函数的
栈帧空间
00EE1873  call        00EE1244  
00EE1878  mov         esp,ebp  
00EE187A  pop         ebp  
//弹出栈顶的值存放到 ebp,栈顶此时的值恰好是main函数的 ebp,esp+4
//此时恢复了main函数的栈帧维护,esp指向mian函数栈帧的栈顶,
ebp指向了main函数栈帧的栈底
00EE187B  ret  
//ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令
下一条指令的地址,此时 esp+4 ,接着就直接跳转到call指令下一条指令
的地址处,举行执行程序。

回到call指令的下一条指令的地址处

00EE18F7  add         esp,8  
00EE18FA  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", ret);
00EE18FD  mov         eax,dword ptr [ebp-20h]  
00EE1900  push        eax  
00EE1901  push        0EE7B30h  
00EE1906  call        00EE10D2  
00EE190B  add         esp,8  

调用完Add函数,回到函数时,继续执行后面的代码。

00EE18F7  add         esp,8  
//esp加上8,等价于跳过main函数中压栈的 a'和b' 
00EE18FA  mov         dword ptr [ebp-20h],eax  
//将 eax 中的值,存放到 ebp-0x20h的地址处,也就是存储到main函数
//中的变量 ret 中,eax 中的值就是Add函数中计算的 x 和 y 的和,显而
//易见,本次函数的返回值是由 eax 寄存器带回来的,程序是在函数调用
//返回之后,在 eax 中读取返回值的。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值