这标题一念出来我立刻想到了一个名人:白素贞……当然,此女与本文无关,下面进入正题:
其实程序运行就好比一帧一帧地放电影,每一帧是一次函数调用,电影放完了,我们就看到结局了。
我们用一个递归求解阶乘的程序来看看这个放映过程(fac.c):
#include <stdio.h> int fac(int n) { if(n <= 1) return 1; return n * fac(n-1); } int main() { int n = 3; int ans = fac(n); printf("%d! = %d\n", n, ans); return 0; }
首先 main 函数被调用(程序可不是从 main 开始执行的):
|
main 函数创建了一帧:
进入 main 函数,前 4 条指令开辟了这片空间,在退出 main 函数之前的 leave ret 回收了这片空间(C++ 在回收这片空间之前要析构此函数中的所有局部对象)。在 main 函数执行期间 ebp 一直指向 帧顶 - 4 的位置, ebp 被称为帧指针也就是这个原因。
调用函数的时候,先传参数,然后 call,具体这个过程怎么实现有相关规定,这样的规定被称为调用惯例, C语言中有多种调用惯例,它们的不同之处在于:
各种调用惯例《程序员的自我修养》——链接、装载与库 这本书中有简要介绍,我照抄后在本文后面列出。C语言默认的调用惯例是 cdecl:
可以从 printf("%d! = %d\n", n, ans); 的调用过程中看出。
虽然 VC、gcc 都默认使用 cdecl 调用惯例,但它们的实现却各有风格:
说完调用惯例我们接着来看第一次调用 fac:
fac(3) 开辟了第一个 fac 帧:
这时还不满足递归终止条件,于是fac(3)又递归地调用了fac(2), fac(2)又递归的调用了fac(1),到这个时候栈变成了如下情况:
上图的箭头的含义很明显: 从 ebp 可回溯到所有的函数帧,这是由于每个函数开头都来两条 pushl %ebp、movl %esp, %ebp造成的。
参数总是调用者写入,被调用者来读取(被调用者修改参数毫无意义),这是一种默契^_^。
程序继续运行:
最终程序结束(进程僵死,一会儿后操作系统会来收尸(回收内存及其他资源))。
函数帧保存的是函数的一个完整的局部环境,保证了函数调用的正确返回(函数帧中有返回地址)、返回后继续正确地执行,因此函数帧是 C语言 能调来调去的保障。
调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
---|---|---|---|
cdecl | 函数调用方 | 从右至左的顺序压参数入栈 | 下划线+函数名 |
stdcall | 函数本身 | 从右至左的顺序压参数入栈 | 下划线+函数名+@+参数的字节数, 如函数 int func(int a, double b)的修饰名是 _func@12 |
fastcall | 函数本身 | 头两个 DWORD(4字节)类型或者更少字节的参数 被放入寄存器,其他剩下的参数按从右至左的顺序入栈 | @+函数名+@+参数的字节数 |
pascal | 函数本身 | 从左至右的顺序入栈 | 较为复杂,参见pascal文档 |