函数的调用过程
每一次函数的调用都是一个过程,这个过程我们称之为函数的调用过程。这个过程需要为函数开辟栈空间,用于函数调用中临时变量的保存,保护。这块栈空间我们称之为函数栈帧。
先写一段代码:
#include <stdio.h>
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);
return 0;
}
在调用main函数之前
- 先按F10进行逐过程调试
可以看到在调用主函数main之前,还调用了
__tmainCRTStartup()函数 和 mainCRTStartup()函数。 - 然后我们对准主函数main右键,转到反汇编。
可以观察到先是移动ebp、esp两个寄存器。
ebp-保存堆栈底部的地址
esp-保存堆栈顶部的地址
在调用main函数之前,__tmainCRTStartup()函数的堆栈
- 在反汇编里继续往下走
堆栈从下往上是由高地址到低地址,函数一步步往后走,堆栈是逐步往上面压栈。
注:每个方块都为4个字节
1、在__tmainCRTStartup()上压一个栈,移动esp,ebp。
2、为main函数预开辟一个E4h大小的空间。
3、在预开辟空间上压三个栈ebx,esi,edi(先不做介绍,因为到最后这三个栈会被原封弹出栈空间)。
4、将为main开辟的空间初始化为CCCCCCCC。
开始执行main函数
- 定义变量赋值
1、创建a,放在[ebp-8h],从ebp开始向上数第8个字节的空间用来存放a的值0Ah。
2、创建n,放在[ebp-14h],从ebp开始向上数第20个字节的空间来存放b的值14h。
3,、创建ret,放在[ebp-20h],从ebp开始向上数第32个字节的空间来存在ret的值0。 - 准备给函数传参
1、将[ebp-14h](也就是b)赋值给eax,并把eax压栈。
2、将[ebp-8](也就是a)赋值给ecx,并且压栈。
3、开始调用函数(记住call前面的地址),当F10走到这里时,按F11进入函数,然后继续F10。
此时,又会在栈顶部压一个地址,用来存放调用函数的地址(以便用完函数可以准确的跳回主函数main)
函数的调用
- 跟main函数一样,Add函数会在栈内开辟一部分空间
1、将main函数的ebp保存起来压栈(以便调用完Add函数后可以返回到main函数的底部)。
2、将新的ebp放在栈顶(Add函数堆栈的底部)
3、为Add函数预开辟一片空间大小为CCh字节
4、在顶部继续压三个栈ebx,esi,edi。
最后将为Add开辟的CCh字节大小的空间全部初始化为全C。 - 开始执行函数体
1、定义z,[ebp-8]即ebp向上数第8个字节处空间为z=0。
2、将[ebp+8]也就是ebp向下数第8个字节处,也就是a的值20,赋值给寄存器eax。
3、将[ebp+0Ch]也就是ebp向下数第12个字节处,也就是b的值加到eax里,也就是将10和20加起来放到寄存器eax中。
4、将eax赋值给[ebp-8],也就是z。
5、将z的值赋值给寄存器eax(将z的值保存起来)。
开始销毁函数体
1、弹出edi,esi,ebx三个压栈。
2、将esp移动到ebp位置,此时Add函数直接全部弹出(调用完毕)
3、弹出ebp,因为当初保存的main函数的ebp,所以此时ebp返回到main底部。ret弹出刚才保存的调用函数的地址,main下面的栈空间。
Add函数完成销毁。
新的esp、ebp如下:
回到主函数main
1、esp加8,esp向下移8个字节。也就是将ecx,eax弹出。即a跟b的临时拷贝(形参)移出。
2、将Add函数内部保存的eax的值赋给[ebp-20],也就是将Add函数计算好的值返回给ret。
结果ret计算完成。
- 下面放一个整体的图
函数栈帧部分内容在不同编译器上出现的结果不同,但是思想都是一致的。小弟用vs2008献丑一把,望大神指导。