初识C语言(函数栈帧的创建和销毁)

一、导入

1.我们为什么要学习函数的栈帧

因为函数,函数在C语言中是代码块的作用,是相互独立的(拥有独立的功能),可以说C语言的程序就是由一个个的函数组成的。
只从表面看函数的形式,很容易理解,但是其深层的逻辑,如如何传参,如何返回值,如何创建,如何销毁,这些问题的解决只有在了解函数的栈帧之后,才能理解并回答。

2.函数的栈帧是什么

定义:在调用函数时,在调用栈上为这个函数所创建的空间

里面能放什么:
1、在该函数里面创建的临时变量(非静态的局部变量以及编译器自动生产的其他临时变量)
2、函数参数和函数返回值
3、保存上下文信息(比如寄存器,寄存器是内存的一部分,是不会因为程序的结束就销毁)

特点:
1、从高地址到低地址
我们可以把栈帧理解为一个柜子,创建函数就是在那个柜子里面再放上一个小空间,然后标榜“这个就是我的领地了”,而在函数中创建临时变量或者执行其他操作,就像生活中在柜子的最底部开始放东西,这对应在C语言就是从高地址到低地址。

2、是动态变化的:
不是说我们放了这个小空间,它就定死了是这么大,这个小空间是会随着里面所需要内存的增多而变大,即是动态变化的。(压栈增大,弹出栈减小)

3、先入栈的数据后出栈:
容器理解法,参照第一条

二、实例

代码

#include <stdio.h>
//计算3和5的和
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 3;
	int b = 5;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

反汇编

反汇编可以将我们在写代码时,那些看不见的操作,细致地反映出来

int main()
{
00A018B0  push        ebp  
00A018B1  mov         ebp,esp  
00A018B3  sub         esp,0E4h  
00A018B9  push        ebx  
00A018BA  push        esi  
00A018BB  push        edi  
00A018BC  lea         edi,[ebp-24h]  
00A018BF  mov         ecx,9  
00A018C4  mov         eax,0CCCCCCCCh  
00A018C9  rep stos    dword ptr es:[edi]  
00A018CB  mov         ecx,0A0C003h  
00A018D0  call        00A0131B  
	int a = 3;
00A018D5  mov         dword ptr [ebp-8],3  
	int b = 5;
00A018DC  mov         dword ptr [ebp-14h],5  
	int ret = 0;
00A018E3  mov         dword ptr [ebp-20h],0  
	ret = Add(a, b);
00A018EA  mov         eax,dword ptr [ebp-14h]  
00A018ED  push        eax  
00A018EE  mov         ecx,dword ptr [ebp-8]  
00A018F1  push        ecx  
00A018F2  call        00A010B4  
00A018F7  add         esp,8  
00A018FA  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", ret);
00A018FD  mov         eax,dword ptr [ebp-20h]  
00A01900  push        eax  
00A01901  push        0A07B30h  
00A01906  call        00A010D2  
00A0190B  add         esp,8  
	return 0;
00A0190E  xor         eax,eax  
}
00A01910  pop         edi  
00A01911  pop         esi  

int Add(int x, int y)
{
001F1770  push        ebp  
001F1771  mov         ebp,esp  
001F1773  sub         esp,0CCh  
001F1779  push        ebx  
001F177A  push        esi  
001F177B  push        edi  
001F177C  lea         edi,[ebp-0Ch]  
001F177F  mov         ecx,3  
001F1784  mov         eax,0CCCCCCCCh  
001F1789  rep stos    dword ptr es:[edi]  
001F178B  mov         ecx,1FC003h  
001F1790  call        001F131B  
	int z = 0;
001F1795  mov         dword ptr [ebp-8],0  
	z = x + y;
001F179C  mov         eax,dword ptr [ebp+8]  
001F179F  add         eax,dword ptr [ebp+0Ch]  
001F17A2  mov         dword ptr [ebp-8],eax  
	return z;
001F17A5  mov         eax,dword ptr [ebp-8]  
}
007217A8  pop         edi  
007217A9  pop         esi  
007217AA  pop         ebx  
007217B8  mov         esp,ebp  
007217BA  pop         ebp  
007217BB  ret  

三、具体分析

1.main函数

1.1 反汇编代码

