【文档翻译】__cdecl/__stdcall/__fastcall?解开神秘的调用约定!
本文档译自 www.codeproject.com 的文章 "Calling Conventions Demystified",作者 Nemanja Trifunovic,原文参见此处引言 - Introduction
在学习 Windows 编程的漫长、艰难而美妙的旅途中,你可能会对函数声明前出现的奇怪说明符感到好奇,比如 __cdecl、__stdcall、__fastcall、WINAPI 等等。在阅读过 MSDN 或其他参考资料之后,你可能知道了这些说明符是用来为函数指定一种叫“调用约定”的东西。在这篇文章中,我会使用 Visual C++ 来向你解释不同的调用约定。我要强调的是,上面提到的说明符是微软特有的,如果你想编写可移植代码,就不应该使用它们。
那么,调用约定究竟是什么呢?当我们调用函数时,通常会将参数传递给它,并获得返回值。而调用约定就描述了参数是如何传递、值是如何从函数返回的。它还指定了函数名称的修饰方式。不过,编写优秀的 C/C++ 程序真的一定要了解调用约定吗?并不是。但是,它可能有助于调试。此外,如果要把 C/C++ 与汇编代码链接,那么这也有帮助。
要理解本文,你需要具备汇编编程的一些非常基本的知识。
无论使用哪种调用约定,都会发生以下情况:
[*]所有参数都被扩展到 4 字节(除非特别说明,默认在 Win32 上),并放入内存的适当位置,这些位置通常在栈上。不过它们也可能被放在寄存器中,这便是通过调用约定指定的。
[*]程序执行流会跳转到被调用函数的地址。
[*]在函数内部,寄存器 ESI、EDI、EBX 和 EBP 的值被保存在栈上。执行这些操作的代码部分称为 function prolog,通常由编译器生成。
[*]执行函数代码,并将返回值放入 EAX 寄存器中。
[*]寄存器 ESI、EDI、EBX 和 EBP 的值从栈中恢复。执行此操作的代码段称为 function epilog,与 function prolog 一样,在大多数情况下,它由编译器生成。
[*]参数从栈中移除。此操作称为清栈(stack cleanup),可以在被调用函数的内部执行,也可以由调用方执行,具体取决于所使用的调用约定。
作为调用约定的例子(不考虑 this),我们将使用一个简单的函数:
int sumExample (int a, int b)
{
return a + b;
}对这个函数的调用看起来像这样:
int c = sum (2, 3);对于使用 __cdecl、__stdcall、__fastcall 的例子,我会把示例代码编译成 C 代码。本文后面提到的函数名修饰用的是 C 的修饰方法。C++ 的名称修饰方法超出了本文的讨论范围。
C 调用约定 - C calling convention (__cdecl)
这个约定是 C/C++ 的默认调用约定。如果项目被设置成使用其他的调用约定,我们也可以通过显式声明 __cdecl 来为某个函数指定:
int __cdecl sumExample (int a, int b);__cdecl 调用约定的主要特点是:
[*]参数将从右到左依次压入栈中。
[*]由调用者执行清栈。
[*]函数名用下划线字符 _ 作为前缀进行修饰。
现在,示例函数的调用看起来像这样:
; // 参数从右到左依次压入栈中
push 3
push 2
; // 调用函数
call _sumExample
; // 增加参数的总大小到 ESP 寄存器(向高位移动栈指针),以此来清理堆栈
add esp,8
; // 将 EAX 的返回值复制到局部变量 (int c)
mov dword ptr ,eax被调用函数 sumExample 的内部如下所示:
; // function prolog
push ebp
mov ebp,esp
sub esp,0C0h
push ebx
push esi
push edi
lea edi,
mov ecx,30h
mov eax,0CCCCCCCCh
rep stos dword ptr
; // return a + b;
mov eax,dword ptr
add eax,dword ptr
; // function epilog
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
标准调用约定 - Standard calling convention (__stdcall)
这个调用约定常常用在 Win32 API 的函数上。事实上,WINAPI 只是 __stdcall 的另一个名称。
#define WINAPI __stdcall同样,可以为一个函数显式指定标准调用约定:
int __stdcall sumExample (int a, int b);我们也可以使用编译器选项 /Gz 来给所有未显式声明约定的函数指定 __stdcall。
__stdcall 调用约定的主要特点是:
[*]参数将从右到左依次压入栈中。
[*]由被调用的函数执行清栈。
[*]函数名通过添加下划线 _ 和 @ 字符和所需的堆栈空间字节数来修饰。
调用示例如下:
; // 参数从右到左依次压入栈中
push 3
push 2
; // 调用函数
call _sumExample@8
; // 将 EAX 的返回值复制到局部变量 (int c)
mov dword ptr ,eax函数如下所示:
; // 此处是 function prolog (和 __cdecl 的例子一样,略过)
; // return a + b;
mov eax,dword ptr
add eax,dword ptr
; // 此处是 function epilog (和 __cdecl 的例子一样,略过)
; // 清栈并返回控制流
ret 8因为栈由被调用的函数清理,所以通常 __stdcall 调用约定创建的可执行文件比 __cdecl 要小。因为在 __cdecl 中,必须为每个函数调用生成清栈的代码。另一方面,参数数量可变的函数(如 printf())必须使用 __cdecl,因为只有调用者知道函数调用中的参数数量;所以,也只有调用方才能执行清栈。
Fast 调用约定 - Fast calling convention (__fastcall)
__fastcall指出,只要有可能,参数就应该放在寄存器中,而不是栈中。这减少了函数调用的成本,因为使用寄存器的操作比使用堆栈的操作要快。
我们可以显式声明 __fastcall 来使用约定,如下所示:
int __fastcall sumExample (int a, int b);我们也可以使用编译器选项 /Gr 来给所有未显式声明约定的函数指定 __fastcall。
__fastcall 的主要特点是:
[*]需要 32 位大小(及以下)的前两个函数参数被放入寄存器 ECX 和 EDX。其余的从右向左压入堆栈。
[*]被调用的函数负责从堆栈中弹出参数。
[*]函数名通过在开头添加 @ 字符并附加 @ 和参数所需的字节数(十进制)来修饰。
注意:Microsoft 保留在未来的编译器版本中更改传递参数的寄存器的权利。
调用例子如下:
; // 将参数放入寄存器 EDX 和 ECX 中
mov edx,3
mov ecx,2
; // 调用函数
call @fastcallSum@8
; // 从寄存器 EAX 拷贝返回值到局部变量 (int c)
mov dword ptr ,eax函数内部:
; // function prolog
push ebp
mov ebp,esp
sub esp,0D8h
push ebx
push esi
push edi
push ecx
lea edi,
mov ecx,36h
mov eax,0CCCCCCCCh
rep stos dword ptr
pop ecx
mov dword ptr ,edx
mov dword ptr ,ecx
; // return a + b;
mov eax,dword ptr
add eax,dword ptr
;// function epilog
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret这个调用约定究竟和 __cdecl、__stdcall 相比有多快呢?你可以自己寻找答案。通过声明不同的约定,再比较执行时间看看吧。我没有发现 __fastcall 比其他调用约定更快,不过你可能会得出不同的结论。
Thiscall - Thiscall
Thiscall 是调用 C++ 类成员函数的默认调用约定(参数数量可变的除外)。
这种约定的主要特点是:
[*]参数将从右到左依次压入栈中。this被放在 ECX 寄存器中。
[*]由被调用的函数执行清栈。
这个调用约定的例子有点不同。首先,代码被编译为 C++,而不是 C。其次,我们用一个带有成员函数的结构体,而不是用自由函数。
struct CSum
{
int sum ( int a, int b) {return a+b;}
};函数调用的汇编代码如下所示:
push 3
push 2
lea ecx,
call ?sum@CSum@@QAEHHH@Z ; CSum::sum
mov dword ptr ,eax函数内部如下所示:
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
push ecx
lea edi,
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr
pop ecx
mov dword ptr ,ecx
mov eax,dword ptr
add eax,dword ptr
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8如果我们有一个成员函数使用可变数量参数会发生什么?在这种情况下,会使用 __cdecl,this 最后被压入栈。
总结 - Conclusion
长话短说,我们总结调用约定之间的主要区别:
[*]__cdecl 是 C 和 C++ 程序的默认调用约定。这种调用约定的优点是,它允许使用具有可变数量参数的函数。缺点是它会创建更大的可执行文件。
[*]__stdcall 多用于 Win32 API 函数。它不允许函数具有可变数量的参数。
[*]__fastcall 尝试将参数放在寄存器中,而不是堆栈中,从而使函数调用更快。
[*]Thscall 调用约定是不使用可变参数的 C++ 成员函数使用的默认调用约定。
在大多数情况下,这就是你需要了解的关于调用约定的全部内容。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]