我们平时敲代码可以发现:当我们有一个清晰的思路时,编程就仅仅只是在框架内往其“添砖加瓦”。同一个道理,当我们清楚了解一段代码每个函数的开辟、调用、运行、返回时,那我们学习语言时往往事半功倍。所以,我们需要掌握函数的调用过程。
下面用Add函数为例,阐述函数的调用过程。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include <stdlib.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
printf("sum=%d\n", sum);
system("pause");
return 0;
}
当程序调试但main()没被调用时
我们使用【调试-调用堆栈】可以发现:在main()函数执行前计算机首先执行了两个函数:
既然执行函数,就要为这两个函数开辟空间。(注意:此时main()还没执行)
在这里为了方便理解首先介绍两个概念:
- 函数栈帧。我们使用函数时要为函数开辟栈空间,用于本次函数的调用中临时变量的保存、保护现场。这块栈空间我们称之为函数栈帧。
- ebp,esp寄存器。在函数调用过程中两个寄存器存放了维护这个栈的栈底和栈顶指针。
现在给 mainCRTStartup 函数开辟空间(__tmainCRTStartup函数同理,在此不做介绍)
继续执行代码,进到main(),我们把c代码反汇编:
第一行:push ebp 把ebp压到栈里面去(ebp存的是为mainCRTStartup开辟的地址)
第二行:把esp的值给ebp (esp、ebp指向同一地址)
第三行:esp地址减4Ch (esp地址变小,由于栈空间由高地址指向底地址,esp向上移动,代表为main函数预开辟空间)
第四、五、六行:压栈3次,在栈顶放3个元素
第七行:lea 加载有效地址
第八、九、十行:从edi(地址ebp-4Ch)开始向下拷贝eax(CCCC)的内容到ptr,拷贝ecx(13h)次,每次拷贝dword(双字,4个字节)个空间。
第一行:将局部变量a放到 (当前函数)栈底地址中
第二行:将局部变量b放到 (当前函数)栈底地址-4中
第三、四行:将形参b放到eax中,且压栈
第五、六行:将形参a放到ecx中,且压栈
第七行:call调用函数 (压栈地址 —— call指令下一条指令的地址)
执行call指令之后,按两下f11,跳转到Add函数:
过程创建跟main()函数一样。事实上,每个函数过程都是如此。
第一行:将局部变量z放到 (当前函数)栈底地址中
第二行:将地址ebp+8内的内容(即形参b)赋值给eax
第三行:获取形参a和b相加,结果赋给eax
第四行:将eax赋值给z
第五行:将z的值保存在eax中
函数的返回值通过寄存器返回。到此,函数调用完,开始销毁。
第一、二、三行:pop三次,出栈三次
第四行:由于ebp是当前函数栈底,所以将ebp赋值给esp,使其直接销毁当前函数,跳到上一级函数的栈底处
第五行:pop ebs出栈 将出栈的内容保存到ebp,回到main函数的栈帧(Add函数销毁)
第六行:ret指令会使得出栈一次,并将出栈的内容当做地址。将程序执行跳转到该地址处。
函数返回,返回到add这一行
add+8相当于将两个形参a,b销毁,生命周期结束。
将eax(之前存的是z的返回值)赋值给ret。
以上就是整个代码运行时函数的调用过程。
注:函数开始执行程序代码时,这个时候程序将使用一个运行时堆栈(函数栈帧stack),存储函数的局部变量和返回地址。