int main()
{
00A018B0  push        ebp  
00A018B1  mov         ebp,esp  
00A018B3  sub         esp,0E4h  
00A018B9  push        ebx  
00A018BA  push        esi  
00A018BB  push        edi  
00A018BC  lea         edi,[ebp-24h]  
00A018BF  mov         ecx,9  
00A018C4  mov         eax,0CCCCCCCCh  
00A018C9  rep stos    dword ptr es:[edi]  
00A018CB  mov         ecx,0A0C003h  
00A018D0  call        00A0131B  
	int a = 3;
00A018D5  mov         dword ptr [ebp-8],3  
	int b = 5;
00A018DC  mov         dword ptr [ebp-14h],5  
	int ret = 0;
00A018E3  mov         dword ptr [ebp-20h],0  
	ret = Add(a, b);
00A018EA  mov         eax,dword ptr [ebp-14h]  
00A018ED  push        eax  
00A018EE  mov         ecx,dword ptr [ebp-8]  
00A018F1  push        ecx  
00A018F2  call        00A010B4  
00A018F7  add         esp,8  
00A018FA  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", ret);
00A018FD  mov         eax,dword ptr [ebp-20h]  
00A01900  push        eax  
00A01901  push        0A07B30h  
00A01906  call        00A010D2  
00A0190B  add         esp,8  
	return 0;
00A0190E  xor         eax,eax  
}
00A01910  pop         edi  
00A01911  pop         esi  

汇编指令介绍:
push:压栈,即在底部下面的那个内存空间上,再放上一个空间(注意是从高地址到低地址)esp这个栈顶的寄存器也需要随之改变
eg.push    ebp    压上去

mov:数值的赋值
eg. mov    ebp,esp     把esp的值赋值给ebp

sub:减
eg.sub    esp,0E4h    esp减去0E4h的值后,esp会产生一个新的值

lea:加载有效地址
eg.lea    edi,[ebp-24h]    把[ebp-24h]的这个地址给edi,那实际上出现了一个新的edi

call:压入返回地址+转入目标函数
在函数返回main函数的时候,就可以直接跳到call的下一句指令

寄存器介绍
eax:用来存放临时的值,常用于函数的返回值

ebx:用来存放临时的值

ebp:用来存放栈底的地址------高地址

esp:用来存放栈顶的地址------低地址 esp和ebp是用来维护函数的,当前在调用哪一个函数,就去维护哪个函数

1.2细分解析

1.2.1先给main函数搞一个空间
00A018B0  push        ebp  
00A018B1  mov         ebp,esp  
00A018B3  sub         esp,0E4h  

如何搞空间
给ebp和esp分别赋值上不同的值,中间划出的地址就是main函数的栈帧空间

1.2.2把寄存器的后路准备好
00A018B9  push        ebx  
00A018BA  push        esi  
00A018BB  push        edi  

需要压栈ebx、esi、edi,保存了三个寄存器的值
作用:保存了三个寄存器的值,因为它们在后续的函数使用中,值可能会改变,先存一下,便于退出函数时恢复

1.2.3给这个空间初始化
00A018BC  lea         edi,[ebp-24h]  
00A018BF  mov         ecx,9  
00A018C4  mov         eax,0CCCCCCCCh  
00A018C9  rep stos    dword ptr es:[edi]  
00A018CB  mov         ecx,0A0C003h  
00A018D0  call        00A0131B  

相当于

edi = ebp - 24h;
ecx = 9;
eax = 0xCCCCCCCC;
for (; ecx = 0; --ecx, edi += 4)
{
	*(int*)edi = eax;
}

把ebp-24h的地址,放到edi中
把9赋值给ecx
把0xCCCCCCCC赋值给eax
把从edi到ebp的这一段都赋值给CCCCCCCC

这里已经在改寄存器里面的值了,所以之前给寄存器留一条后路,是一个必要的操作

扩展:为什么随机值我们打出来是“烫”
因为在栈所开辟的空间,每一个字节都被初始化成CCCCCCCC,而其所对应的汉字编码,就是烫

1.2.4 创建临时变量和赋值

这个的实现是在函数的栈帧空间中实现的

int a = 3;
00A018D5  mov         dword ptr [ebp-8],3  
	int b = 5;
00A018DC  mov         dword ptr [ebp-14h],5  
	int ret = 0;
00A018E3  mov         dword ptr [ebp-20h],0  

