C/C++中的调用约定
在C/C++编程中,调用约定(calling conventions)是一组指定如何调用函数的规则。主要在你调用代码之外的函数(例如OS API,操作系统应用程序接口)或OS调用你(如WinMain的情况)时起作用。如果编译器不知道正确的调用约定,那么你很可能会遇到非常奇怪的崩溃,因为堆栈将无法正确管理。调用约定用于确保函数在不同平台和编译器之间正确且一致地调用。调用约定有助于标准化编译器的函数调用和参数传递方式,标准化可实现编程语言之间的互操作性。
并非所有约定都在所有支持的平台上可用,某些约定使用平台特定的实现。在大多数情况下,将忽略在特定平台上指定不支持的约定的关键字或编译器开关,并将使用平台默认约定。
调用约定通常在函数声明或定义时通过特定的关键字或属性指定。例如,在MSVC中,可以使用__cdecl、__stdcall、__fastcall等关键字来指定调用约定。在GCC中,可以使用__attribute__((cdecl))、__attribute__((stdcall))等属性来指定调用约定。在跨平台开发或调用第三方库时,需要特别注意调用约定的匹配。
C/C++中的调用约定是确定以下准则:
(1).如何将参数传递到堆栈。
(2).谁将清除堆栈,调用者还是被调用者?
(3).将使用哪些寄存器以及如何使用。
C++代码在编译阶段结束时转换为目标代码(object code)。然后我们得到目标文件(object file)。目标文件链接在一起以创建二进制文件(如exe、lib、dll)。在创建目标文件之前,我们可以告诉编译器停止并给我们.asm文件。这是将转换为目标文件的汇编文件。不同的调用约定会产生不同的汇编代码(Different calling conventions produce different assembly codes)。
MSVC支持几种函数的调用约定:"__cdecl"、"__stdcall"、"__fastcall"等,默认情况下MSVC把C语言的函数当作"__cdecl"类型,这种情况下它对该函数不进行任何符号修饰。但是一旦我们使用其它的函数调用约定时,MSVC编译器就会对符号名进行修饰,比如使用"__stdcall"调用约定的函数Add就会被修饰成"_Add@16",前面以"_"开头,后面以"@n"结尾,n表示函数调用时参数所占堆栈空间的大小。使用.def文件可以将导出函数重新命名。当一个DLL被多个语言编写的模块使用时,采用这种方法导出一个函数往往会很有用。我们经常看到Windows的API都采用"WINAPI"这种方式声明,而"WINAPI"实际上是一个被定义为"__stdcall"的宏。微软以DLL的形式提供Windows的API,而每个DLL中的导出函数又以这种"__stdcall"的方式被声明。
MSVC下不同的调用约定:使用时将以下修饰符放置在变量或者函数名称的前面,告诉编译器设置堆栈、推送参数和获取返回值的规则
1.__cdecl:C declaration,C/C++程序的默认调用约定。是一种"干净"的调用约定,不使用任何特殊指令或寄存器来传递函数参数或返回值。
(1).参数从右向左依次入栈(因此第一个参数最接近堆栈顶部)。
(2).调用者负责清理堆栈,即调用者会在返回后弹出所有参数。
(3).创建比__stdcall更大的可执行文件,因为它要求每个函数调用都包含堆栈清理代码。
(4).当调用者按照此约定清理堆栈时,我们可以为使用__cdecl调用约定的函数提供可变参数。
2.__stdcall:Standard Call,Win32 API函数使用的特定于Microsoft的调用约定。
(1).参数从右向左依次入栈(因此第一个参数最接近堆栈顶部)。
(2).被调用者(自身)负责清理堆栈,即被调用者在返回前会弹出所有参数。
3.__fastcall:在__fastcall调用约定中,如果可能的话,参数会被传递给寄存器。
(1).前两个参数(从左到右的顺序)通过寄存器ECX和EDX传递。其余参数从右向左依次入栈。
(2).被调用者负责清理堆栈。
4.__thiscall:仅适用于C++,类内方法使用的默认调用约定。
(1).参数从右向左依次入栈。
(2).this指针通过寄存器ECX传递,而不是在堆栈上传递。
(3).不能对非成员函数使用此调用约定。
(4).被调用者负责清理堆栈。
(5).__thiscall只能出现在非静态成员函数上。
在C/C++中使用调用约定的优点:
1.兼容性:不同的平台和编译器对函数参数的传递方式和返回值的返回方式可能有不同的要求。通过使用特定的调用约定,你可以确保你的代码与不同的平台和编译器兼容。
2.一致性:使用一致的调用约定有助于确保你的代码易于阅读和理解。
3.优化:某些调用约定旨在比其他调用约定更高效。例如,__fastcall调用约定可以比__cdecl或__stdcall更快,因为它使用寄存器传递函数参数。这对于经常调用的函数尤其有用。
4.安全性:使用一致的调用约定有助于防止出现诸如堆栈损坏或返回值不正确等错误。
在C/C++中使用调用约定的缺点:
1.复杂性:使用不同的调用约定会增加代码的复杂性,尤其是在处理具有许多不同功能的大型项目时。这会使代码更难理解和维护。
2.兼容性问题:如果你使用的调用约定不受特定平台或编译器支持,则可能必须修改代码或使用不同的调用约定。这可能很耗时,并且可能需要进行额外的测试以确保代码仍然正确。
3.性能开销:某些调用约定可能会带来少量性能开销,尤其是在它们需要额外的指令或寄存器使用时。这对于大多数应用程序来说可能并不重要,但对于高性能代码来说,这可能是一个考虑因素。
4.可移植性:如果你使用的调用约定特定于特定平台或编译器,则你的代码可能无法移植到其他平台或编译器。如果你想在不同平台上重用代码,或者你正在使用混合环境,这可能会成为一个问题。
注:以上整理的内容主要来自:
1. https://www.geeksforgeeks.org
2. https://www.javatpoint.com
以下为测试代码:
#include "calling_convention.hpp"
#include <iostream>
namespace {
#ifdef _MSC_VER
#define CDECL __cdecl
#define STDCALL __stdcall
#define FASTCALL __fastcall
#define THISCALL __thiscall
#else
#define CDECL __attribute__((cdecl))
#define STDCALL __attribute__((stdcall))
#define FASTCALL __attribute__((fastcall))
#define THISCALL __attribute__((thiscall))
#endif
int CDECL add_cdecl(int a, int b) { return (a + b); }
int STDCALL add_stdcall(int a, int b) { return (a + b); }
int FASTCALL add_fastcall(int a, int b) { return (a + b); }
class Tmp {
public:
int THISCALL add_thiscall(int a, int b) { return (a + b); }
};
} // namespace
int test_calling_convention()
{
auto ret1 = add_cdecl(1, 2);
auto ret2 = add_stdcall(1, 2);
auto ret3 = add_fastcall(1, 2);
Tmp tmp;
auto ret4 = tmp.add_thiscall(1, 2);
std::cout << "results: " << ret1 << "," << ret2 << "," << ret3 << "," << ret4 << std::endl;
return 0;
}
GitHub:https://github.com/fengbingchun/Messy_Test