在 C/C++ 中有不同的调用约定:stdcall
、extern
、pascal
等。有多少这样的调用约定可用,每个是什么意思?有没有描述这些的链接?
6 回答
简单的回答:我使用 cdecl、stdcall 和 fastcall。我很少使用fastcall。stdcall 用于调用 Windows API 函数。
详细答案(从维基百科窃取):
cdecl - 在 cdecl 中,子程序参数在堆栈上传递。EAX 寄存器中返回整数值和内存地址,ST0 x87 寄存器中返回浮点值。寄存器 EAX、ECX 和 EDX 是调用者保存的,其余的是被调用者保存的。x87 浮点寄存器 ST0 到 ST7 在调用新函数时必须为空(弹出或释放),而在退出函数时 ST1 到 ST7 必须为空。ST0 在不用于返回值时也必须为空。
syscall - 这与 cdecl 类似,因为参数是从右向左推送的。不保留 EAX、ECX 和 EDX。双字参数列表的大小在 AL 中传递。
pascal - 参数按从左到右的顺序压栈(与 cdecl 相反),被调用者负责在返回前平衡栈。
stdcall - stdcall[4] 调用约定是 Pascal 调用约定的变体,其中被调用者负责清理堆栈,但参数按从右到左的顺序压入堆栈,如 _cdecl 调用惯例。寄存器 EAX、ECX 和 EDX 被指定在函数中使用。返回值存储在 EAX 寄存器中。
fastcall - __fastcall 约定(又名 __msfastcall)传递适合 ECX 和 EDX 的前两个参数(从左到右评估)。剩余的参数从右到左压入堆栈。
向量调用- 在 Visual Studio 2013 中,Microsoft 引入了 __vectorcall 调用约定以响应游戏、图形、视频/音频和编解码器开发人员的效率问题。 [7] 对于 IA-32 和 x64 代码,__vectorcall 分别类似于 __fastcall 和原始 x64 调用约定,但扩展它们以支持使用 SIMD 寄存器传递向量参数。对于 x64,当前六个参数中的任何一个是向量类型(float、double、__m128、__m256 等)时,它们通过相应的 XMM/YMM 寄存器传入。与 IA-32 类似,多达六个 XMM/YMM 寄存器从左到右顺序分配给向量类型参数,无论位置如何。此外,__vectorcall 添加了对传递同质向量聚合 (HVA) 值的支持,这些值是仅由最多四个相同向量类型组成的复合类型,使用相同的六个寄存器。一旦为向量类型参数分配了寄存器,未使用的寄存器就会从左到右分配给 HVA 参数,无论位置如何。使用前四个 XMM/YMM 寄存器返回生成的向量类型和 HVA 值。
safecall - Microsoft Windows 上的 Delphi 和 Free Pascal,safecall 调用约定封装了 COM(组件对象模型)错误处理,因此异常不会泄露给调用者,而是按照 COM 的要求在 HRESULT 返回值中报告/奥莱。当从 Delphi 代码调用 safecall 函数时,Delphi 也会自动检查返回的 HRESULT 并在必要时引发异常。
safecall 调用约定与 stdcall 调用约定相同,只是异常在 EAX 中作为 HResult(而不是在 FS:[0] 中)传递回调用者,而函数结果在堆栈上通过引用传递为尽管它是最终的“输出”参数。当从 Delphi 调用 Delphi 函数时,此调用约定将与任何其他调用约定一样出现,因为尽管异常在 EAX 中被传回,但调用者会自动将它们转换回适当的异常。使用其他语言创建的 COM 对象时,HResults 将作为异常自动引发,Get 函数的结果在结果中而不是参数中。在 Delphi 中使用 safecall 创建 COM 对象时,无需担心 HResults,
Microsoft X64 调用约定- Windows 和预引导 UEFI 上遵循 Microsoft x64 调用约定[12][13](x86-64 上的长模式)。它将寄存器 RCX、RDX、R8、R9 用于前四个整数或指针参数(按此顺序),而 XMM0、XMM1、XMM2、XMM3 用于浮点参数。附加参数被压入堆栈(从右到左)。如果 64 位或更少,则在 RAX 中返回整数返回值(类似于 x86)。浮点返回值在 XMM0 中返回。长度小于 64 位的参数不进行零扩展;高位不归零。
在 Windows 上下文中为 x64 架构编译时(无论是使用 Microsoft 还是非 Microsoft 工具),只有一种调用约定——这里描述的一种,因此 stdcall、thiscall、cdecl、fastcall 等现在都是一个和相同的。
在 Microsoft x64 调用约定中,调用者有责任在调用函数之前在堆栈上分配 32 字节的“影子空间”(无论使用的实际参数数量如何),并在调用之后弹出堆栈。阴影空间用于溢出 RCX、RDX、R8 和 R9,[14] 但必须使所有函数都可用,即使是那些参数少于四个的函数。
寄存器 RAX、RCX、RDX、R8、R9、R10、R11 被认为是易失性的(调用者保存)。 [15]
寄存器 RBX、RBP、RDI、RSI、RSP、R12、R13、R14 和 R15 被认为是非易失性的(被调用者保存)。 [15]
例如,一个接受 5 个整数参数的函数将接受寄存器中的第一个到第四个,而第五个将被压入影子空间的顶部。所以当进入被调用函数时,堆栈将由(按升序排列)返回地址、后跟影子空间(32 字节)和第五个参数组成。
在 x86-64 中,Visual Studio 2008 将浮点数存储在 XMM6 和 XMM7(以及 XMM8 到 XMM15)中;因此,对于 x86-64,用户编写的汇编语言例程必须保留 XMM6 和 XMM7(与 x86 相比,其中用户编写的汇编语言例程不需要保留 XMM6 和 XMM7)。换句话说,当从 x86 移植到 x86-64 时,必须更新用户编写的汇编语言例程以在函数之前/之后保存/恢复 XMM6 和 XMM7。
标准 C 和标准 C++ 都没有这样的概念 - 这些是特定编译器、链接器和/或操作系统的功能,因此您应该真正指出您感兴趣的特定技术。
标准 C++ 基本上有两个:extern "C"
和extern "C++"
. 后者是默认值;当您需要链接到 C 代码时使用此前者。编译器可以定义除“C”和“C++”之外的其他字符串。例如,与其 Pascal 兄弟兼容的编译器可以定义extern "Pascal".
不幸的是,一些编译器已经发明了关键字。在这些情况下,请参阅编译器文档。
这些涉及将参数放在调用堆栈上的顺序,以及何时使用按值调用和/或按引用调用语义。它们是编译器特定的扩展,旨在简化多语言编程。
它们是调用某些库中的函数所需的特定于平台的扩展,尤其是 Win32 API。它们是非标准的并且特定于每个编译器,尽管 MSVC 的选项是 x86 上 Windows 的事实上的标准。通常,需要它们的库会在头文件中声明它们,并且它们将透明地工作。它们之间的主要区别在于,C 在历史上使用了一种效率较低的约定,该约定允许任意类型的可变数量的参数,而 Windows 和大多数其他语言的做法不同。但是,许多差异,例如左手或右手推动以及让调用者或被调用函数清理,都是非常随意的。
它们在很大程度上与 64 位代码无关:关于调用约定的圣战从未在这些平台上发生过。
在一些常见情况下,您可能需要将其中一种添加到函数中。需要与用其他语言(有时甚至是其他 C++ 编译器)编写的模块链接的 C++ 模块必须使用extern "C"
命名约定以实现兼容性。回调函数需要使用与调用者相同的调用约定,在 Windows API 中是CALLBACK
,而不是默认值。共享库可能需要使用与其内部使用的调用约定不同的调用约定导出其函数,或者可能希望__cdecl
在默认更改的情况下使用显式。在某些平台上,您可能会或可能不会获得更好的性能__fastcall
:它主要使用一两个参数加速短叶函数,并可能使某些程序变慢。
fastcall 是优化的,但没有人使用它