3 调用约定
调用约定决定了以下内容:函数参数的压栈顺序、由调用者还是被调用者平衡堆栈。
3.1 __cdecl
__cdecl是C和C++程序的默认调用约定:参数通过堆栈来传递,从右向左依次入栈,由调用者平衡堆栈。
同样的代码,我们在AddInt函数前面加上__cdecl调用约定
int __cdecl AddInt(int a, int b)
{
int c = a+b;
return c;
}
F5运行断下后,按“ALT+F8”打开反汇编窗口,没有任何变化,和没加__cdecl一样,说明默认调用约定就是__cdecl。
3.2 __stdcall
同样的代码,我们在AddInt函数前面加上__stdcall调用约定
int __stdcall AddInt(int a, int b)
{
int c = a+b;
return c;
}
再反汇编看看main函数里,调用的过程如下
9: int x = AddInt(1, 3);
00401078 push 3
0040107A push 1
0040107C call @ILT+10(AddInt) (0040100f)
参数还是一样从右向左依次入栈,但是没有了add esp,8这句,那谁来平衡堆栈呢?看看AddInt函数的反汇编的最后一条指令:
0040104A ret 8
这一句就相当于
ret
add esp,8
所以__stdcall的调用约定是参数通过堆栈来传递,从右向左依次入栈,由被调用者平衡堆栈。
一般Windows API函数都是__stdcall,在Windef.h中可以找到如下的定义:
#define WINAPI __stdcall
3.3 __fastcall
同样的代码,我们在AddInt函数前面加上__fastcall调用约定
int __fastcall AddInt(int a, int b)
{
int c = a+b;
return c;
}
再反汇编看看main函数里,调用的过程如下
9: int x = AddInt(1, 3);
00401078 mov edx,3
0040107D mov ecx,1
00401082 call @ILT+0(AddInt) (00401005)
可以看到,两个参数分别用ECX和EDX传递。如果更多参数会怎么样呢?
int __fastcall AddInt(int a, int b, int c, int d)
{
int e = a+b+c+d;
return e;
}
调用的过程如下:
9: int x = AddInt(1, 3, 7, 9);
00401078 push 9
0040107A push 7
0040107C mov edx,3
00401081 mov ecx,1
00401086 call @ILT+15(AddInt) (00401014)
可以看出来,__fastcall的调用约定是:第一个参数通过ECX传递,第二个参数通过EDX传递,第三个参数起从右向左依次入栈,由被调用者平衡堆栈。
3.4 类的成员函数
对于类的成员函数来说,除了要传递普通的参数,还有一个隐藏的参数——this指针。
class Example
{
public:
int AddInt(int a, int b)
{
int c = a+b;
return c;
}
};
int main()
{
Example a;
a.AddInt(1, 3);
return 0;
}
反汇编看一下调用的过程:
16: a.AddInt(1, 3);
0040B468 push 3
0040B46A push 1
0040B46C lea ecx,[ebp-4]
0040B46F call @ILT+5(Example::AddInt) (0040100a)
在调用之前,多了一个指令lea ecx,[ebp-4],这一句实际上就是将this指针传递给ECX,可以看出来,类成员函数的默认调用约定是:参数通过堆栈来传递,从右向左依次入栈,由被调用者平衡堆栈栈,this指针通过ECX传递。除了this指针,其他都和__stdcall相同。
所以我们只要看到call调用前,某个地址传递给了ECX,就可以知道十有八九调用的是一个类成员函数。
值得注意的是VC编译器默认使用ECX传递this指针,但是Borland C++编译器却是用EAX,不同的编译器处理的方式不一样。
更进一步,如果指定类的成员函数调用约定为__cdecl、__stdcall或者是__fastcall,会是什么情况呢?
3.4.1 __cdecl
15: a.AddInt(1, 3);
00401038 push 3
0040103A push 1
0040103C lea eax,[ebp-4]
0040103F push eax
00401040 call @ILT+20(Example::AddInt) (00401019)
00401045 add esp,0Ch
3.4.2 __stdcall
15: a.AddInt(1, 3);
00401038 push 3
0040103A push 1
0040103C lea eax,[ebp-4]
0040103F push eax
00401040 call @ILT+0(Example::AddInt) (00401005)
3.4.3 __fastcall
15: a.AddInt(1, 3);
00401038 push 3
0040103A mov edx,1
0040103F lea ecx,[ebp-4]
00401042 call @ILT+10(Example::AddInt) (0040100f)
可以看出,如果指定了调用约定,实际上编译器把this指针当成函数的第一个参数进行处理了。