函数调用约定(stdcall cdecl thiscall fastcall)

引子

一位朋友在使用函数指针时出现了一个错误:这个函数指针 FP 要求有 4 个参数,而他将一个只有 3 个参数的函数作为 FP 使用,编译和运行都没有报错,但这样造成的运行结果可能是不正确的。下面一个例子来重现这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef int(*FunPt)(int a, int b, int c, int d);

int add(int a, int b, int c)
{
return a + b + c;
}

int main()
{
cout << ((FunPt)(add))(1, 2, 3, 4) << endl;

return 0;
}

如上例,这两个函数的签名不同。 FunPt 要求 4 个参数,而函数 add 只有 3 个参数。虽然在编译和运行时都没有报错,但毫无疑问,返回的结果是错误的。为什么会出现这样的错误呢,这要从函数的调用约定说起。

__cdecl 调用约定

从 C 语言时代开始就有了这个调用约定。它又称 C调用约定,是 C 程序默认的调用约定(现在也是 C++ 程序默认的调用约定)。在这种约定下,函数参数从右向左入栈,函数堆栈由调用者清理,所以它允许函数参数的个数不确定,且它生成的可执行文件大小 会比 __stdcall 函数大。
按C编译方式,_cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。

__stacall 调用约定

它是 Pascal 程序的默认调用方式,所以又称 Pascal 调用约定。和 __decel 一样,参数是从右到左入栈的方式,不同的是堆栈将由被调用函数来清理。
按C编译方式,_stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数

__fastcall 调用约定

它通 CPU 寄存器传递参数,所以调用较快。这也是为什么叫 “fastcall” 的原因。
按C编译方式,_fastcall调用约定在输出函数名前面加“@”符号,后面加“@”符号和参数的字节数

__thiscall 调用约定

它是 C++ 成员函数是默认调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理。如果参数个数确定,this指针通过ecx传递给被调用者,函数自身清理栈;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈,由调用者清理栈。它的参数也是从右向左入栈的。

比较

下面来比较 cdecl 和 stdcall 两种方式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

int __cdecl cAdd(int a, int b)
{
return a + b;
}

int __stdcall sAdd(int a, int b)
{
return a + b;
}

int main(void)
{
int s1 = cAdd(1, 2);
int s2 = sAdd(3, 4);
return 0;
}

借助 Visual Studio ,我们查看汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void)
{
//......
int s1 = cAdd(1, 2);
00FA17BE push 2
00FA17C0 push 1
00FA17C2 call cAdd (0FA1154h)
00FA17C7 add esp,8 ;清理栈
00FA17CA mov dword ptr [s1],eax
int s2 = sAdd(3, 4);
00FA17CD push 4
00FA17CF push 3
00FA17D1 call sAdd (0FA1316h)
00FA17D6 mov dword ptr [s2],eax
return 0;
00FA17D9 xor eax,eax
}

可以看出:main 在调用由 __cdecl 标记的 cAdd 函数后,清理了栈,而调用由 __stdcall标记的 sAdd 函数后,并没有清理栈。