把3赋值给[ebp-8]这个地址,这个地址其实就是a变量
把5赋值给[ebp-14h]这个地址,这个地址其实就是b变量
把0赋值给[ebp-20h]这个地址,这个地址其实就是ret变量
而原本在这个地方的CCCCCCCC会被覆盖

1.2.5 传参给Add函数
	ret = Add(a, b);
00A018EA  mov         eax,dword ptr [ebp-14h]  
00A018ED  push        eax  
00A018EE  mov         ecx,dword ptr [ebp-8]  
00A018F1  push        ecx    
00A018F2  call        00A010B4  

因为传的是a,b这两个变量,就把这两个变量的地址,分别传给eax、ecx这两个寄存器,并且压栈,然后call是保存下一条指令,并且跳转函数(压入返回地址+转入目标函数)

2.Add函数的内部操作

2.1 反汇编代码

int Add(int x, int y)
{
001F1770  push        ebp  
001F1771  mov         ebp,esp  
001F1773  sub         esp,0CCh  
001F1779  push        ebx  
001F177A  push        esi  
001F177B  push        edi  
001F177C  lea         edi,[ebp-0Ch]  
001F177F  mov         ecx,3  
001F1784  mov         eax,0CCCCCCCCh  
001F1789  rep stos    dword ptr es:[edi]  
001F178B  mov         ecx,1FC003h  
001F1790  call        001F131B  
	int z = 0;
001F1795  mov         dword ptr [ebp-8],0  
	z = x + y;
001F179C  mov         eax,dword ptr [ebp+8]  
001F179F  add         eax,dword ptr [ebp+0Ch]  
001F17A2  mov         dword ptr [ebp-8],eax  
	return z;
001F17A5  mov         eax,dword ptr [ebp-8]  
}

2.2 细分解析

2.2.1创建空间
001F1770  push        ebp  
001F1771  mov         ebp,esp  
001F1773  sub         esp,0CCh  
001F1779  push        ebx  
001F177A  push        esi  
001F177B  push        edi  
001F177C  lea         edi,[ebp-0Ch]  
001F177F  mov         ecx,3  
001F1784  mov         eax,0CCCCCCCCh  
001F1789  rep stos    dword ptr es:[edi]  
001F178B  mov         ecx,1FC003h  
001F1790  call        001F131B  

创建这边和之前介绍的main函数的创建,是同理的

2.2.2 函数里面的求和
int z = 0;
001F1795  mov         dword ptr [ebp-8],0  
	z = x + y;
001F179C  mov         eax,dword ptr [ebp+8]  
001F179F  add         eax,dword ptr [ebp+0Ch]  
001F17A2  mov         dword ptr [ebp-8],eax  

z是在Add函数的栈帧创建的

把[ebp+8]地址上的值放到eax中
把[ebp+0Ch] 地址上的值加到eax中
通过edp的地址进行偏移访问到了函数调用前压栈进去 的参数,并没有单独创建什么变量

把eax的值放到[ebp-8]处,其实就是给z

2.2.3 函数的值返回
return z;
001F17A5  mov         eax,dword ptr [ebp-8]  

把z的值给寄存器eax,通过寄存器带回计算的结果

2.2.4 函数栈帧的销毁
007217A8  pop         edi  
007217A9  pop         esi  
007217AA  pop         ebx  
007217B8  mov         esp,ebp  
007217BA  pop         ebp  
007217BB  ret  

汇编指令介绍
pop:弹出一个值至指定位置,同时esp也要发生改变
eg.pop    edi    把栈顶最上面的那个值弹出来,把值存放edi,同时esp也要发生变化,esp+4

ret:恢复返回位置,压入eip,相当于pop eip

压出栈顶上的三个:ebx,esi,edi
把ebp的值给esp,这使得esp和ebp的内存空间还给操作系统
弹出栈顶上的一个值给ebp,这个值,刚好是之前存的main函数的ebp,此时恢复了main函数的栈帧维护,esp指向main函数的栈顶,ebp指向函数的栈底
ret相当于pop eip,此时弹出的一个是call指令下一条指令的地址处,即返回到main函数里面

3.Add返回到main函数之后

3.1反汇编代码

printf的那部分,是库函数,不作了解

00A018F7  add         esp,8  
00A018FA  mov         dword ptr [ebp-20h],eax  

esp+8实现了跳过传参的那两个空间
把eax的值给[ebp-20h],也就是ret

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值