在windows平台上的C++编程中经常会看到一些__stdcall, __cdecl, WINAPI, CALLBACK等等关键字在函数前面,在.NET中还有__clrcall, __thiscall等关键字,有时加不加它们都可以,但是有时必须加上,不然编译不过。本文要讨论的就是这些关键字:调用约定(Calling Convention),有时也叫做“函数调用约定”或者“调用规范”。本文采用MSDN的官方翻译:“调用约定”。
那什么是调用约定呢?首先让我们看看一个函数调用到底需要经历哪几个过程,编译器到底为我们做了些什么。
1. 把函数的参数压栈或者储存到寄存器
2. 跳转到函数
3. 把函数使用到的一些寄存器压栈
4. 执行函数
5. 处理函数返回值
6. 对于第3步中压栈的那些寄存器,恢复它们原来的值
7. 根据不同的调用约定,清除第1步中压栈的参数,然后返回,或者先返回然后清除。
可以看到第6步是第3步的逆操作,而第7步是第1,2步的逆操作,调用约定主要是定义了第1,7步骤中的规则:怎么去传递参数,谁负责去清除栈上的参数。
在正式开始介绍各种调用约定之前,有必要说明一下:这些调用约定是和编译器相关的,所以这些关键字前都有两个下划线,不同的编译器有不同的实现。比如VC和C++ Builder对于__fastcall的定义很不一样,以至于C++ Builder引入了__msfastcall关键字来和VC的__fastcall兼容。本文将要介绍的是VC的各种调用约定,文中所有的代码在Windows 2003, Visual Studio2005中测试通过,反编译工具使用的是VS2005和WinDbg。(代码被编译成debug版本。因为在release版本中,编译器会作代码优化)
1. __cdecl
这个是Visual C++中最最常用的调用约定,但是在代码里并不常见。为什么呢?原因就是它太常用了,VC把它作为了默认值,也就是说一个函数如果不声明任何的调用约定,那这个函数用的就是__cdecl。下面两句是等同的。
void f(int x);
void __cdecl f(int x);
现在让我们看看编译器到底怎么实现这种调用约定的。假设我们现在编译下面这段代码:
// 调用函数f1 f1(1, 2, 3, 4); // 函数f1的实现 int __cdecl f1(int a, int b, int c, int d) { return a + b + c + d; }
编译后的反汇编是:
;调用函数f1,4个参数分别是1,2,3和4
00401093 push 4 ;参数从右到左开始压栈,先压最后一个
00401095 push 3 ;第3个参数压栈
00401097 push 2 ;第2个参数压栈
00401099 push 1 ;第1个参数压栈
0040109B call f1 (401005h) ;调用函数f1
004010A0 add esp,10h ;清除栈上的4个参数
;函数f1的实现
push ebp ;保存寄存器ebp
mov ebp,esp ;将当前栈指针赋值给ebp
mov eax,dword ptr [ebp+8] ;eax为参数a
add eax,dword ptr [ebp+0Ch] ;eax = eax + 参数b
add eax,dword ptr [ebp+10h] ;eax = eax + 参数c
add eax,dword ptr [ebp+14h] ;eax = eax + 参数d
pop ebp ;恢复寄存器ebp的值
ret ;函数返回,返回值是eax
可以看到清除参数的工作是由caller(调用者,就是调用函数f1的地方)来负责。因为我们一共有4个int的参数,每个int是 4个byte,一共16个byte,换算成16进制是10h,所以上面粗体的反汇编(add esp,10h),通过直接把esp加10h来清除4个参数。(esp是指向栈顶的寄存器)
如果上面的反汇编有困难的话,可以记住这么一句话:__cdecl是由调用者来清除栈上的参数。