函数调用约定(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 函数后,并没有清理栈。