调用惯例(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通过前面的方式设置这种惯例不行。