我们知道,一个C程序由若干个函数组成,C程序的执行实际上就是函数之间的相互调用。请看下面的代码:
#include <stdio.h>
void funcA(int m, int n)
{
printf("funcA被调用\n");
}
void funcB(float a, float b)
{
funcA(100, 200);
printf("funcB被调用\n");
}
int main()
{
funcB(19.9, 28.5);
printf("main被调用\n");
return 0;
}
main() 调用了 funcB(),funcB() 又调用了 funcA()。对于main() 调用 funcB(),我们称 main() 是调用方,funcB() 是被调用方;同理,对于 funcB() 调用 funcA(),funcB() 是调用方,funcA() 是被调用方。
函数的参数(实参)由调用方压入栈中供被调用方使用,它们之间要有一致的约定。例如,参数是从左到右入栈还是从右到左入栈,如果双方理解不一致,被调用方使用参数时就会出错。
以 funcB() 为例,假设 main() 函数先将 19.9 入栈,后将 28.5 入栈,但是 funcB() 在使用这些实参时却认为 28.5 先入栈,19.9 后入栈,那么就一定会产生混乱,误以为19.9 是传递给 b、28.5 是传递给 a 的。
所以,函数调用方和被调用方必须遵守同样的约定,理解要一致,这称为调用惯例(Calling Convention)。
1、一个调用惯例一般规定以下两方面的内容:
2、函数参数的传递方式,是通过栈传递还是通过寄存器传递(这里我们只讲解通过栈传递的情况)。
3、函数参数的传递顺序,是从左到右入栈还是从右到左入栈。
4、参数弹出方式。函数调用结束后需要将压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由调用方来完成,也可以由被调用方来完成。
函数名修饰方式。函数名在编译时会被修改,调用惯例可以决定如何修改函数名。
在C语言中,存在多种调用惯例,可以在函数声明或函数定义时指定,例如:
#include <stdio.h>
int __cdecl max(int m, int n);
int main()
{
int a = max(10, 20);
printf("a = %d\n", a);
return 0;
}
int __cdecl max(int m, int n)
{
int max = m>n ? m : n;
return max;
}
函数调用惯例在函数声明和函数定义时都可以指定,语法格式为:
返回值类型 调用惯例 函数名(函数参数)
在函数声明处是为调用方指定调用惯例,而在函数定义处是为被调用方(也就是函数本身)指定调用惯例。
__cdecl是C语言默认的调用惯例,在平时编程中,我们其实很少去指定调用惯例,这个时候就使用默认的 __cdecl。
注意:__cdecl 并不是标准关键字,上面的写法在 VC/VS 下有效,但是在 GCC 下,要使用 __attribute__((cdecl))
除了 cdecl,还有其他调用惯例,请看下表:
调用惯例(Calling Convention):
函数的调用方和被调用方对于函数如何调用需要有一个明确的约定,只有双方都遵守同样的约定,函数才能被正确的调用。
调用惯例一般会涉及到一下三个方面:
1、函数参数传递的顺序与方式
函数传递参数的方式有很多中,可以通过寄存器、栈和内存区域传递,不过最常见的是通过栈传递。函数的调用方先将参数压入栈中,函数自己再从栈中取出参数。对于有多个参数的函数,调用惯例要规定调用方将函数压入栈的顺序:是从左到右,还是从右到左。有些惯例还允许通过寄存器传递参数,以提高性能。
2、栈的维护方式
在函数压入栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个工作既可以由调用方来做,也可以由函数来做。
3、名字修饰
为了链接的时候对调用管理进行区分,调用管理要对函数本身的名字进行修饰。不同的调用管理有不同的名字修饰策略。
这里我们主要介绍的cdecl、stdcall、fastcall三种c中主要的调用惯例,还有pascal、naked call、thiscall调用管理。
下面用一个实际的例子来看看这些调用方式具体是怎么实现的:
#include <stdio.h>
void __attribute__ (( cdecl))
a(int a, int b, int c)
{
char buffer1[5];
char buffer2[10];
}
int main ( int argc, char *argv[] )
{
a(1, 2, 3);
return 0;
}
/* ---------- 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通过前面的方式设置这种惯例不行。