参考了 微软的文档 和 Deepseek v4 网页快速模式。这里讨论主要是方便 x86 逆向汇编的时候参考。
__cdecl
在 x64 和 arm 上编译器会忽略 __cdecl。因为按照 x64 和 arm 的约定,自变量应该尽可能传入寄存器,后续自变量传入堆栈。
此外,这是 C/C++ 默认的调用约定(除非你改了项目设置)。
- 参数传递:从右到左依次压入栈(
push)。 - 栈清理:调用者(Caller) 负责在
call返回后清理压入的参数(即add esp, 参数总字节数)。 - 可变参数:支持
printf这种参数个数不固定的函数。因为只有调用者才知道自己压了多少个参数,所以必须由调用者自己加回去。 - 名称修饰:函数名前加下划线,如
_MyFunc。 - 应用场景:绝大多数的 C 库函数、你自己写的普通函数。
__stdcall
这是 Windows 标准 API(如 kernel32.dll、user32.dll)的默认约定。
- 参数传递:从右到左依次压入栈(和
__cdecl一样)。 - 栈清理:被调用者(Callee) 负责在返回前清理栈。函数结尾不是简单的
ret,而是ret 0x10(表示返回时自动把栈顶抬高 16 字节,清掉 4 个参数)。 - 可变参数:不支持。因为被调用者不知道调用者压了几个参数,没法清理。
- 名称修饰:函数名加下划线,并在后面加上参数的字节总数。比如
int func(int a, int b)会被修饰为_func@8(两个 int 共 8 字节)。这也是为什么GetProcAddress在 C 里要声明为__stdcall,否则找不到符号。 - 应用场景:Win32 API 函数、COM 接口、以及大多数需要被外部调用的系统级函数。
__fastcall
这个约定试图用寄存器传递参数,以减少访问栈内存的开销。
- 参数传递:前两个(或一个,取决于编译器)能放进寄存器的参数,会按从左到右的顺序分别放入
ecx和edx。剩下的参数从右到左压入栈。 - 栈清理:被调用者(Callee) 负责清理栈上剩余的参数。
- 应用场景:现在很少有人直接用这个关键字了,因为现代编译器优化得极好,且 64 位已经全面转向寄存器传参,
__fastcall略显尴尬。
__thiscall
这是 C++ 类成员函数(非静态)的默认约定。
- 参数传递:
this指针(指向类实例自己的指针)被放入ECX寄存器中。其余参数从右到左压入栈。 - 栈清理:被调用者(Callee) 负责清理栈(类似于
__stdcall),因为this在寄存器里,不占用栈空间,清理逻辑跟__stdcall一样。 - 注意:在微软的 MSVC 编译器中,如果函数是可变参数的成员函数,则会退化成
__cdecl,以支持printf风格的类成员调用。