详解函数的调用过程
·代码示例
我们用一段简单的代码来进行测试,代码如下:
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = Add(a, b);
printf("ret=%d\n", ret);
system("pause");
return 0;
}
我们用vs进入调试,打开【调用栈堆】后会发现main函数在__tmainCRTStartup函数中被调用,而__tmainCRTStartup函数则是在mainCRTStartup中被调用,这两个在后面的说明中就保留mainCRTStartup入口。
我们都知道函数在被调用的过程中,每一次调用都要开辟一块内存空间。那么这一块内存空间是由谁来进行维护的呢?我们会答是由操作系统来完成维护的,但是这还并不准确,那么我们就要了解一下寄存器的概念。
·什么是寄存器
寄存器是*处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。在*处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在*处理器的算术及逻辑部件中,寄存器有累加器(ACC)。——来自百度百科
看完概念以后,我们再去想,一个函数如果要去返回一个值,但是被调用的函数销毁后是什么来存储这个值的。其实也是寄存器,这样的话其实也就不难理解了。并且我们下面对于函数调用的理解也离不开寄存器。
·函数调用过程
·mian函数
我们进入调试转入到反汇编同时打开监视和内存。如下图:
我们查看ebp和esp的地址:
我们这一段内存空间就是为mainCRTStartup开辟的,体现在内存中就是如下:
我们知道函数是在栈开辟空间的,由高地址向低地址。与此同时我们也需要知道esp和ebp两个寄存器,ebp指向的是栈底即高地址处,而esp指向的是栈顶即低地址处。接着用下面的图来演示调用的过程:
00961420 push ebp :将ebp进行压栈,压如ebp之后,esp便指向了ebp。
00961421 mov ebp,esp :将esp的值存到ebp中,意思就是ebp和esp此时指向同一处,即上面压入的ebp。
00961423 sub esp,0E4h :将esp减去0E4h的值,0E4h转换成十进制我们发现是228。那么这个时候ebp和esp便不是指向同一个位置。因为减少,所以esp在我们的图上就是向上移动的。这也就是说明,操作系统又为内存开辟了一块空间,而这一块空间就是main函数的预开辟空间,我们把它称之为函数的运行时堆栈也可以叫做函数的栈帧。
我们十进制来验证一下:
发现确实是228,这也是main函数的初始的大小。
00961429 push ebx :压入ebx寄存器。
0096142A push esi :压入esi寄存器。
0096142B push edi :压入edi寄存器。
0096142C lea edi,[ebp-0E4h] :将ebp减去0E4h的值放入edi中,edi也就是存储的main函数栈顶的地址。
00961432 mov ecx,39h :将39h存储到ecx中,39h转换成10进制便是57。ecx的作用在下面我们会讲到,是一个次数。
00961437 mov eax,0CCCCCCCCh :将0CCCCCCCCh存储到eax中。eax主要是用来初始化。
0096143C rep stos dword ptr es:[edi] :重复拷贝4字节的内容eax,共拷贝ecx次。这时我们发现57*4也正好是228,这一步的操作就是对内存中main函数的空间进行初始化,这一步操作相信大家都不难理解。我们验证一下:
下面是我们这一部分的栈堆空间图:
接下来我们看main函数剩下的部分:
0096143E mov dword ptr [a],0Ah :这一段就是将0Ah放入,0Ah也就是我们给a赋值的10,这里如果用vc6.0编译器的话我们会看到[a]其实是[ebp-4],也就是ebp向上移了4个字节给a开辟了空间。这时我们也可以理解到临时变量时存放在栈空间中的,也可以说是存放在main开辟的内存空间中的了。当然我们也有必要打开内存验证一下:
00961445 mov dword ptr [b],14h :同上。
0096144C mov dword ptr [ret],0 :同上。
00961453 mov eax,dword ptr [b] :将b的值保存到eax中。可以理解为a'。这里我们也应该能初步理解到形参只是实参的拷贝这一概念了。
00961456 push eax :压入eax。
00961457 mov ecx,dword ptr [a] :将a的值保存到ecx中。可以理解为b'。
0096145A push ecx :压入ecx。
0096145B call _Add (09610E6h) :call指令指向的是下一条指令的地址,因为我们调用完函数之后需要知道应该从哪一条继续执行。这一句执行后,也就会跳到Add函数中。
00961460 add esp,8 :esp加8,位置向下。
00961463 mov dword ptr [ret],eax :将eax中的值赋给ret。注意了,这里的eax并不是上面保存的b的值,因为在call指令中,我们就已经进入到了Add函数之中,eax存放的其实是Add函数的返回值z。
·Add函数
009613D0 push ebp :压入ebp,注意,这里的ebp我们可以理解为ebp-main,至于为什么我们下面会说道。
009613D1 mov ebp,esp :将esp赋给ebp,ebp和esp指向同一个位置,这边同main函数是一样的。
009613D3 sub esp,0CCh :同main函数,这个时候系统也已经给Add函数开辟了内存空间了。
009613D9 push ebx :同main函数。
009613DA push esi :同main函数。
009613DB push edi :同main函数。
009613DC lea edi,[ebp-0CCh] :将ebp减去0CCh的值放入edi中,也就是Add栈顶的位置。
009613E2 mov ecx,33h :同上。
009613E7 mov eax,0CCCCCCCCh :同上。
009613EC rep stos dword ptr es:[edi] :同上。
009613EE mov dword ptr [z],0 :给z开辟空间并且赋值为0。
009613F5 mov eax,dword ptr [x] :将x的值放入eax中,这边的x其实是上面ebp+8也就是上面的a'。
009613F8 add eax,dword ptr [y] :这里就是eax加上b'其实也就是a'+b'。
009613FB mov dword ptr [z],eax :将eax放入到z中。
009613FE mov eax,dword ptr [z] :将z放入到eax中。
00961401 pop edi :弹出edi。
00961402 pop esi :弹出esi。
00961403 pop ebx :弹出ebx。
00961404 mov esp,ebp :将ebp赋给esp。
00961406 pop ebp :弹出栈顶给ebp,ebp就会回到初始的位置,也就是ebp-main。而这个ebp-main其实就是存放main函数ebp的地址,这样子我们的ebp和esp在Add函数销毁后又回到了main函数中,继续对main函数剩下的语句进行操作。
00961407 ret :用于返回,也就是返回到了main函数的下一条指令中,见上文。
最后附上总图,Add函数同main函数就不再画了:
上一篇: LAMP中PHP效能 的动态扩展