关于栈帧,从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),我们称为栈底指针,寄存器esp指向当前的栈帧的顶部(低地址),我们称为栈顶指针。
注意:EBP指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。
接下来我们通过下面的这个小程序来进行分析这个过程。
测试环境:VC6.0
#include<stdio.h>
int Sub(int x,int y)
{
int t=0;
t=x-y;
return t;
}
int main()
{
int a=10;
int b=20;
int c=0;
c=Sub(a,b);
return 0;
}
首先拿出这段程序的汇编代码
9: {
00401060 push ebp
00401061 mov ebp,esp
00401063 sub esp,4Ch
00401066 push ebx
00401067 push esi
00401068 push edi
00401069 lea edi,[ebp-4Ch]
0040106C mov ecx,13h
00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi]
10: int a=10;
00401078 mov dword ptr [ebp-4],0Ah
11: int b=20;
0040107F mov dword ptr [ebp-8],14h
12: int c=0;
00401086 mov dword ptr [ebp-0Ch],0
13: c=Sub(a,b);
0040108D mov eax,dword ptr [ebp-8]
00401090 push eax
00401091 mov ecx,dword ptr [ebp-4]
00401094 push ecx
00401095 call @ILT+0(_Sub) (00401005)
0040109A add esp,8
0040109D mov dword ptr [ebp-0Ch],eax
14:
15: return 0;
接下来,我们对此进行分析:
在这里我们要知道在VC++下,连接器对控制台程序设置的入口函数是 mainCRTStartup,mainCRTStartup 再调用main 函数;
所以当我们操作时,首先会给mainCRTStartup()函数开辟一段空间,然后esp和ebp在他们所在的位置。
push ebp
push意思是压栈的意思,我们在这里就可以理解为入栈操作,在这里就是把ebp栈,就是ebp的地址压入。
mov ebp,esp
这个的意思就是上一栈帧的顶部,就是这个栈帧的底部。所以这时,ebp和esp都位于栈顶。
sub esp,4Ch
push ebx
push esi
push edi
在这里的,第一句说的就是开辟空间4ch这么一块的大小,接下来继续三次压栈。
lea edi,[ebp-4Ch]
mov ecx,13h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
接下来所做的就是初始化的操作,
lea edi,[ebp-4Ch]
这一句话是把ebp-44h放到edi中
mov ecx,13h
这句话把13h放到ecx中去,ecx是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。作循环用。
mov eax,0CCCCCCCCh
接下来把0CCCCCCCCh放到eax里面去,eax 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。
rep stos dword ptr [edi]
这句话的意思就是对edi开始,向高地址的部分进行字节拷贝,每一次拷贝4个字节。
拷贝的内容就是eax的内容,拷贝次数为13h次
VC 6.0通过查看内存验证效果:
10: int a=10;
00401078 mov dword ptr [ebp-4],0Ah
在这就是把a放到ebp-4的位置
11: int b=20;
0040107F mov dword ptr [ebp-8],14h
在这就是把b放到ebp-8的位置
12: int c=0;
00401086 mov dword ptr [ebp-0Ch],0
在这就是把c放到ebp-12的位置
在这其实把所有的变量转换成了地址,在这里我们要清楚机器不认识你的变量名,它认识的只是地址。
13: c=Sub(a,b);
0040108D mov eax,dword ptr [ebp-8]
00401090 push eax
00401091 mov ecx,dword ptr [ebp-4]
00401094 push ecx
在这其实你就是传递参数,把两个参数进行了压栈
内存中的情况:
00401095 call @ILT+0(_Sub) (00401005)
0040109A add esp,8
0040109D mov dword ptr [ebp-0Ch],eax
call指令在这里会再次进行一次压栈,把它下面那条语句的地址压进去,也就是0040109A
在这是因为小端存储,所以出现到了这种情况。
接下来,就进入Sub()函数:
1: #include<stdio.h>
2: int Sub(int x,int y)
3: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
4: int t=0;
00401038 mov dword ptr [ebp-4],0
5: t=x-y;
0040103F mov eax,dword ptr [ebp+8]
00401042 sub eax,dword ptr [ebp+0Ch]
00401045 mov dword ptr [ebp-4],eax
6: return t;
00401048 mov eax,dword ptr [ebp-4]
7: }
0040104B pop edi
0040104C pop esi
0040104D pop ebx
0040104E mov esp,ebp
00401050 pop ebp
00401051 ret
00401020 push ebp
在这里再次进行压栈,在这首先压栈ebp,在这压进去的是main函数的ebp,这些ebp在后期都会有很大的作用。
内存效果图:
00401021 mov ebp,esp
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
这些指令进行的其实也就和上面我们对main()的分析一样在这移动ebp,然后进行压栈三次,然后进行初始化。
内存效果如图:
4: int t=0;
00401038 mov dword ptr [ebp-4],0
在这把t放到ebp-4的位置
5: t=x-y;
0040103F mov eax,dword ptr [ebp+8]
00401042 sub eax,dword ptr [ebp+0Ch]
00401045 mov dword ptr [ebp-4],eax
在这就相当于进行了对ebp+8保存的参数和ebp+12保存的参数进行了减法。然后,把算出来的结果,放到ebp-4的位置。也就是算出来的结果放到了t里面去
6: return t;
00401048 mov eax,dword ptr [ebp-4]
在这把ebp-4的值放到寄存器eax返回,这的eax直到返回程序才会再次看到。在这要注意,这里不一定只用寄存器返回,毕竟寄存器是有大小的,太大也会放不下。通常情况下自定义类型返回采用寄存器,因为自定义类型比较小,32位平台下最大也只有8个字节
在这里要牵扯一个概念:
现场保护:当出现中断时,把CPU现在的状态,也就是中断的入口地址保存在寄存器中,随后转向执行其他任务,当任务完成,从寄存器中取出地址继续执行。保护现场其实就是保存中断前一时刻的状态不被破坏。保护现场通过利用一系列PUSH指令保护CPU现场,即将相关寄存器的内容入栈保护起来。
在这你就可以知道为什么对ebp要push进去。
7: }
0040104B pop edi
0040104C pop esi
0040104D pop ebx
0040104E mov esp,ebp
00401050 pop ebp
接下来的指令就是返回,先进行3次出栈,把栈顶的指令分别给了edi,esi,ebx三个寄存器。然后把ebp给了esp,这时也就是让esp指向了ebp的位置,这是ebp和esp指向同一位置,这个位置就是你所保存的main()函数的ebp,然后再pop ebp,这样ebp就维护到main函数的栈帧了。
在这里你需要特别注意下,这里上面的空间不属于你了,但是如果没有人使用这块空间,这块空间的依然没有变化。
00401051 ret
在这,当ret指令执行之后,会pop一下,把这个地址pop以后,就从Sub函数返回了main()函数,这也是最初为什么要保存这个地址的原因。这样call指令就完成了。
0040109A add esp,8
0040109D mov dword ptr [ebp-0Ch],eax
接下来是分析main函数中的这两句,在这首先给esp+8,加了8之后,就把形参也弹出去,形参这是也没用了。
然后,ebp-0Ch,就是c,然后把eax放到c中,意思就是把刚才计算的结果放到c中。
接下来,和对函数的返回类似,对main()函数的返回,然后再销毁main()函数,执行ret指令。
博客写的肯定有不足指出,望大家多多指出!!