我们都应该知道,高级语言的函数调用过程中,有“栈”这么一个概念,被调用函数的局部变量是存放在栈中的,函数调用的参数也是通过栈传递的。那么,调用函数是怎么把各种数据压入栈中,被调用函数又是怎么对栈进行操作以获取必要的数据呢?函数调用发生完毕之后,谁又负责清理这个栈?这就用到了函数调用栈规范!
函数调用栈规范是指编译器的一中“约定”,他规定了调用者如何传递参数,被调用者如何获取参数,调用完成后怎么清理栈,怎么传递返回值等。编译器在编译程序的时候遵循这种规范,从而使程序正确的执行。对于不同的编译器,不同的高级语言,这种规范是不尽相同的。
在X86平台下的Linux内核中,常用的函数调用规范有:C,FastCall,Pascal等,下面简单介绍下这几种规范:
1、C规范
规定调用者将参数从右至左压栈,返回值通过EAX寄存器传递,如果返回值超过32位,则使用EDX:EAX传递,最后由调用者负责清理栈。这个规范被大多数C编译器遵守。
我们需要注意的是,由调用者负责清理堆栈,可以支持变参函数,比如我们所熟悉的printf函数。让我们看个例子:
printf("%d, %d, %d\n", i, j, k); |
这个语句的目的是调用一个printf函数。被调用的函数在编译的时候并不知道自己将要被传递多少个函数,但是调用者知道。这个语句返汇编后大概的样子如下:
pushl $k //伪汇编,将k入栈 pushl $j pushl $i push $addr //addr为第一个参数的语句的地址 call printf addl $0x10, %esp |
或者翻译成:
sub $0x10, %esp //先申请栈空间 mov $addr, (%esp) mov $i, 0x4(%esp) mov $j, 0x8(%esp) mov $k, 0xc(%esp) call printf addl $0x10, %esp |
实际上,第二种翻译和第一种完成的功能基本一致,都是越右边的参数越靠近栈底,因为栈是从高地址往低地址增长的,所以地址也就越高。printf函数被调用后,先读栈顶的参数,也就是"%d, %d, %d\n",然后根据这个参数确定其他参数的个数,并向高地址寻找即可。
从上面的汇编代码可以看出,
addl $0x10, %esp |
这条指令是负责清理栈的,他位于调用者种,他知道自己应该清理多大的栈。而在被调用者中,往往通过
ret $n |
这条指令来清理栈的,因为printf在编译时并不知道自己会被传递多少个参数,因此这个n也就不能确定,所以没办法在被调用者中清理栈。
2、FastCall
顾名思义,fastcall就是快速调用的意思。因为压栈操作必须要访存,对于一些经常被调用的参数简单的小函数来说会有一定的开销,因此可以通过寄存器传参。以gcc为例,gcc使用fastcall的时候,默认从左到右的前两个参数通过ECX和EDX两个寄存器来传递,其他参数使用栈传递,但是可以通过__attribute__((regparm(n)))来控制可使用寄存器的数目,例如regparm(3)就表示前三个参数使用寄存器来传递,默认寄存器是EAX,ECX,EDX。返回值传递和栈清理同C规范相同。
这种规范应该是对C规范的一种补充和优化,在linux内核中经常用到,比如系统调用。
3、Pascal
参数从左到右入栈,被调用者负责清理栈,返回值通过EAX或EDX:EAX传递。
[...] C语言之所以可以支持可变参数函数,一个重要的原因是C调用规范中规定C语言函数调用时,参数是从右向左压入栈的;这样一个函数实现的时候,就无需关心调用他的函数会传递几个参数过来,而只要关心自己用到几个;以printf为例: printf("%d%sn",i,s); [...]