c语言中函数调用惯例

调用惯例(Calling Convention):函数的调用方和被调用方对于函数如何调用需要有一个明确的约定,只有双方都遵守同样的约定,函数才能被正确的调用。

  调用惯例一般会涉及到一下三个方面:

1 函数参数传递的顺序与方式

  函数传递参数的方式有很多中,可以通过寄存器、栈和内存区域传递,不过最常见的是通过栈传递。函数的调用方先将参数压入栈中,函数自己再从栈中取出参数。对于有多个参数的函数,调用惯例要规定调用方将函数压入栈的顺序:是从左到右,还是从右到左。有些惯例还允许通过寄存器传递参数,以提高性能。

2 栈的维护方式

  在函数压入栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个工作既可以由调用方来做,也可以由函数来做。

3 名字修饰

  为了链接的时候对调用管理进行区分,调用管理要对函数本身的名字进行修饰。不同的调用管理有不同的名字修饰策略。

  这里我们主要介绍的cdecl、stdcall、fastcall三种c中主要的调用惯例,还有pascal、naked call、thiscall调用管理。这三种调用的区别如下:

调用惯例 出栈方 参数传递 名字修饰
cdecl 函数调用方 从右至左压入栈 下划线+函数名
stdcall 函数本身 从右至左压入栈 下划线+函数名+@+参数的字节数
fastcall 函数本身

头两个DWORD(4字节)类型或者占更少

字节的参数被放入寄存器,其它剩下的参数

按从右至左的顺序压入栈

@+函数名+@+参数的字节数

  下面用一个实际的例子来看看这些调用方式具体是怎么实现的:

 1 #include <stdio.h>
 2 
 3 void __attribute__ (( cdecl))
 4 a(int a, int b, int c) 
 5 {
 6     char buffer1[5];
 7     char buffer2[10];
 8 }
 9 
10 int main ( int argc, char *argv[] )
11 {
12     a(1, 2, 3);
13     return 0;
14 }                /* ----------  end of function main  ---------- */

 

  编译上面的代码,然后反汇编看下main函数和a函数的汇编代码:

 

  从反汇编的代码中可以看出main函数调用a时,参数是通过栈传输的,并且是从右至左向栈中压。a函数并没有维护栈,但是main函数貌似也没有维护栈,其实不然,main函数是用mov指令代替了push指令,所以esp的值并没有改变,也就不必维护了。不过如果用push,那就要维护esp的值,在编译时加上“-mno-accumulate-outgoing-args”选项就可以看到这种情况。这种调用惯例是gcc默认的,也就是cdecl惯例。

  如果把上面代码中的第3行的cdecl换成stdcall,情况又会是怎样的呢?我们反汇编看下:


  确实可以在a函数中看到它用ret指令维护了堆栈。不过对于用mov实现栈参数压入的main来说却反而要维护esp了,由于a中让esp减了0xc,所以回到main中后就必须回复esp的值,这也是为什么call a后main中将esp加了0xc。

  如果把上面代码中的第3行的cdecl换成fastcall,情况又会是怎样的呢?我们反汇编看下:

  

  从main中可以看出,调用a函数的前两个参数分别通过ecx和edx传送,最后一个参数通过栈传送。a函数也维护了栈。

  pascal调用惯例是将参数从左至右传送,函数自己维护栈,函数命名比较的复杂,貌似gcc通过前面的方式设置这种惯例不行。

 

posted @ 2013-05-28 16:51  在于思考  阅读(2497)  评论(0编辑  收藏  举报