目录
在 C/C++ 里,调用约定(Calling Convention)是一套规则,规定函数调用时参数怎么传、栈由谁清理、返回值怎么交回。它保证调用方和被调用方对一次调用长什么样理解一致。不同约定影响代码体积、性能和跨模块互操作性。
本文用 GCC 和 VS2026 的编译产物做示例,重点看调用约定对编译后符号(symbol)的影响。
调用约定约束了什么
不管哪种约定,本质都在回答三个问题:
- 参数怎么传:走栈(Stack)还是走寄存器(Register)。
- 栈谁清理:调用者(Caller)弹栈,还是被调用者(Callee)弹栈。
- 名字怎么修饰(Name Mangling):源码里的 add 在目标文件里变成什么符号,链接器靠它找函数。
下面三个是平时说得最多的约定。
__cdecl
C 语言的默认约定,也是可变参数函数(比如 printf)唯一能用的约定。
- 适用场景:C 默认;需要 … 可变参数时必选。
- 参数传递:从右到左压栈。
- 栈的维护:调用方负责清理(调用后自己恢复栈顶)。
- 名字修饰:
- Windows / MSVC:符号前加下划线,如 _add。
- 非 Windows(GCC/Clang):x86-64 下基本走 System V AMD64 ABI(寄存器传参),GCC 忽略 stdcall/fastcall 这类属性;x86-32 下符号就是函数名本身(GCC 默认行为)。
- C++ 里若用 extern “C” 修饰,符号同 C;否则按 C++ 规则修饰(带参数类型)。
- 特点:支持可变参数(只有调用方知道传了几个,才能正确弹栈)。代价是每次调用都要清栈,代码体积略大。
示例:C 源码
#include <stdio.h>
// __cdecl 是 GCC 编译器的默认值
int __attribute__((cdecl)) add_cdecl(int a, int b)
{
return a + b;
}
int main()
{
int x = 5, y = 10;
int result = add_cdecl(x, y);
// 可变参数函数是 __cdecl 的典型例子
printf("This is a variadic function call: %d, %s\n", 42, "hello");
return 0;
}
GCC 编译并查看符号
gcc -c temp.c -o temp.o
nm temp.o
# 0000000000000000 T add_cdecl
# 0000000000000018 T main
# U printf
查看 32 位汇编
gcc -m32 -S temp.c -o temp.s
add_cdecl:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl 12(%ebp), %eax
addl %edx, %eax
popl %ebp
ret ; # 调用者清理栈
g++ 编译(C++ 名字修饰)
g++ -c temp.c -o temp.o
nm temp.o
# 0000000000000000 T _Z9add_cdeclii ; 按 C++ 规则修饰
# 0000000000000018 T main
# U printf
用 extern "C" 修饰后
extern "C" int __attribute__((cdecl)) add_cdecl(int a, int b)
{
return a + b;
}
g++ -c temp.c -o temp.o
nm temp.o
# 0000000000000000 T add_cdecl ; 按 C 的约定生成符号
VS 2026 编译
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
; _add_cdecl
cl /c temp.cpp
dumpbin /symbols Test.obj | findstr add
; ?add_cdecl@@YGHHH@Z (int __stdcall add_cdecl(int,int))
__stdcall
Windows API 的常用约定(比如 MessageBox),需要显式声明。
- 适用场景:Win32 API。
- 参数传递:从右到左压栈(同 __cdecl)。
- 栈的维护:被调用方清理(函数返回前自己恢复栈顶)。
- 名字修饰:
- Windows / MSVC:符号前加下划线,后接 @ 和参数总字节数,如 _add@8。
- 非 Windows:x86-64 下同样走寄存器 ABI;x86-32 下 GCC 会做名称修饰,但规则与 MSVC 不同。
- 特点:不支持可变参数(被调用方不知道传了几个,没法正确弹栈)。被调用方弹栈,代码更紧凑,但灵活性低。
示例:C 源码
int __attribute__((stdcall)) add_stdcall(int a, int b)
{
return a + b;
}
GCC 32 位汇编
gcc -m32 -S temp.c -o temp.s
add_stdcall:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl 12(%ebp), %eax
addl %edx, %eax
popl %ebp
ret $8 ; 被调用者清理栈(ret 8)
GCC / g++ 查看符号
gcc -m32 temp.c -c temp.o
nm temp.o
; 00000000 T add_stdcall
; 00000019 T main
g++ -m32 temp.c -c temp.o
nm temp.o
; 00000000 T _Z11add_stdcallii
VS 2026 编译
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
; _add@8
cl /c temp.cpp
dumpbin /symbols temp.obj | findstr add
; ?add@@YGHHH@Z (int __stdcall add(int,int))
__fastcall
追求性能的场景,用寄存器传前几个参数,减少压栈。
- 适用场景:对调用频繁、参数少的 hot 函数做加速。
- 参数传递:前两个(或更多,看编译器)整型或指针参数走寄存器(x86 里是 ECX、EDX),其余从右到左压栈。
- 栈的维护:被调用方清理。
- 名字修饰:
- Windows / MSVC:符号前加 @,后接 @ 和参数总字节数,如 @add@8。
- 非 Windows:x86-64 走寄存器 ABI;x86-32 下 GCC 会做名称修饰,规则与 MSVC 不同。
- 特点:靠寄存器传参减少栈操作,调用更快;但不支持可变参数。
示例:C 源码
int __attribute__((fastcall)) add_fastcall(int a, int b)
{
return a + b;
}
GCC 32 位查看符号
gcc -m32 temp.c -c temp.o
nm temp.o
; 00000000 T add_fastcall
; 00000020 T main
GCC 32 位汇编
gcc -m32 -S temp.c -o temp.s
add_fastcall:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl %ecx, -4(%ebp) ; ECX(第一个参数 a)存入局部变量
movl %edx, -8(%ebp) ; EDX(第二个参数 b)存入局部变量
movl -4(%ebp), %edx
movl -8(%ebp), %eax
addl %edx, %eax
leave
ret
g++ / VS 2026 查看符号
g++ -m32 temp.c -c temp.o
nm temp.o
; 00000000 T _Z12add_fastcallii
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
; @add@8
cl /c temp.cpp
dumpbin /symbols temp.obj | findstr add
; ?add@@YIHHH@Z (int __fastcall add(int,int))
一眼对比
| __cdecl | __stdcall | __fastcall | |
|---|---|---|---|
| 参数传递 | 右到左压栈 | 右到左压栈 | 前几个走寄存器,其余压栈 |
| 清栈方 | 调用者 | 被调用者 | 被调用者 |
| 可变参数 | 支持 | 不支持 | 不支持 |
| MSVC 符号 | _add | _add@N | @add@N |
| C++ 符号 | _Z9add_cdeclii | _Z11add_stdcallii | _Z12add_fastcallii |
| 典型用途 | C 默认、printf | Win32 API | 性能敏感函数 |
总结
本文只列举了最常提到的三个约定,还有 __thiscall 等没展开,但日常基本够用。
核心要记住:调用约定影响的是编译后生成的符号,这对调用库函数尤其关键。
- 兼容性:调用方和被调用方必须用同一种约定,否则会出现栈损坏、参数错乱甚至崩溃。
- 跨编译器差异:MSVC、GCC、Clang 对 __fastcall 等的实现可能不同,跨编译器调用要谨慎。
- 名字修饰:C 和 C++ 混编时,修饰规则不一致会报未定义符号,用 extern “C” 统一。
- 取舍:__fastcall 快但不支持可变参数,__cdecl 灵活但稍慢。
如果应用程序的调用约定和库的编译约定不一致,很可能编译通过、链接失败(Windows 上很常见),或者出现莫名其妙的运行时异常。