从汇编看c语言函数调用
学C语言时,就听老师说函数调用时是通过栈来记录信息,又听说什么“保留现场”,"恢复现场"一些既听不懂,也不知道怎么弄懂的东西。最近正在学习Linux下的汇编,现在就通过一个简单的例子来展示一下汇编级的函数调用,这样能够增加大家对C语言的理解。虽然并不是很完善,但是足够阐明函数调用的思想。
在Linux下通过命令gcc -S functest.c,可以生成汇编程序functest.s
//functest.c
#include <stdio.h>
void func(int a, int b)
{
int c;
int d;
c = a;
d = b;
}
int main()
{
func(2, 3);
return 0;
}
在Linux下通过命令gcc -S functest.c,可以生成汇编程序functest.s
首先可以看到一点,一个简单的C程序的汇编却是如此之长,可以想到良好的程序书写习惯有多大的好处。
言归正传,我们不分析每条指令都是什么意思,我们重点看函数调用那一部分。
我们注意在调用call func之前,第25,26,27行是为func准备参数。首先把%esp减去8个字节,这是因为在Linux中int型为4个字节(想想谭浩强C语言程序设计书中说int为2个字节?这东西依赖于具体的操作系统和编译器),即手动修改栈指针,可以发现C语言中函数参数是保存在栈中的,进一步,我们发现gcc把3先压入栈中,之后是2,可以说明,C中函数参数的压栈顺序和函数书写顺序正好相反。执行完这3条指令后,栈中的内容如下:
之后28行调用func,通过call指令。执行call指令时,不仅仅去调用func函数,而且做了一个我们程序员看不到的动作,就是把下一条指令的地址压入到栈中。这点我们稍后会讲到,而且从这我们也能略窥到,函数名只不过是地址的别名,只不过是为了方便程序员理解,我们可以完全用16进制地址取代这些别名,当然计算机并不觉得用地址比用别名更困难。
现在我们进入func。6,7行的两条指令可以认为是套路,具体原因可以见[注1]。先把%ebp的值压入栈,之后把栈顶赋给%ebp,隐约我们可以猜到,因为在这里修改了%ebp,而又不想原来的%ebp被覆盖,那么只好先把原来的%ebp存储到栈中,必要的时候可以在栈中恢复原来的%ebp。第8行,将栈顶向下移,这个操作的目的是为函数中的临时变量在栈中分配存储空间。有人会想为什么不用其它的内存或者寄存器去存储临时变量?这是因为如果为大型项目编写程序,要跟踪哪些变量使用内存,而哪些使用寄存器简直就是恶梦,所以C中用栈存储临时变量,因为在栈在函数调用后会释放或者清空,那么这些变量是不可能被其它函数所调用,故这也是“局部变量”的由来。这个行为也使程序员在函数中要对操作得非常小心,例如我们在函数中开了一个很大的数组,那么在为这个数组开辟栈空间时,很可能出现段错误。执行完6,7,8三行后,栈中的内容如下:
9,10行是把3赋值给d,11,12是把2赋值给c。那么栈中的内容确定下来如下:
可以看到,有两个为局部变量开辟的占空间实际并没有被使用到。
有人现在有疑问那个标记”XXXX”的单元式干什么用的,现在我来告诉你那个其实就是函数的返回地址,记得我前面说过,call指令会把调用函数后的下一条指令地址入栈,那么那个地址就存在XXXX那个单元中,这样执行完函数时可以根据栈中的这个地址返回到主调用程序,继续运行。那么完整的栈内容如下:
这样函数基本执行完毕,leave是恢复调用func之前的栈内容和%ebp内容,完全可以用以下指令替代:
movl %ebp, %esp
popl %ebp
ret 指令就是返回到主程序,不赘述。
30行为主程序中从栈中删除调用func时的参数2,3。
这样一个函数的完整调用过程就给大家展示完了,希望对大家有所帮助。本人刚学汇编,如有错误,还望大家指出。
注1:这样做,是为了对函数参数容易进行读取。在设计之初,考虑到用根据%esp和偏移量来对栈中的参数进行访问,但是由于在函数中%esp极有可能改变,那么维护这个偏移量也变的复杂,所以利用%ebp记录进入函数时栈指针的位置,而%ebp在这个函数中不会被改变,那么通过%ebp去定位函数参数就变的非常容易,所以一般有人把%ebp也称为栈底指针,而这也成为了汇编程序员的习惯。