在C/C++中,调用约定(Calling Convention)是一套规则,用于规定函数调用时参数传递方式、栈的维护方式、返回值的传递方式等,确保函数调用方和被调用方对函数调用的理解一致,不同的调用约定适用于不同的场景,主要影响代码的大小、性能和互操作性。
本文将以GCC和VS2026编译的代码为示例展示调用约定对代码编译后符号的影响。
调用约定做了哪些约束?
- 参数如何传递:是通过栈(Stack),还是通过寄存器(Register)。
- 栈的清理责任:是由调用者(Caller)负责清理栈,还是由被调用者(Callee)负责清理。
- 函数名的修饰方式(Name Mangling):编译器如何将源代码中的函数名转换为目标文件中的符号名,这对于链接器(Linker)找到正确的函数至关重要。
__cdecl
- 适用场景:C 语言默认调用约定。
- 参数传递:参数从右到左压入栈。
- 栈的维护:由调用方清理栈,即调用方在调用函数后主动恢复栈顶指针。
- 函数名修饰:
- 在 Windows 平台(MSVC 编译器):
- 编译后符号名前加下划线,如 _add。
- 这种修饰是 Windows 链接器区分不同调用约定函数的方式。
- 在非 Windows 平台(GCC/Clang):
- 在 x86-64(64位) 体系结构下,几乎所有调用约定都主要使用寄存器(如 System V AMD64 ABI),GCC 会忽略 stdcall/fastcall等属性,函数名修饰遵循 C++ name mangling 规则或 C 的简单规则。
- 在 x86(32位) 体系结构下,GCC 确实会为 cdeclcall函数进行名称修饰,但规则与 Windows/MSVC 不同。如符号名就是函数名本身,而且这GCC的默认行为。
- C++中若用 extern “C”修饰,那么编译后的符号则同C,否则按C++命名修饰(包含参数类型)。
- 在 Windows 平台(MSVC 编译器):
- 特点:支持可变参数(如 printf),因为只有调用方知道实际传递的参数个数,才能正确清理栈。但调用方每次调用都要清理栈,可能增加代码体积。
- 示例
#include <stdio.h>
// __cdecl GCC编译器的默认值
int __attribute__((cdecl)) add_cdecl(int a, int b)
{
return a + b;
}
int main()
{
int x = 5, y = 10;
// 调用 __cdecl 函数
int result = add_cdecl(x, y);
printf(“Result of add_cdecl(%d, %d) is: %d\n”, x, y, result);
// 可变参数函数是 __cdecl 的典型例子
printf(“This is a variadic function call: %d, %s\n”, 42, “hello”);
return 0;
}
- gcc 编译并查看编译后的符号
gcc -c temp.c temp.o
nm temp.o
0000000000000000 T add_cdecl
0000000000000018 T main
U printf
- 查看汇编
gcc -m32 -S temp.c -o temp.s
add_cdecl:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl 8(%ebp), %edx
movl 12(%ebp), %eax
addl %edx, %eax
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret ; # 调用者清理栈
.cfi_endproc
- g++ 编译并查看编译后的符号
g++ -c temp.c temp.o
nm temp.o
# g++ 编译器按照C++的规则对函数名进行了修饰。
0000000000000000 T _Z9add_cdeclii
0000000000000018 T main
U printf
- 用extern 修饰add_cdecl
extern “C” int __attribute__((cdecl)) add_cdecl(int a, int b)
{
return a + b;
}
- g++ 编译并查看编译后的符号
g++ -c temp.c temp.o
nm temp.o
# extern “C” 修饰后函数将按照C的编译约定生成符号。
0000000000000000 T add_cdecl
0000000000000018 T main
U printf
- VS 2026 编译并查看编译后的符号
// C 编译
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
_add_cdecl
// C++ 编译
cl /c temp.cpp
dumpbin /symbols Test.obj | findstr add
?add_cdecl@@YGHHH@Z (int __stdcall add_cdecl(int,int))
__stdcall
- 适用场景:Windows API 常用(如 MessageBox),需显式声明。
- 参数传递:参数从右到左压入栈(同 __cdecl)。
- 栈的维护:由被调用方清理栈,被调用函数返回前恢复栈顶指针。
- 函数名修饰:
- 在 Windows 平台(MSVC 编译器):
- 编译后符号名前加下划线,后加@和参数总字节数,如 _add@8。
- 这种修饰是 Windows 链接器区分不同调用约定函数的方式。
- 在非 Windows 平台(GCC/Clang):
- 在 x86-64(64位) 体系结构下,几乎所有调用约定都主要使用寄存器(如 System V AMD64 ABI),GCC 会忽略 stdcall/fastcall等属性,函数名修饰遵循 C++ name mangling 规则或 C 的简单规则。
- 在 x86(32位) 体系结构下,GCC 确实会为 stdcall函数进行名称修饰,但规则与 Windows/MSVC 不同。
- 特点:不支持可变参数(被调用方无法预知参数个数,无法正确清理栈)。由于被调用方清理栈,生成的代码更紧凑,但灵活性较低。
- 示例
- 在 Windows 平台(MSVC 编译器):
int __attribute__((stdcall)) add_stdcall(int a, int b)
{
return a + b;
}
- 查看汇编
gcc -m32 -S temp.c -o temp.s
add_stdcall:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl 8(%ebp), %edx
movl 12(%ebp), %eax
addl %edx, %eax
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret $8 ; 被调用者清理栈(ret 8)
.cfi_endproc
- gcc 编译并查看编译后的符号
gcc -m32 temp.c -c temp.o
nm temp.o
U _GLOBAL_OFFSET_TABLE_
00000000 T __x86.get_pc_thunk.ax
00000000 T __x86.get_pc_thunk.bx
00000000 T add_stdcall
00000019 T main
U printf
- g++ 编译并查看编译后的符号
g++ -m32 temp.c -c temp.o
nm temp.o
U _GLOBAL_OFFSET_TABLE_
00000000 T _Z11add_stdcallii
00000000 T __x86.get_pc_thunk.ax
00000019 T main
- VS 2026 编译并查看编译后的符号
// 编译C代码
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
_add@8
// 编译C++
cl /c temp.cpp
dumpbin /symbols temp.obj | findstr add
?add@@YGHHH@Z (int __stdcall add(int,int))
__fastcall
- 适用场景:追求性能的场景,通过寄存器传递部分参数减少栈操作。
- 参数传递:前两个(或更多,取决于编译器)整型/指针参数通过寄存器(如x86中ECX、EDX)传递,其余参数从右到左压入栈。
- 栈的维护:由被调用方清理栈。
- 函数名修饰:
- 在 Windows 平台(MSVC 编译器):
- 函数的符号名进行特殊修饰,通常是在函数名前加@并在后面加上@和参数总字节数。例如,int add(int a, int b) 会被修饰为 @add@8。
- 这种修饰是 Windows 链接器区分不同调用约定函数的方式。
- 在非 Windows 平台(GCC/Clang):
- 在 x86-64(64位) 体系结构下,几乎所有调用约定都主要使用寄存器(如 System V AMD64 ABI),GCC 会忽略 stdcall/fastcall等属性,函数名修饰遵循 C++ name mangling 规则或 C 的简单规则。
- 在 x86(32位) 体系结构下,GCC 确实会为fastcall函数进行名称修饰,但规则与 Windows/MSVC 不同。
- 特点:它的主要优化是通过寄存器传递部分参数(通常是前两个整数或指针参数),以减少栈操作,从而提高函数调用的效率。
- 示例
- 在 Windows 平台(MSVC 编译器):
int __attribute__((fastcall)) add_fastcall(int a, int b)
{
return a+b;
}
- gcc 编译并查看编译后的符号
gcc -m32 temp.c -c temp.o
nm temp.o
U _GLOBAL_OFFSET_TABLE_
00000000 T __x86.get_pc_thunk.ax
00000000 T add_fastcall
00000020 T main
- 查看汇编
gcc -m32 -S temp.c -o temp.s
add_fastcall:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $8, %esp
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl %ecx, -4(%ebp) ; 将 ECX 寄存器的值(第一个参数 a)存入栈上的局部变量空间
movl %edx, -8(%ebp) ; 将 EDX 寄存器的值(第二个参数 b)存入栈上的局部变量空间
movl -4(%ebp), %edx
movl -8(%ebp), %eax
addl %edx, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
- g++ 编译并查看编译后的符号
g++ -m32 temp.c -c temp.o
nm temp.o
U _GLOBAL_OFFSET_TABLE_
00000000 T _Z12add_fastcallii
00000000 T __x86.get_pc_thunk.ax
00000020 T main
- VS 2026 编译并查看编译后的符号
// C 编译
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
@add@8
// C++ 编译
cl /c temp.cpp
dumpbin /symbols temp.obj | findstr add
?add@@YIHHH@Z (int __fastcall add(int,int))
总结
本文仅列举了经常被提起的调用约定,还有其他的调用约定(如:__thiscall)并未被枚举在本文,但是已经足够了。
本文的主旨是要说明不同调用约定对编译后生成的代码符号的影响,这个影响在对库函数的调用上影响是非常大的。
如果应用程序的调用约定和被调用库编译时采用的调用约定不一致,那么就非常有可能导致编译成功,但是链接失败的错误(在Windows较常见),也可能会遇到一些莫名奇妙的运行时异常。
最后关于调用约定有以下几点建议:
- 兼容性:调用方和被调用方必须使用相同的调用约定,否则会导致栈损坏、参数错误等崩溃。
- 跨编译器差异:不同编译器(如 MSVC、GCC、Clang)对某些约定(如 __fastcall)的实现可能不同,跨编译器调用需谨慎。
- 命名修饰:链接时若函数名修饰不一致(如 C 和 C++ 混合调用),会导致“未定义符号”错误,需用 extern “C” 统一修饰规则。
- 性能与灵活性权衡:__fastcall 更快但不支持可变参数,__cdecl 灵活但稍慢。

