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

来源:互联网 发布:6寸windows平板 编辑:程序博客网 时间:2024/05/19 13:43

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

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

在X86平台下的Linux内核中,常用的函数调用规范有:C,FastCall,Pascal等,下面简单介绍下这几种规范:

1、C规范

规定调用者将参数从右至左压栈,返回值通过EAX寄存器传递,如果返回值超过32位,则使用EDX:EAX传递,最后由调用者负责清理栈。这个规范被大多数C编译器遵守。

我们需要注意的是,由调用者负责清理堆栈,可以支持变参函数,比如我们所熟悉的printf函数。让我们看个例子:

[cpp] view plain copy
print?
  1. printf(“%d, %d, %d\n”, i, j, k);  
printf("%d, %d, %d\n", i, j, k);

这个语句的目的是调用一个printf函数。被调用的函数在编译的时候并不知道自己将要被传递多少个函数,但是调用者知道。这个语句返汇编后大概的样子如下:

[cpp] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. pushl   k&nbsp;&nbsp;</span><span class="comment">//伪汇编,将k入栈</span><span>&nbsp;&nbsp;</span></span></li><li class=""><span>pushl&nbsp;&nbsp;&nbsp;j  
  2. pushl   i&nbsp;&nbsp;</span></li><li class=""><span>push&nbsp;&nbsp;&nbsp;&nbsp;addr //addr为第一个参数的语句的地址  
  3. call     printf  
  4. addl    0x10,&nbsp;%esp&nbsp;&nbsp;</span></li></ol><div class="save_code tracking-ad" data-mod="popu_249"><a href="javascript:;" target="_blank"><img src="http://static.blog.csdn.net/images/save_snippets.png"></a></div></div><pre code_snippet_id="533404" snippet_file_name="blog_20141126_2_4735559" name="code" class="cpp" style="display: none;">pushlk //伪汇编,将k入栈pushl jpushlipush addr//addrcallprintfaddl0x10, %esp

    或者翻译成:

    [cpp] view plain copy
    print?在CODE上查看代码片派生到我的代码片
    1. sub    0x10,&nbsp;%esp&nbsp;</span><span class="comment">//先申请栈空间</span><span>&nbsp;&nbsp;</span></span></li><li class=""><span>mov&nbsp;&nbsp;&nbsp;addr, (%esp)  
    2. mov   i,&nbsp;0x4(%esp)&nbsp;&nbsp;</span></li><li class=""><span>mov&nbsp;&nbsp;&nbsp;j, 0x8(%esp)  
    3. mov   k,&nbsp;0xc(%esp)&nbsp;&nbsp;</span></li><li class=""><span>call&nbsp;&nbsp;&nbsp;&nbsp;printf&nbsp;&nbsp;</span></li><li class="alt"><span>addl&nbsp;&nbsp;&nbsp;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”,然后根据这个参数确定其他参数的个数,并向高地址寻找即可。

    从上面的汇编代码可以看出,

    [cpp] view plain copy
    print?在CODE上查看代码片派生到我的代码片
    1. addl   0x10,&nbsp;%esp&nbsp;&nbsp;</span></span></li></ol><div class="save_code tracking-ad" data-mod="popu_249"><a href="javascript:;" target="_blank"><img src="http://static.blog.csdn.net/images/save_snippets.png"></a></div></div><pre code_snippet_id="533404" snippet_file_name="blog_20141126_4_9796279" name="code" class="cpp" style="display: none;">addl0x10, %esp

      这条指令是负责清理栈的,他位于调用者种,他知道自己应该清理多大的栈。而在被调用者中,往往通过

      [cpp] view plain copy
      print?在CODE上查看代码片派生到我的代码片
      1. ret   n&nbsp;&nbsp;</span></span></li></ol><div class="save_code tracking-ad" data-mod="popu_249"><a href="javascript:;" target="_blank"><img src="http://static.blog.csdn.net/images/save_snippets.png"></a></div></div><pre code_snippet_id="533404" snippet_file_name="blog_20141126_5_7571432" name="code" class="cpp" style="display: none;">retn这条指令来清理栈的,因为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
原创粉丝点击