前言
本次实验室主要是在vs2013编译器下进行的。计算机内部存在着各种各样的寄存器,例如eax ebx ecx edx以及ebp esp,其中ebp和esp ,这两个寄存器存放的是函数开辟栈区域的地址,这两个地址是用来维护函数栈帧的。ebp指向的是函数栈帧的底部,esp指向的是栈帧的顶部。每一个函数调用,都要在栈区创建一份空间,然后esp和ebp就会维护这段空间的地址,直到这个函数调用完成开始返回,esp和ebp回到main函数。函数的入口是main函数,main函数也是被其他函数所调用的,调用关系如下:
函数栈帧的创建
函数栈帧的创建主要是分为以下几个部分:main函数执行前的准备操作,执行main函数,调用函数并返回,执行main函数。首先需要知道这一系列都是栈空间上执行的,栈是从高地址往低地址增长的过程。
main函数执行前的准备操作
函数代码如下:
int Add(int a, int b)
{
int z = 0;
z = a + b;
return z;
}
int main() {
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
第一阶段的汇编代码如下:
00D01960 push ebp
00D01961 mov ebp,esp
00D01963 sub esp,0E4h
00D01969 push ebx
00D0196A push esi
00D0196B push edi
00D0196C lea edi,[ebp-0E4h]
00D0196F mov ecx,39h
00D01974 mov eax,0CCCCCCCCh
00D01979 rep stos dword ptr es:[edi]
14: int a = 10;
第一阶段主要是main函数栈帧的开辟,首先esp和ebp从_tmainCRTStartup的栈帧向下增长,记录下该栈帧ebp的值,此时在push的时候esp同样会向上挪动,紧接着将esp此时的值赋给ebp作为main函数的栈底,此时esp进行sub操作,在一个新的位置作为栈顶,此时esp到ebp之间的空间就是此时为mian函数开辟的空间,然后压入三个寄存器ebx,esi,edi,如下图所示:
紧接着lea(load effective address)edi,[ebp-0E4h],主要是为了把main函数栈帧的最开始的ebp的值放入edi寄存中,紧接着接下来的三句汇编指令主要是对这段main函数栈帧的空间进行初始化,全部初始化成0CCCCCCCCh。整个完整的过程如下图所示:
调用函数
14: int a = 10;
00D01985 mov dword ptr [ebp-8],0Ah
15: int b = 20;
00D0198C mov dword ptr [ebp-14h],14h
16: int c = 0;
00D01993 mov dword ptr [ebp-20h],0
17: c = Add(a, b);
00D0199A mov eax,dword ptr [ebp-14h]
00D0199D push eax
00D0199E mov ecx,dword ptr [ebp-8]
00D019A1 push ecx
00D019A2 call 00D011C2
00D019A7 add esp,8
00D019AA mov dword ptr [ebp-20h],eax
首先对变量进行初始化,在ebp-8的位置,将值置为10,ebp-20的位置置为20,ebp-32的位置将值置为0,分别对应了a,b,c三个变量:
紧接着调用函数,对于形参而言,首先将变量从右向左进行拷贝,分别拷贝到eax和ecx寄存上,再将这两个寄存器压栈 。紧接着call函数,此时会进行地址的跳转,但是此时会将call语句的下一条语句的地址压入栈顶,为了返回的时候能够重新回到这里。
执行该函数的时候,跟main函数类似,首先需要为他开辟一段栈帧空间,并且进行初始化成CCCCCCCCh。
00D01780 push ebp
00D01781 mov ebp,esp
00D01783 sub esp,0CCh
00D01789 push ebx
00D0178A push esi
00D0178B push edi
00D0178C lea edi,[ebp-0CCh]
00D0178F mov ecx,33h
00D01794 mov eax,0CCCCCCCCh
00D01799 rep stos dword ptr es:[edi]
在初始化完成之后,开始调用形参进行计算,将参数的值从eax和ecx中取出来,并且赋值给z,最后返回的时候将z的值拷贝给eax寄存器,至此函数调用结束。
8: int z = 0;
00D017A5 mov dword ptr [ebp-8],0
9: z = a + b;
00D017AC mov eax,dword ptr [ebp+8]
00D017AF add eax,dword ptr [ebp+0Ch]
00D017B2 mov dword ptr [ebp-8],eax
10: return z;
00D017B5 mov eax,dword ptr [ebp-8]
返回main函数
在执行完函数之后,紧接着开始返回。
00D017B8 pop edi
00D017B9 pop esi
00D017BA pop ebx
00D017C8 mov esp,ebp
00D017CA pop ebp
00D017CB ret
00D019A7 add esp,8
00D019AA mov dword ptr [ebp-20h],eax
把空间回收,把ebp的值给esp,然后弹出栈顶元素并且赋值给ebp,也就是main函数的栈底元素的值,就回到了main函数的栈帧空间,此时esp指向的是调用函数语句的下一条语句的地址,ret指令就是执行栈顶元素的地址的那一条语句。紧接着add esp 8将esp指向了edi的位置,此时形参就被销毁。然后把eax的值交给变量c。
main函数栈帧销毁跟add函数的销毁一样。
总结
局部变量的创建:为函数分配好栈帧空间,对栈帧空间进行初始化,然后给局部变量分配空间初始化,所以初始化局部变量的值是随机值,是栈帧初始化的时候的随机值。函数的传参通过还没调用函数的时候,从右向左进行压栈,压入到eax和ecx寄存器中,紧接着给函数建立栈帧,然后通过偏移量找到对应的参数,所以形参只是实参的一份临时拷贝。函数的返回是:在调用函数的时候,就已经把call指令的下一条指令的地址压入栈中,然后把调用该函数的上一个函数的栈帧的ebp值也压入栈帧中,等函数返回的时候,弹出ebp,就可以找到上一个函数的ebp,然后指针往下走,因为记住了call下一条指令的函数地址,所以返回到的时候,就可以接着call的下一条指令接着运行。