在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
// 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_OFFSETTABLE , %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 202 6 编译并查看编译后的符号
// 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_OFFSETTABLE, %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_OFFSETTABLE 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_OFFSETTABLE 00000000 T _Z11add_stdcallii 00000000 T __x86.get_pc_thunk.ax 00000019 T main
- VS 202 6 编译并查看编译后的符号
/ / 编译C代码 cl /c temp.c dumpbin /symbols temp.obj | findstr add _add@8
/ / 编译C++ cl /c temp.c pp 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_OFFSETTABLE 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_OFFSETTABLE, %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_OFFSETTABLE 00000000 T _Z12add_fastcallii 00000000 T __x86.get_pc_thunk.ax 00000020 T main
- VS 202 6 编译并查看编译后的符号
// 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 灵活但稍慢。

