Linux下常用的函数调用栈规范

来源:互联网 发布:网络舆情内参是什么 编辑:程序博客网 时间:2024/05/06 23:20

我们都应该知道,高级语言的函数调用过程中,有“栈”这么一个概念,被调用函数的局部变量是存放在栈中的,函数调用的参数也是通过栈传递的。那么,调用函数是怎么把各种数据压入栈中,被调用函数又是怎么对栈进行操作以获取必要的数据呢?函数调用发生完毕之后,谁又负责清理这个栈?这就用到了函数调用栈规范

函数调用栈规范是指编译器的一中“约定”,他规定了调用者如何传递参数,被调用者如何获取参数,调用完成后怎么清理栈,怎么传递返回值等。编译器在编译程序的时候遵循这种规范,从而使程序正确的执行。对于不同的编译器,不同的高级语言,这种规范是不尽相同的。

在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   $jpushl   $ipush    $addr //addr为第一个参数的语句的地址call     printfaddl    $0x10, %esp

或者翻译成:

sub    $0x10, %esp //先申请栈空间mov   $addr, (%esp)mov   $i, 0x4(%esp)mov   $j, 0x8(%esp)mov   $k, 0xc(%esp)call    printfaddl   $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传递。

0 0
原创粉丝点击