FROM:http://bbs.pediy.com/showthread.php?t=141028
FROM: http://bbs.pediy.com/showthread.php?t=91541
在长时间辛苦的学习C++的Windows编程的过程中,你可能有时候有点好奇,某个时候在函数声明前会出现奇怪的符号,像 __cdecl, __stdcall, __fastcall, WINAPI, 等等,之后通过MSDN或其他一些参考,你可能会发现,这些符号是用于指定函数的调用约定,在这篇文章中,我将可能尝试解释VC++不同的调用约定(也可能是其他Windows平台的C/C++编译器中使用的约定),我在这里强调一下,如果你想编写可移植代码,就不能使用这些调用约定。
你可能会问:声明是调用约定?当一个函数被调用的时候,通常会传递参数给它和检查返回值;那么,调用约定就描述了如何传递参数给函数和检查函数的返回值,它还指定了函数名是哪种声明;写优秀的C++程序真的有必要了解调用约定吗;不是,但是它可能会对调试很有帮助;此外它是连接C/C++和汇编代码必须使用的。
为了理解本文章,你需要一些很基本的汇编语言程序设计知识。
使用调用约定的时候可能会发生一些下面的情况:
1.所有参数扩大到4字节(当然在Win32平台上),并且储存在合适的内存位置,这个位置筒仓是栈,但也有可能是寄存器,它都是通过调用约定指定的。
2.程序执行跳转到被调用函数的地址
3.在函数内部,保存ESI,EDI,EBX和EBP到栈,执行这些操作代码的部分,被称为函数过程,一般是由编译器生成
4.函数指定的代码被执行,并把返回值的放在EAX寄存器内
5.从堆栈中恢复ESI,EDI,EBX和EBP寄存器,这段代码称为功能终止,与函数过程相对应,在大多数情况下,一般是由编译器生成。
6.从堆栈中移除参数,这个操作称为清理堆栈,可能是调用函数内部执行或调用方执行,这个根据调用约定处理
调用约定的一个例子,我们将会使用一个简单的函数:
int sumExample (int a, int b) { return a + b; }
int c = sum (2, 3);
int __cdecl sumExample (int a, int b);
; // 参数从右到左传递到堆栈中 push 3 push 2 ; // 调用函数 call _sumExample ; // 通过增加ESP寄存器大小清理堆栈参数 add esp,8 ; // 复制返回值到一个局部变量 (int c) mov dword ptr [c],eax
; // function prolog push ebp mov ebp,esp sub esp,0C0h push ebx push esi push edi lea edi,[ebp-0C0h] mov ecx,30h mov eax,0CCCCCCCCh rep stos dword ptr [edi] ; // return a + b; mov eax,dword ptr [a] add eax,dword ptr [b] ; // function epilog pop edi pop esi pop ebx mov esp,ebp pop ebp ret
#define WINAPI __stdcall
int __stdcall sumExample (int a, int b);
; // 参数从右到左传递到堆栈 push 3 push 2 ; // 调用函数 call _sumExample@8 ; // 复制返回值到一个局部变量 (int c) mov dword ptr [c],eax
; // function prolog goes here (the same code as in the __cdecl example) ; // return a + b; mov eax,dword ptr [a] add eax,dword ptr [b] ; // function epilog goes here (the same code as in the __cdecl example) ; // cleanup the stack and return ret 8
尽可能快速的调用约定表明参数可以放置在寄存器里面,而不是放置在堆栈上面;这减少了函数调用的成本,因为寄存器操作速度比堆栈快。
我们可以显式声明一个函数使用__fastcall约定“,如下所示:
int __fastcall sumExample (int a, int b);
; // put the arguments in the registers EDX and ECX mov edx,3 mov ecx,2 ; // call the function call @fastcallSum@8 ; // copy the return value from EAX to a local variable (int c) mov dword ptr [c],eax
// function prolog push ebp mov ebp,esp sub esp,0D8h push ebx push esi push edi push ecx lea edi,[ebp-0D8h] mov ecx,36h mov eax,0CCCCCCCCh rep stos dword ptr [edi] pop ecx mov dword ptr [ebp-14h],edx mov dword ptr [ebp-8],ecx ; // return a + b; mov eax,dword ptr [a] add eax,dword ptr [b] ;// function epilog pop edi pop esi pop ebx mov esp,ebp pop ebp ret
Thiscall是默认调用C + +类(除了那些带有可变数量的参数)的成员函数调用约定。
thiscall调用约定的主要特点是:
1.从左向右传递参数放置到堆栈,这个是放在ECX里面
2.是由被调用函数的堆栈清理
这个调用约定的例子有点不同。首先,作为C + +而不是C编译的代码,我们有一个成员函数的结构,而不是一个全局函数。
struct CSum
{
int sum ( int a, int b) {return a+b;}
};
函数调用的汇编代码看起来像这样:
push 3
push 2
lea ecx,[sumObj]
call ?sum@CSum@@QAEHHH@Z ; CSum::sum
mov dword ptr [s4],eax
这个函数它自己产生下面的代码:
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
push ecx
lea edi,[ebp-0CCh]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-8],ecx
mov eax,dword ptr [a]
add eax,dword ptr [b]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8
如果我们现在有一个一个可变数目的参数的成员函数,会发生什么情况?在这种情况下,使用_cdecl,这个是压入堆栈的最后。
==========================================================================================================================================================
在此只是讨论VC6环境下C/C++ 32位函数调用约定,其它环境感兴趣的自己去发掘与验证。
函数调用约定(call convention)涉及函数参数如何传递, 谁平栈(还有命名,返回值等问题不在此讨论), 在VC6环境下有4种调用约定, 分别说明如下:
__cdecl 参数全部参过栈传递, 压栈顺序从右到左, 即最后一个参数先入栈; 调用者负责平栈。举例如下:
print("",1);的汇码如下:
004010B6 6A 01 push 1
004010B8 68 7C 20 42 00 push offset string "" (0042207c)
004010BD E8 7E 06 00 00 call printf (00401740)
004010C2 83 C4 08 add esp,8
最后一句 add esp,8就是平栈的,因为调用函数print时压入了两个DWORD大小的参数。
__stdcall 参数全部通过栈传递,压栈顺序从右到左; 被调用者负责平栈。
CreateFileA("\\\\.\\PHYSICALDRIVE0",GENERIC_READ,FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,0,OPEN_EXISTING,0,0);的实现代码如下:
004010F4 6A 00 push 0
004010F6 6A 00 push 0
004010F8 6A 03 push 3
004010FA 6A 00 push 0
004010FC 6A 07 push 7
004010FE 68 00 00 00 80 push 80000000h
00401103 68 34 20 42 00 push offset string "\\\\.\\PHYSICALDRIVE0" (00422034)
00401108 FF 15 60 A1 42 00 call dword ptr [__imp__CreateFileA@28 (0042a160)]
调用CreateFileA的代码并没有去平栈,再看CreateFileA的部分代码如下
7C801A28 8B FF mov edi,edi
7C801A2A 55 push ebp
7C801A2B 8B EC mov ebp,esp
7C801A2D FF 75 08 push dword ptr [ebp+8]
7C801A30 E8 DF C6 00 00 call 7C80E114
7C801A35 85 C0 test eax,eax
7C801A37 74 1E je 7C801A57
7C801A39 FF 75 20 push dword ptr [ebp+20h]
7C801A3C FF 75 1C push dword ptr [ebp+1Ch]
7C801A3F FF 75 18 push dword ptr [ebp+18h]
7C801A42 FF 75 14 push dword ptr [ebp+14h]
7C801A45 FF 75 10 push dword ptr [ebp+10h]
7C801A48 FF 75 0C push dword ptr [ebp+0Ch]
7C801A4B FF 70 04 push dword ptr [eax+4]
7C801A4E E8 AD ED 00 00 call 7C810800
7C801A53 5D pop ebp
7C801A54 C2 1C 00 ret 1Ch
可以看到, 函数返回 ret 1ch, 函数返回时弹出了1Ch字节,因为它有7个DWORD大小的参数,7*4=1Ch
__fastcall, 前两个(如果有的话)DWORD或更小大小的参数通过REG传递,第一个在ecx中,第二个在edx中,如果还有更多的参数, 则通过栈传递, 同样是从右到左,由被调用者负责平栈
__thiscall, 类(包括class/struct/union)非静态成员函数默认的调用约定类型,C++中不能显式声明它,这种调用约定有点象是__fastcall与__stdcall的混合体, 隐含的this指针通过ecx传递,其它参数从右到左压栈, 被调用者负责平栈。
__cdecl类型的函数可以实现特殊的功能,即参数数量可变,如printf,其它调用类型不可以实现。
WINDOWS API大部声明为 WINAPI,实际上它是__stdcall,不过并非所有的WINDOWS API都是WINAPI调用约定的, 有许多__cdecl调用的。
__fastcall也是比较常见的,WINDOWS 内核的许多API是__fastcall类型,写驱动的要注意。
有一点是很少有资料提到,就是类的非静态成员调用约定不是一定是__thiscall, 你可以为一个非静态成员函数指定调用约定类型为__cdecl, __stdcall, 或__fastcall, 这种情况下,隐含的this指针总是函数的第一个参数, 对于__cdecl/__stdcall来说this指针被最后一个压入栈中,对于__fastcall来说, this指针仍然由ecx传递。所以对于类成员函数也可以实现可变参数,也可以强制要求将this指针通过栈传递。