目录
1.引言
2.常见函数调用约定
2.1.__cdecl
2.2.__stdcall
2.3.__fastcall
2.4.__thiscall
3.几种调用约定比较
4.注意事项
1.引言
在C和C++编程中,函数调用约定(Calling Convention)定义了函数如何接收参数、如何返回值以及由谁来清理栈上的参数。不同的平台和编译器可能会支持不同的调用约定。在Windows平台上,尤其是在使用Microsoft的编译器(如MSVC)时,常见的调用约定包括__cdecl
、__stdcall
、__fastcall
和__thiscall。
对于一个基本函数 int func(int a, int b);
,C++ 底层通过帧栈来描述函数调用的过程,一个帧栈过程大致包含了以下过程:
-
栈空间的开辟,也叫栈拉伸。
-
调用子函数参数准备,这个过程会把子函数需要的参数压栈。
-
寄存器现场保护,跳转调用子函数。
-
栈空间回收,也叫栈平衡。
在这些过程中,每种编译器实现不同,导致了同一段函数调用得到的汇编代码也不相同。参数按照什么顺序压栈?调用完成后由谁负责清理压栈的参数?为了统一这些行为,于是又了函数调用约定。
2.常见函数调用约定
2.1.__cdecl
__cdecl 是 C/C++ 默认的调用约定,声明函数是不加任何调用约定的限制会默认采用 __cdecl 调用约定。该调用约定有以下特点:
-
按从右至左的顺序压参数入栈。
-
由调用者把参数弹出栈。切记:对于传送参数的内存栈是由调用者来维护的,返回值在
EAX
中。因此对于像printf
这样可变参数的函数必须用这种约定。 -
编译器在编译这种调用规则的函数时,生成修饰名在输出函数名前加上一个下划线前缀,格式为
_function
。如函数int add(int a, int b)
的修饰名是_add
。
测试代码如下:
#include <iostream>int __cdecl test(int x, int y, int s, int t)
{return x + y + s + t;
}int main()
{int a = 1; // 断点test(1, 2, 3, 4);return 0;
}
显示反汇编得到下面结果:
-
参数从右向左进行压栈,先压栈 2,再压栈 1。push 压栈的时候就进行了栈拉伸,这里是 8 个字节。
-
在 test 函数调用完成后,调用者也就是 main 函数中进行了栈平衡。
2.2.__stdcall
__stdcall 是微软规定的 C++ 标准调用方式,__stdcall 通常用于 Win32 API 中,具有如下特点:
-
按从右至左的顺序压参数入栈。
-
由被调用者(callee)负责清理栈上的参数。切记:函数自己在退出时清空堆栈,返回值在EAX中。
-
__stdcall 调用约定在输出函数名前加上一个下划线前缀,后面加上一个 @ 符号和其参数的字节数,格式为 _function@number。如函数int sub(int a, int b)的修饰名是 _sub@8。
测试代码如下:
#include <iostream>int __stdcall test(int x, int y, int s, int t)
{return x + y + s + t;
}int main()
{int a = 1; // 断点test(1, 2, 3, 4);return 0;
}
显示反汇编得到下面结果:
-
在 main 函数中进行参数压栈时,参数从右往左进行压栈。push 压栈的时候就进行了栈拉伸,这里是 8 个字节。
-
在 test 函数调用结束后,main 函数中没有进行栈平衡操作。
-
在 test 函数返回时,通过
ret 8
实现了栈平衡,也就是被调用者负责栈平衡。
2.3.__fastcall
__fastcall 调用约定按照名字理解应该更快一些,这是由于它是用的寄存器传递参数,相对于内存而言,寄存器的读写速度更快,该调用约定有以下特点:
-
__fastcall 用 ECX 和 EDX 传送前两个 DWORD 或更小的参数,剩下的参数仍自右向左压栈传递。
-
由被调用者把参数弹出栈。
-
__fastcall 调用约定在输出函数名前加上一个 @ 符号,后面也是一个 @ 符号和其参数的字节数,格式为 @function@number,如 int sub(int a, int b) 的修饰名是 @sub@8。
测试代码如下:
#include <iostream>int __fastcall test(int x, int y, int s, int t)
{return x + y + s + t;
}int main()
{int a = 1; // 断点test(1, 2, 3, 4);return 0;
}
显示反汇编得到下面结果:
-
参数压栈时,前两个参数通过寄存器传递,第三个参数开始进行压栈,同样是从右向左。
-
在 test 函数调用结束后,main 函数中没有进行栈平衡操作。
-
在 test 函数返回时,通过
ret 8
实现了栈平衡,也就是被调用者负责栈平衡。
2.4.__thiscall
__thiscall 是 x86 架构上类的非可变参数成员函数的默认调用约定,具有以下特点:
-
按从右至左的顺序压参数入栈。
-
this 指针通过寄存器 ECX 传递,而不是在堆栈上传递。
-
由被调用者把参数弹出栈。
当成员函数是不定参数的情况下,采用 __cdecl 调用约定,所有参数都会被压栈,this 指针最后被压栈。
3.几种调用约定比较
调用约定 | 参数传递方式 | 清理栈方 | 符号修饰 | 使用场合 |
---|---|---|---|---|
__cdecl | 从右到左 | 调用者清理 | _functionname | C/C++默认调用约定 |
__stdcall | 从右到左 | 被调用者清理 | _functionname@paramsize | Win API |
__fastcall | 从右到左 | 被调用者清理 | @functionname@paramsize | 调用速度快 |
__thiscall | 从右到左 | 被调用者清理 | \ | 成员函数非不定参数 |
4.注意事项
- 调用约定影响函数的链接和调用方式,因此在定义和声明函数时,调用约定必须一致。
- 在跨语言或跨编译器调用函数时,需要特别注意调用约定的匹配,以避免潜在的崩溃或数据损坏。
- 在64位系统上,许多调用约定之间的差异变得不那么重要,因为所有的参数通常都通过寄存器传递(例如,Windows上的
__vectorcall
)。
理解这些调用约定对于深入理解和优化Windows平台上的C/C++代码非常重要。另外值得注意的是上面的调用约定是针对 x86 架构,在 x64、arm64 这样的架构上有更多的寄存器用来传递参数,实际的情况又会和上面不同了。
推荐阅读:
从汇编来角度剖析C语言函数调用过程_如何用汇编实现函数调用-CSDN博客