目录

在 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 默认、printfWin32 API性能敏感函数

总结

本文只列举了最常提到的三个约定,还有 __thiscall 等没展开,但日常基本够用。

核心要记住:调用约定影响的是编译后生成的符号,这对调用库函数尤其关键。

  • 兼容性:调用方和被调用方必须用同一种约定,否则会出现栈损坏、参数错乱甚至崩溃。
  • 跨编译器差异:MSVC、GCC、Clang 对 __fastcall 等的实现可能不同,跨编译器调用要谨慎。
  • 名字修饰:C 和 C++ 混编时,修饰规则不一致会报未定义符号,用 extern “C” 统一。
  • 取舍:__fastcall 快但不支持可变参数,__cdecl 灵活但稍慢。

如果应用程序的调用约定和库的编译约定不一致,很可能编译通过、链接失败(Windows 上很常见),或者出现莫名其妙的运行时异常。