本文链接:C语言的三种函数调用方式
C语言提供了三种调用函数的方式:cdecl,stdcall和fastcall。cdecl是标准的C语言调用函数的方式(C declared);stdcall是大多数链接库中使用的调用方式,比如Windows API,JNI API等等;fastcall顾名思义,就是说它调用起来比较快。这三种方式在C语言编写中几乎没有任何差别,但对应了不同的底层实现。
要使用这三种调用函数的方法,需要在函数声明和定义时写明__cdecl(可不写),__stdcall,__fastcall。注意每个词前面是两个下划线符号。
下面来一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdio.h> int cdecl_func(int a, int b, int c) { return a + b + c; } int __stdcall stdcall_func(int a, int b, int c) { return a + b + c; } int __fastcall fastcall_func(int a, int b, int c) { return a + b + c; } int main() { printf("%d\n", cdecl_func(1, 2, 3)); printf("%d\n", stdcall_func(1, 2, 3)); printf("%d\n", fastcall_func(1, 2, 3)); } |
cdecl的调用方式是由调用方控制参数栈,被调用的函数在返回时不需要对自己使用的栈进行清理,所以cdecl_func编译出来的汇编代码如下:
1 2 3 4 |
movl 8(%esp), %eax addl 4(%esp), %eax addl 12(%esp), %eax ret |
调用方的代码如下:
1 2 3 4 |
movl $3, 8(%esp) movl $2, 4(%esp) movl $1, (%esp) call _cdecl_func |
可以看出其采用了放置参数到栈上,然后调用的方式,可以避免清理栈的问题。
stdcall和cdecl不同,是由被调用函数来清理参数栈的,所以编译出来的函数最后一句不同,表示返回的同时清理12字节的栈空间:
1 2 3 4 |
movl 8(%esp), %eax addl 4(%esp), %eax addl 12(%esp), %eax ret $12 |
同时,调用时采用压栈的方式:
1 2 3 4 |
pushl $3 pushl $2 pushl $1 call _stdcall_func@12 |
这里有一点,stdcall的函数在gcc编译器下生成的标识符名会加“@参数字节数”,有时候会在(动态)链接时引起“找不到标识符”错误。通过加入-Wl,--kill-at
可以移除“@”,从而修复错误,在此不作详述。
fastcall通过使用寄存器传参,达到快速调用的目的,使用的寄存器为ecx和edx,如果有多于两个的参数,则使用stdcall的方式进行栈传参:
1 2 3 4 |
addl %edx, %ecx movl %ecx, %eax addl 4(%esp), %eax ret $4 |
1 2 3 4 |
pushl $3 movl $2, %edx movl $1, %ecx call @fastcall_func@12 |
gcc下fastcall函数的命名方式更加不同,生成的标识符是以“@”开头,而不是下划线“_”。
至此,就介绍完毕C语言中三种不同的调用方式。在汇编编程的时候,最常用的往往是stdcall,而且大部分API库也都用了这种方式。但是cdecl有其独特的优势,就是可以支持不定长的参数,比如printf。在gcc中,如果给一个stdcall函数设置不定长参数,那么编译后的结果就会失去stdcall的属性,实际上成为一个cdecl函数。至于fastcall,可以在一些需要快速传参的地方使用,但是很多时候还是使用内联函数更好一些。