Win32调用约定详解

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);
关于__cdecl, __stdcall, __fastcall几种调用约定,我编译的示范代码是基于C语言的(不是C++)函数声明,在后面的文章中提到C语言用能到的声明方式,C++的声明方式超出了这篇文章的范围。
  C 调用约定(__cdecl)
这个调用约定是C/C++默认的(编译选项指定/GD),如果一个工程是使用的其他调用约定,我们仍然可以声明一个函数使用_cdecl调用约定:
代码:
int __cdecl sumExample (int a, int b);
_cdecl调用约定的主要特点是:
1.参数从右到左传递到堆栈中
2.堆栈的清理时有调用者执行
3.声明函数名前面加上一个下划线"_"
现在让我们看下_cdecl调用的一个例子:
代码:
; // 参数从右到左传递到堆栈中
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
标准调用约定(_stdcall)
  这个是使用于Win32API函数的调用约定;事实上WINAPI只不过是_stdcall的另一个名称。
代码:
#define WINAPI __stdcall
我们可以显示的声明一个函数使用_stdcall调用约定。
代码:
int __stdcall sumExample (int a, int b);
另外,我们可以使用编译选项/Gz来指定一些其他调用约定不明确的函数使用此调用约定。
_stdcall调用约定的主要特点如下:
1.参数从右到左传递到堆栈
2.堆栈清理由被调用函数执行
3.函数名前加上一个下划线字符和附加一个"@"字符和堆栈所需的空间字节。
范例如下:
代码:
; 
// 参数从右到左传递到堆栈
 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
__stdcall调用约定,因为堆栈是由被调用函数清理的,使用的空间比_cdecl更小,其中,必须为每个函数调用生成清理堆栈的代码,另一方面,可变数目的参数(如printf()的)的函数必须使用__cdecl,因为只有在调用方知道每个函数调用中的参数数量,因此只有调用者可以执行堆栈清理。


尽可能快速的调用约定表明参数可以放置在寄存器里面,而不是放置在堆栈上面;这减少了函数调用的成本,因为寄存器操作速度比堆栈快。
我们可以显式声明一个函数使用__fastcall约定“,如下所示:

代码:
int __fastcall sumExample (int a, int b);
我们还可以使用的编译器选项/ GR来指定一些其他的调用约定声明不明确的为__fastcall。
__fastcall调用约定的主要特点是:
1.前两个函数的参数,需要32位或更少被放入寄存器ECX和EDX。他们的其余部分都推由右至左的堆栈。
2.所调用函数的参数是从堆栈中弹出。
3.函数名声明,在函数名前面加上一个“@”字符和附加一个“@”的字节数(十进制)的参数所需的空间。
注:微软保留权利更改在未来版本的编译器传递参数的寄存器。
这里是一个例子:
代码:
; // 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
这是调用约定快速,对比下__cdecl和__stdcall,设置编译器选项/ GR,比较执行时间。我没有找到__fastcall必须比其他调用调用约定更快的了,但你可能会得出不同的结果。


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指针通过栈传递。

你可能感兴趣的:(Win32调用约定详解)