每当函数被调用的时候,都会向内存空间申请一定的地址(地址的大小由编译器决定),这一块内存就被成为函数栈帧。
在栈中有这一些寄存器(这些寄存器与内存是独立分开的)每个函数的栈帧是由esp和ebp来维护的。
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
我们那下面的一段C语言代码,进行反编汇来进行理解。
#define _CRT_SECURE_NO_WARNINGS 1
#include
int Add(int x, int y)
{
int res = 0;
res = x + y;
return res;
}
int main()
{
int a = 10;
int b = 20;
int res = 0;
res=Add(a, b);
printf("%d", res);
return 0;
}
在main函数调用前,先进行了invoke_main函数的调用。这里我们不去深度研究这个函数的作用。
所以我们就可以知道,在我们写的代码中,至少有三个函数(main、invoke_main、Add)拥有自己的栈帧。这里我们进行到反编汇。
int main()
{
00A618B0 push ebp //把ebp的值压入栈中,这里的ebp的值存放的是invoke_main这个函数的栈底地址。
00A618B1 mov ebp,esp //将esp(invoke_main函数的栈顶地址)的值赋给ebp
00A618B3 sub esp,0E4h //这个0E4h就是编译器为我们的main函数申请到的函数地址,这里将esp的值减去0E4h
00A618B9 push ebx
00A618BA push esi
00A618BB push edi //进行了三个压栈操作
//下面的代码是对栈帧空间进行初始化
00A618BC lea edi,[ebp-24h] //把ebp-24h的值赋给edi
00A618BF mov ecx,9 //把9赋值给ecx
00A618C4 mov eax,0CCCCCCCCh //把0CCCCCCCCh赋值给eax
00A618C9 rep stos dword ptr es:[edi] // 将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCC
00A618CB mov ecx,offset _F6609206_zhanzhentest@c (0A6C003h)
接下来进行到函数的内部:
int a = 10;
00A618D5 mov dword ptr [ebp-8],0Ah //在ebp-8的地址空间为a开辟一个内存空间,存放10
int b = 20;
00A618DC mov dword ptr [ebp-14h],14h //在ebp-14h的地址空间b 开辟一个内存空间,存放20
int res = 0;
00A618E3 mov dword ptr [ebp-20h],0 //在ebp-20h的地址空间res 开辟一个内存空间,存放0
res=Add(a, b);
00A618EA mov eax,dword ptr [ebp-14h] //此时ebp-14h的值为b的值,将b的值赋给eax
00A618ED push eax //把eax的值进行压栈 esp-4
00A618EE mov ecx,dword ptr [ebp-8] //此时ebp-8的值是a的值,把a的值赋给ecx
00A618F1 push ecx //把ecx的值进行压栈 esp-4
//我们可以看到,在创建形参的时候,形参是实参的一份临时拷贝,放在原函数的栈帧中
//接下来会用到call指令,这个指令的运行结果途中所示。
/*call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。这样做的作用是,当函数调用完后,栈帧被销毁后,可以继续找到调用函数的下一条指令的地址,进而继续执行程序。*/
//我们可以看到 call指令的下一条指令的地址是00A618F7
00A618F2 call 00A610B4 //调用Add函数
//这时我们跳转到函数内部
int Add(int x, int y)
{
00D91770 push ebp //把main函数的ebp的值压入栈中,esp-4
00D91771 mov ebp,esp //把esp的值赋给ebp
00D91773 sub esp,0CCh //将esp减去0CCh
00D91779 push ebx
00D9177A push esi
00D9177B push edi //三个压栈操作
00D9177C lea edi,[ebp-0Ch]
00D9177F mov ecx,3
00D91784 mov eax,0CCCCCCCCh
00D91789 rep stos dword ptr es:[edi] //初始化Add函数的栈帧
00D9178B mov ecx,0D9C003h
00D91790 call 00D9131B
int res = 0;
00D91795 mov dword ptr [ebp-8],0
res = x + y;
00D9179C mov eax,dword ptr [ebp+8] //把a’的值放在eax寄存器中
00D9179F add eax,dword ptr [ebp+0Ch] //把b’的值与eax相加后即存在eax中
00D917A2 mov dword ptr [ebp-8],eax //把eax的值赋值给res
return res;
00D917A5 mov eax,dword ptr [ebp-8] //把return的res的值赋给eax寄存器
}
//下面进行栈帧的销毁,我们会发现,即使函数的栈帧被销毁了,我们的eax寄存器依然存着我们的返回值
00D917A8 pop edi //出栈操作,把栈顶的值赋给edi并删除,esp-4
00D917A9 pop esi //出栈操作,把栈顶的值赋给esi并删除,esp-4
00D917AA pop ebx //出栈操作,把栈顶的值赋给ebi并删除,esp-4
00D917AB add esp,0CCh //0CCh与我们当时申请到的空间相同,所以删除掉
00D917B1 cmp ebp,esp
00D917B3 call 00D91244
00D917B8 mov esp,ebp //把ebp的值赋给esp
00D917BA pop ebp //把栈顶弹出赋值给ebp,此时ebp指向main函数的ebp
00D917BB ret //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指
令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行
也就是进行到下面的指令。
00A618F7 add esp,8 //相当于把a’和b’进行销毁
00A618FA mov dword ptr [ebp-20h],eax //把eax的值赋给了我们main中的res
//接着进行以下操作
printf("%d", res);
00A618FD mov eax,dword ptr [ebp-20h]
00A61900 push eax
00A61901 push 0A67B30h
00A61906 call 00A610D2
00A6190B add esp,8