参考了 微软的文档 和 Deepseek v4 网页快速模式。这里讨论主要是方便 x86 逆向汇编的时候参考。

__cdecl

在 x64 和 arm 上编译器会忽略 __cdecl。因为按照 x64 和 arm 的约定,自变量应该尽可能传入寄存器,后续自变量传入堆栈。

此外,这是 C/C++ 默认的调用约定(除非你改了项目设置)。

  • 参数传递:从右到左依次压入栈(push)。
  • 栈清理调用者(Caller) 负责在 call 返回后清理压入的参数(即 add esp, 参数总字节数)。
  • 可变参数:支持 printf 这种参数个数不固定的函数。因为只有调用者才知道自己压了多少个参数,所以必须由调用者自己加回去。
  • 名称修饰:函数名前加下划线,如 _MyFunc
  • 应用场景:绝大多数的 C 库函数、你自己写的普通函数。

__stdcall

这是 Windows 标准 API(如 kernel32.dlluser32.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 风格的类成员调用。