欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

详解函数的调用过程

程序员文章站 2022-06-14 22:26:20
...

·代码示例

我们用一段简单的代码来进行测试,代码如下:

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函数就不再画了:

详解函数的调用过程