每次函数调用,都为函数开辟一块空间,成为栈帧。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),我们称为栈底指针,寄存器esp指向当前的栈帧的顶部(低地址),我们称为栈顶指针。
注意:EBP指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。
给段代码,剖析下面函数运行过程。
运行环境:VC6.0,(相比VS,更容易查看内存)
#include<stdio.h>
int Sub(int x,int y)
{
int t=;
t=x-y;
return t;
}
int main()
{
int a=;
int b=;
int c=;
c=Sub(a,b);
return ;
}
给出这段代码的汇编代码
: int main()
: {
push ebp
mov ebp,esp
sub esp,Ch
push ebx
push esi
push edi
lea edi,[ebp-Ch]
C mov ecx,h
mov eax,CCCCCCCCh
rep stos dword ptr [edi]
: int a=;
mov dword ptr [ebp-],Ah
: int b=;
F mov dword ptr [ebp-],h
: int c=;
mov dword ptr [ebp-Ch],
: c=Sub(a,b);
D mov eax,dword ptr [ebp-]
push eax
mov ecx,dword ptr [ebp-]
push ecx
call @ILT+(_Sub) ()
A add esp,
D mov dword ptr [ebp-Ch],eax
:
: return ;
接下来分析这段汇编代码
在这里我们要知道在VC++下,连接器对控制台程序设置的入口函数是 mainCRTStartup,mainCRTStartup 再调用main 函数;
所以当我们操作时,首先会给mainCRTStartup()函数开辟一段空间,然后esp和ebp在他们所在的位置
(1) push ebp
push就是压栈,把ebp 的地址压入栈中,
注:每次压栈后,esp都指向最新的栈顶位置
(2) mov ebp,esp
使ebp=esp,即ebp也指向栈顶位置
(3) 为函数预开辟空间
sub esp,Ch
(4)3个push 以及初始化开辟的空间
push ebx
push esi
push edi
lea edi,[ebp-Ch]
mov ecx,h
mov eax,CCCCCCCCh
rep stos dword ptr [edi]
解释一下,3个push 分别把ebx,esi,edi 3个寄存器压入栈中。
lea 就是把 [ebp-4Ch]的地址放在edi中,ebp-4Ch是3个push之前esp的位置
2个move操作,ecx寄存器的值为13h,eax为初始化值0ccccccccch
然后rep stos:实际上就是把初始化开辟的空间,初始值为eax寄存器内的值0CCCCCCCCh,
从edi开始(edi保存的esp的位置),向高地址的部分进行字节拷贝,每一次拷贝4个字节。
拷贝的内容就是eax的内容,拷贝次数为13h次。
注:用0xccccccccch初始化,所以未初始化的字符串,经常看到“烫烫”
可以查看内存值的变化,来验证
(5)实参入栈
10: int a=;
00401078 mov dword ptr [ebp-4],0Ah
11: int b=;
0040107F mov dword ptr [ebp-8],14h
12: int c=;
00401086 mov dword ptr [ebp-0Ch],0
(6)调用sub函数准备,形参入栈
形参从右向左入栈的,看出形参是实参的一份拷贝
: c=Sub(a,b);
D mov eax,dword ptr [ebp-]
push eax
mov ecx,dword ptr [ebp-]
push ecx
ebp-8就是b的位置,ebp-4就是a的位置
(7)call指令
00401095 call @ILT+(_Sub) ()
A add esp,
call指令就是把下一条指令add的地址0040109A压入栈中
(8)进入sub函数
: int Sub(int x,int y)
: {
push ebp
mov ebp,esp
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;
8 mov dword ptr [ebp-],
: t=x-y;
F mov eax,dword ptr [ebp+]
sub eax,dword ptr [ebp+0Ch]
00401045 mov dword ptr [ebp-4],eax
6: return t;
8 mov eax,dword ptr [ebp-]
: }
B pop edi
C pop esi
D pop ebx
E mov esp,ebp
pop ebp
ret
步骤其实大致和main函数一样
(8.1) 为sub函数准备
push ebp
此时ebp指向的main函数的栈底指针
mov ebp,esp
sub esp,h
push ebx
push esi
push edi
lea edi,[ebp-h]
C mov ecx,h
mov eax,CCCCCCCCh
rep stos dword ptr [edi]
以上代码 就不细细分析,大概和main函数2,3,4步骤差不多
(8.2)指向sub函数,计算差值
4: int t=;
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]
看出,计算机只认识地址,不认识变量名,
把t初始化为0,然后计算t=x-y,把ebp+8的值(a) 存放在eax,然后把eax值为ebp+12的值(b) 相减 放在eax中,
然后把eax值保存在t中
返回值 t,把ebp-4内的值(t)取出放在eax中
(9)函数调用结束,释放栈帧
这里先介绍一个概念
现场保护 当出现中断时,把CPU现在的状态,也就是中断的入口地址保存在寄存器中,随后转向执行其他任务,当任务完成,从寄存器中取出地址继续执行。保护现场其实就是保存中断前一时刻的状态不被破坏。保护现场通过利用一系列PUSH指令保护CPU现场,即将相关寄存器的内容入栈保护起来。
所以要把ebp 入栈push
B pop edi
C pop esi
D pop ebx
E mov esp,ebp
pop ebp
接下来的指令就是返回,先进行3次出栈,把栈顶的指令分别给了edi,esi,ebx三个寄存器。然后把ebp给了esp,这时也就是让esp指向了ebp的位置,这是ebp和esp指向同一位置,这个位置就是你所保存的main()函数的ebp,然后再pop ebp,这样ebp就维护到main函数的栈帧了
在这,当ret指令执行之后,会pop一下,把这个地址pop以后,就从Sub函数返回了main()函数,这也是最初为什么要保存这个地址的原因。这样call指令就完成了。此时指向mian函数中call指令的下一条指令add
A add esp,
D mov dword ptr [ebp-Ch],eax
main函数中
esp+8 :把形参a,b 释放
mov dword ptr [ebp-0Ch],eax:把eax中值(返回值t)保存在ebp-12(c的位置)中
接下来,和对函数的返回类似,对main()函数的返回,然后再销毁main()函数,执行ret指令。