文章目录
一、导入
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