65.9K
CodeProject 正在变化。 阅读更多。
Home

调用约定详解

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (149投票s)

2001年9月23日

CPOL

6分钟阅读

viewsIcon

505217

Visual C++ 调用约定解释

引言

在学习 Windows C++ 编程漫长、艰苦而又美好的过程中,您可能一直对函数声明前偶尔出现的奇怪修饰符感到好奇,例如 __cdecl__stdcall__fastcallWINAPI 等。查阅 MSDN 或其他参考资料后,您可能发现这些修饰符指定了函数的调用约定。在本文中,我将尝试解释 Visual C++(以及其他 Windows C/C++ 编译器)使用的不同调用约定。我强调,上述修饰符是 Microsoft 特定的,如果您想编写可移植代码,则不应使用它们。

那么,什么是调用约定?当一个函数被调用时,通常会将参数传递给它,并获取返回值。调用约定描述了如何传递参数以及函数如何返回值。它还指定了函数名称如何修饰。理解调用约定对于编写好的 C/C++ 程序真的有必要吗?完全没有。但是,它可能有助于调试。此外,对于将 C/C++ 与汇编代码链接也是必要的。

要理解本文,您需要具备一些非常基本的汇编编程知识。

无论使用何种调用约定,都会发生以下情况:

  1. 所有参数都被扩展为 4 字节(当然在 Win32 上),并放入适当的内存位置。这些位置通常在堆栈上,但也可能在寄存器中;这由调用约定指定。
  2. 程序执行跳转到被调用函数的地址。
  3. 在函数内部,寄存器 ESI、EDI、EBX 和 EBP 被保存到堆栈上。执行这些操作的代码部分称为函数序言,通常由编译器生成。
  4. 执行函数特定代码,并将返回值放入 EAX 寄存器。
  5. 寄存器 ESI、EDI、EBX 和 EBP 从堆栈中恢复。执行此操作的代码段称为函数尾声,与函数序言一样,在大多数情况下由编译器生成。
  6. 参数从堆栈中移除。此操作称为堆栈清理,可以由被调用函数内部执行,也可以由调用方执行,具体取决于所使用的调用约定。

作为调用约定(除了 this)的示例,我们将使用一个简单的函数

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 arguments to the stack, from right to left
push        3    
push        2    

; // call the function
call        _sumExample 

; // cleanup the stack by adding the size of the arguments to ESP register
add         esp,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,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)

此约定通常用于调用 Win32 API 函数。实际上,WINAPI 只是 __stdcall 的另一个名称

#define WINAPI __stdcall

我们可以显式声明一个函数使用 __stdcall 约定

int __stdcall sumExample (int a, int b);

此外,我们可以使用编译器选项 /Gz 为所有未显式声明其他调用约定的函数指定 __stdcall

__stdcall 调用约定的主要特点是:

  1. 参数从右到左传递,并放置在堆栈上。
  2. 堆栈清理由被调用函数执行。
  3. 函数名称通过前缀下划线字符,并附加 '@' 字符和所需堆栈空间的字节数进行修饰。

示例如下:

; // push arguments to the stack, from right to left
  push        3    
  push        2    
  
; // call the function
  call        _sumExample@8

; // copy the return value from EAX to a local variable (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 更小,因为在 __cdecl 中,每次函数调用都必须生成用于堆栈清理的代码。另一方面,具有可变参数的函数(如 printf())必须使用 __cdecl,因为只有调用方知道每次函数调用中的参数数量;因此只有调用方才能执行堆栈清理。

快速调用约定 (__fastcall)

快速调用约定表示只要可能,参数就应放置在寄存器中,而不是堆栈上。这减少了函数调用的开销,因为寄存器操作比堆栈操作更快。

我们可以显式声明一个函数使用 __fastcall 约定,如下所示

int __fastcall sumExample (int a, int b);

我们还可以使用编译器选项 /Gr 为所有未显式声明其他调用约定的函数指定 __fastcall

__fastcall 调用约定的主要特点是:

  1. 前两个需要 32 位或更少的函数参数放置在寄存器 ECX 和 EDX 中。其余参数从右到左推送到堆栈上。
  2. 参数由被调用函数从堆栈中弹出。
  3. 函数名称通过前缀 '@' 字符,并附加 '@' 和参数所需空间字节数(十进制)进行修饰。

注意:Microsoft 保留了在未来编译器版本中更改传递参数的寄存器的权利。

示例如下:

; // 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

Thiscall 是调用 C++ 类成员函数(除了那些具有可变参数的函数)的默认调用约定。

thiscall 调用约定的主要特点是:

  1. 参数从右到左传递,并放置在堆栈上。this 放置在 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,并且 this 最后被推入堆栈。

结论

长话短说,我们将概述调用约定之间的主要区别:

  • __cdecl 是 C 和 C++ 程序的默认调用约定。这种调用约定的优点是它允许使用可变参数的函数。缺点是它创建的可执行文件更大。
  • __stdcall 用于调用 Win32 API 函数。它不允许函数具有可变参数。
  • __fastcall 尝试将参数放入寄存器,而不是堆栈,从而使函数调用更快。
  • Thiscall 调用约定是 C++ 成员函数(不使用可变参数)使用的默认调用约定。

在大多数情况下,这些就是您需要了解的关于调用约定的全部信息。

© . All rights reserved.