在函数的学习过程中,我们简单的了解函数之间是如歌调用并返回是不够的,这时候我们就需要进入内存中来看看对应的栈帧到底是怎么创建并完成整个操作的。
先给一段非常非常简单的代码,以便我们进行分析
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int Add(int x, int y)
{
int sum = 0;
sum = x + y;
return sum;
}
int main()
{
int a = 2;
int b = 3;
int ret = 0;
ret = Add(a, b);
return 0;
}
这段代码十分简单,在main函数中定义并初始化了三个变量,接下来将a,b两个参数传给Add函数,并完成求和操作后将结果传给主函数,接下来我们就进入反汇编来看看到底是怎么完成这一过程的!
要想看懂汇编代码先得掌握一些最基本的指令(先简单介绍下等下对照汇编代码详细解释每一步):
PUSH:压栈;
POP:出栈;
MOV:传送字或字节;
LEA:装入有效地址;
REP:重复;
RET:栈顶字单元出栈,实现一个程序的转移;
还有两个最关键的寄存器:ESP和EBP。
ESP始终指向调用函数栈帧的栈顶;
EBP则是指向栈底。
main函数的汇编代码:
10: int main() //初始化main函数的栈帧空间
11: {
011D16E0 push ebp
011D16E1 mov ebp,esp
011D16E3 sub esp,0E4h
011D16E9 push ebx
011D16EA push esi
011D16EB push edi
011D16EC lea edi,[ebp+FFFFFF1Ch]
011D16F2 mov ecx,39h
011D16F7 mov eax,0CCCCCCCCh
10: int main() // 执行我们的代码进行操作
11: {
011D16FC rep stos dword ptr es:[edi]
12: int a = 2;
011D16FE mov dword ptr [ebp-8],2
13: int b = 3;
011D1705 mov dword ptr [ebp-14h],3
14: int ret = 0;
011D170C mov dword ptr [ebp-20h],0
15: ret = Add(a, b);
011D1713 mov eax,dword ptr [ebp-14h]
011D1716 push eax
011D1717 mov ecx,dword ptr [ebp-8]
011D171A push ecx
011D171B call 011D10F0 //按F11,调到Add函数内部
011D1720 add esp,8
011D1723 mov dword ptr [ebp-20h],eax
16: return 0;
011D1726 xor eax,eax
17: }
Add函数的汇编代码:
4: int Add(int x, int y)
5: { //同样先初始化Add函数栈帧的空间
011D1690 push ebp
011D1691 mov ebp,esp
011D1693 sub esp,0CCh
011D1699 push ebx
011D169A push esi
011D169B push edi
011D169C lea edi,[ebp+FFFFFF34h]
011D16A2 mov ecx,33h
011D16A7 mov eax,0CCCCCCCCh
011D16AC rep stos dword ptr es:[edi]
6: int sum = 0; //开始我们的代码!
011D16AE mov dword ptr [ebp-8],0
7: sum = x + y;
011D16B5 mov eax,dword ptr [ebp+8]
011D16B8 add eax,dword ptr [ebp+0Ch]
011D16BB mov dword ptr [ebp-8],eax
8: return sum; //计算完成将值放入eax中传回
011D16BE mov eax,dword ptr [ebp-8]
9: }
011D16C1 pop edi
011D16C2 pop esi
011D16C3 pop ebx
011D16C4 mov esp,ebp
011D16C6 pop ebp
011D16C7 ret
看汇编不是很形象,所以接下来就拿画好的图来对应代码谈谈(假设上为栈的低地址,下为栈高地址)
一:在整个代码的开始之前:先由ESP和EBP共同维护一块_tmainCRTStarup函数的栈帧,通过它我们才可以调用main函数。
二:调用main函数,初始化这片空间
对应汇编代码如下(对着图看着注释能更快懂):
10: int main()
11: {
011D16E0 push ebp //在第一幅图的基础上先将ebp压入栈中
011D16E1 mov ebp,esp //把第一幅图中ebp挪至esp的位置
011D16E3 sub esp,0E4h //将esp地址减0E4h,相当于将esp向上挪了0E4h个位置
011D16E9 push ebx //将ebx压栈
011D16EA push esi //将esi压栈
011D16EB push edi //将edi压栈
011D16EC lea edi,[ebp+FFFFFF1Ch]
011D16F2 mov ecx,39h
011D16F7 mov eax,0CCCCCCCCh
011D16FC rep stos dword ptr es:[edi] //以上四行我们可以先简单理解将刚才开辟的空间全部初始化为\
// 0CCCCCCCCh
三:进入到我们的逻辑了
12: int a = 2;
011D16FE mov dword ptr [ebp-8],2 //把2放入[ebp-8]地址中 (相当于把a = 2放入栈中)
13: int b = 3
011D1705 mov dword ptr [ebp-14h],3 //把3放入[ebp-14h]地址中(将b = 3放入)
14: int ret = 0;
011D170C mov dword ptr [ebp-20h],0 //把0放入[ebp-20h]地址中(将ret = 0放入)
15: ret = Add(a, b);
011D1713 mov eax,dword ptr [ebp-14h] //把[ebp-14h]地址的内容放入eax寄存器并压栈
011D1716 push eax
011D1717 mov ecx,dword ptr [ebp-8] //把[ebp-8]地址中的内容放入ecx寄存器并压栈(传递参数)
011D171A push ecx
011D171B call 011D10F0 //把call下一条指令的地址压栈,要不一会回不来了!
//在看汇编时走到这时按F11调到函数内部
四:进入Add函数创建并初始化一片空间完成操作(在调用每个函数后都会先创建并初始化一片空间,调用完后即被回收)
4: int Add(int x, int y)
5: {
011D1690 push ebp
011D1691 mov ebp,esp
011D1693 sub esp,0CCh
011D1699 push ebx
011D169A push esi
011D169B push edi
011D169C lea edi,[ebp+FFFFFF34h]
011D16A2 mov ecx,33h
011D16A7 mov eax,0CCCCCCCCh
011D16AC rep stos dword ptr es:[edi]
6: int sum = 0;
011D16AE mov dword ptr [ebp-8],0
7: sum = x + y;
011D16B5 mov eax,dword ptr [ebp+8] //取到形参a的值,放到寄存器eax中
011D16B8 add eax,dword ptr [ebp+0Ch] //将eax与形参b的值相加
011D16BB mov dword ptr [ebp-8],eax //将相加的结果重新拷贝回[ebp-8]中,此时sum=5
8: return sum;
011D16BE mov eax,dword ptr [ebp-8] //再次将此时[ebp-8]的内容放在eax中
9: }
从011D16AE开始一直到011D16BE就完成了调用参数,将结果放回sum,再将sum的值放入寄存器,等待传回结果。
五:返回主函数
011D16C1 pop edi
011D16C2 pop esi
011D16C3 pop ebx //将三个寄存器出栈
011D16C4 mov esp,ebp
011D16C6 pop ebp
011D16C7 ret
上述过程简单面描述就是先将该函数顶部三个寄存器出栈,再回收空间,由于之前的call指令,我们记住了call指令下一条指令的地址,ret操作后,将这个地址也再出栈,找到我们调用之前函数的内部。
六,传回参数
011D1720 add esp,8 //将esp向下挪8,跳过实参
011D1723 mov dword ptr [ebp-20h],eax //将我们之前计算的结果放入[ebp-20h]中
16: return 0;
011D1726 xor eax,eax
17: }
七,回收main的空间,完成整个函数过程
总结:我们在每次调用一个函数的过程中,就相当于再专门为这个函数开辟一片内存空间,等待函数完成内部操作后,再返回调用之前的函数内部,在这过程中,我们需要使用call指令来记住调用前call指令下一条指令的地址,要不回都回不来,每当一个函数完成对应操作后,自动释放空间。但是在函数递归调用时,我们可以设想下如果多次调用自己本身,空间又不能及时的被释放,会占用很大的内存空间,每次在使用时又要初始化,传参,在整体代码上也会受影响,可是如果我们不了解栈帧的话,就可能不会这么容易就知道递归的缺点!
新手上路,如发现错误请及时告知我,谢谢!