非原创, 有删改,仅做个人笔记
1.我们在调用它的时候系统做了什么?
2.main函数中如果还有另一个函数,在跳转后运行完这个函数时,编译器怎么知道下一行执行哪个语句呢?会不会又从头执行了?
3.函数在结束之后(运行到反花括号“}”处),系统又是怎么处理的?
4.不同的语言对函数形参内存的处理都是一样的吗?
5.函数的返回值有哪些类型?都是怎么从函数中返回回来的呢?
示例代码:
#include <stdio.h>
int sum (int a,int b)
{
int temp = 0;
temp = a + b;
return temp;
}
int main ()
{
int a = 10;
int b = 20;
int ret = 0;
ret = sum(a,b);
printf("ret = %d\n",ret);
return 0;
}
运行时,从main开始执行。在调用main函数时,首先要做的就是创建main函数的栈帧。也就是说,调用一个函数之前肯定是先给他开辟栈帧,存放函数中的变量以及其它的东西。具体有什么,我们就来看看main函数的栈帧吧
图一
这是main函数最开始的样子,其实也可以说是每个函数最开始的样子。ebp和esp是两个寄存器,不过它们都是在保存栈的地址,作用和指针没什么差别,我们就把它们看成指针。其中,ebp是栈底指针,处于高地址处,esp是栈顶指针处于低地址处。在调用了函数从左边的花括号开始,函数就开辟好了自己的空间。esp指向一块区域同时也是栈底
进入main之后继续往下走,是a,b,ret三个局部变量。系统就将它们都存入main函数的栈帧中, 通过ebp减去偏移量
的方式来访问内存。
现在,我们将a、b、ret都存入栈了,现在栈长这样:
图二
接下来我们继续执行。执行到这一句:ret = sum(a,b);
我们现在要进入sum函数去了,sum 函数有参数, 此时参数以从右向左的顺序依次入栈, 访问的话通过 ebp减去偏移量访问
, 执行完 sum 函数需要返回到当前上下文以继续执行, 需要将 sum 函数的下一行代码的地址记录到栈中.
现在的栈如下:
图三
该正式地调用sum函数了,对代码进行反汇编可以看到此时执行了一个jmp
指令。我们在讲虚拟地址空间的时候说过给函数分配的地址是一个偏移量,偏移量加上pc寄存器的值才能跳转到函数真实的地址。所以在调用call时,先会跳转到jmp表
确定函数的地址,再进入函数。
同时,esp发生了变化,向低地址方向移动了。这说明,在跳转到jump这里时,sum函数的下一行指令地址入栈了, 用于回到代码原来位置的。现在的栈如下已经接下来sum函数的汇编代码如下:
图四
图五
我们进入到了sum函数里。不过,在往sum函数存它自带的东西之前我们当然要先开辟sum的栈帧
啦!和main函数类似,我们就借此机会了解一下栈帧开辟的具体过程。
首先调用push ebp,根据上图我们此时的ebp指向的是main函数的栈底地址。push ebp即将这个地址入栈,这就是便于sum函数执行完毕之后能让ebp回退到main函数栈底的方法。所以这块内存存的就是调用方函数的栈底地址。我们一开始说main函数最底下那块“神秘区域”现在你明白里面存的什么了吗?没错,就是调用main函数的函数(mainCRTStartup)函数的栈底指针,main函数结束后ebp会回退到mainCRTStartUp()函数的栈底!
接下来mov ebp,esp 将esp赋给ebp,即让ebp指向esp指向的那块区域。由于刚才入栈了一次main函数的栈底地址,所以现在栈已经成了这样:
图六
接下来进行的操作 sub esp 0CCh 将esp+=0CC 即将栈顶指针上移了0CC,作为sum函数的栈顶。也意味着sum函数栈帧的大小 就是0CC。接下来的对edi,eci,ecx寄存器的操作我们都不用关心,因为它们在这例子里什么都没做,只是入栈又出栈。我们要重点关心的是这两句:
mov eax 0CCCCCCCCh
rep stos dword ptr es:[edi]
这两句的意思是先将CCCCCCCC存进eax,再用rep stos指令(类似循环拷贝)对我们开辟的栈进行初始化赋值。于是栈变成了这样:
图七
这时,sum函数的栈帧就完全申请好了。main函数在创建的时候,也会经历这样一串固定的流程。说句题外话,0CCCCCCCC对应的汉字就是“烫”,所以我们平常如果看见一串“烫烫烫烫…”你就应该明白了,你打印的值指向的是栈中没有人为初始化的部分!(另一种情况是堆中未人为初始化,显示的是“屯”)
接下来就是存入temp这个局部变量了,当然地址是ebp-4。这时候我们的栈帧就是完全状态了!
图八
现在执行sum函数, 将里面的局部变量temp等压入栈中
…
这时,我们的sum函数已经执行完了,迎来了它生命的终结:“}”反花括号。那么开辟了的栈帧要怎么回退呢?
图九
我们看到,首先是edi,esi,ebx这三个我们说过的没起作用的寄存器出栈。然后mov esp,ebp即将esp的指向从栈顶直接拉到栈底,放弃栈的空间。此时回到了图六
的状态。
然后pop ebp,这行指令的意思是先将此时栈顶的元素赋值给ebp,再将栈顶出栈。那么这时候ebp就回到了main函数的栈底,回归到图五
的状态。
接下来是ret指令。这个指令又具体干了什么呢?ret和pop很像,也是将当前栈顶元素(sum下一行指令的地址)赋给一个寄存器然后出栈。赋给谁呢?赋给专门存下一行运行地址的 pc寄存器了。
现在梳理一下状况,ebp已经回到main函数的栈底,esp也由于出栈操作退回到了sum函数的两个实参处,eax寄存器中保存着sum函数的运算结果:temp=a+b,pc寄存器中保存着sum下一行的指令。现在栈的情况是图三
.
现在sum函数已然消失,传入的形参a和b自然也应该人去楼空了。编译器执行add esp,8,将esp向高地址移动8位,退还a,b所占的栈帧。此时栈状态是图二.
接下来,我们要将eax中存的计算结果赋给main函数中的ret变量了:
mov dword ptr [ebp-0Ch],eax
至此,sum函数正式结束。接下来就是打印函数了。当打印函数也完成后,我们的main函数也遇到了它的反花括号。重复上述的函数退栈过程,栈底指针回归mainCRTStartUp()中。这便是这份简单的源代码运行的背后的流程。