TOMORROW 星辰 , 版权所有丨原文链接:C 语言函数调用过程图解——内存视角
相信很多编程新手村的同学们都会有一个疑问:C 语言如何调用函数的呢?局部变量的作用域为什么仅限于函数内?这个调用不是指 C 语言上的函数调用的语法,而是在内存的视角下,函数的调用过程。本文将从 C 语言调用实例,内存视角,反汇编代码来探讨 C 语言函数的调用过程,也可以说是C 语言函数调用过程图解。通过这个C 语言函数调用过程图解,同学们将会知道,C 语言函数在调用时,内存空间是怎样变化的。
要想理解这一个过程还好涉及到函数栈帧的概念。函数栈帧指的是,在调用函数时,系统在栈空间中给函数所分配的一段连续空间。其中 ebp(栈帧基址指针)则是指明了当前函数的栈帧基地址,对函数的资源(局部变量、实参等)的访问,都要通过 ebp+offset(偏移量)来进行访问。而 esp 则是栈指针,指示当前栈空间栈顶的位置。
以下代码即是此次探讨 C 语言函数调用过程的实例源码:
int subFunc(int abc)
{
int def=0x9999;
abc=0x8888;
return abc;
}
int _tmain()
{
subFunc(0x2222);
return 0;
}
源码很简单,在一个主函数中,调用一个带参数的子函数。源码使用 Visual Studio2010 进行调试,并同时查看内存窗口、反汇编窗口及变量窗口。
进入调试模式,并将断点定在调用子函数 subFunc()处,然后运行并观察。
通过观察窗口,可以知道,此时还是在执行 main 函数,而 ebp(栈帧基址指针)指向的是 0x0073fb64,esp=0x0073fa98。从反汇编代码可以看到,在调用函数前,需要先将参数压栈,也就是将实参存到了 0x0073fa94 处,然后再调用到子函数。
进入到子函数时,esp 已经变成了 0x0073fa8c,而 0x0073fa90 处存放的是,子函数执行完后返回到 main 函数中的地址。进入到子函数后,先将 main 函数的 ebp 压栈,然后将当前栈顶指针的值赋值给 ebp 作为当前子函数的 subFunc()的栈帧基址指针。此时 esp 和 ebp 都变成了 0x0073fa8c。
紧接着,可以看到,esp 一下子被减去了 0x0cch,也就是说栈空间一下子增长了 0x0cch,并且这段空间全部被赋值为 0xcc。再往下看,可以看到子函数中的局部变量被分配在了 0x0073fa84 处(因为变量是 32 位的,然后 CPU 却是 64 位的,所以空了 32 位不作使用),也就是说,这一段被初始化为 0xcc 的栈空间是被用来给局部变量分配空间的。
接下来再看,在 main 函数传递了一个实参 0x2222 给子函数 subFunc 中的形参 abc。在对 abc 进行读写时,其实就是在对前面实参所被存储的空间进行读写,也就是说形参在作为参数也作为局部变量的同时,它所被分配的内存空间是在函数栈帧基址 ebp 之下。
而子函数被执行完后,返回的过程则是一个与上面过程相逆的过程。将相应的数据出栈,恢复 ebp 等信息,释放子函数的栈空间,返回到主函数。所以局部变量的作用域只是在函数中,当函数被执行完返回时,函数的栈帧都被释放了,局部变量等数据也就没有了,不存在了,也就是说局部变量的生命周期是与函数的生命周期等同的。
经过以上的 C 语言函数调用过程图解,相信已经理解了 C 语言在内存中是如何调用的了。然后可以总结并得出下面的函数调用的栈帧图解。从函数调用的层面看,栈空间是被从下往上一块一块地增长的,并且是后分配的先被释放,先分配的后被释放。