Detailed Explanation of Calling Conventions in C/C++

In C/C++, a calling convention is a set of rules that defines how function parameters are passed, how the stack is maintained, and how return values are passed. This ensures that both the caller and the callee have a consistent understanding of the function call. Different calling conventions are suitable for different scenarios and primarily affect the size, performance, and interoperability of the code.

This article will demonstrate the impact of calling conventions on the compiled symbols of code compiled with GCC and VS2026 as examples.

What Constraints Do Calling Conventions Impose?

1. How parameters are passed: whether through the stack or through registers.

2. Responsibility for stack cleanup: whether the caller or the callee is responsible for cleaning up the stack.

3. Name mangling: how the compiler converts function names in the source code to symbol names in the object file, which is crucial for the linker to find the correct function.

__cdecl

Applicable scenarios: Default calling convention for C language.

Parameter passing: Parameters are pushed onto the stack from right to left.

Stack maintenance: The caller cleans up the stack, meaning the caller actively restores the stack pointer after the function call.

Name mangling:

On Windows platform (MSVC compiler):

Symbol names are prefixed with an underscore after compilation, such as _add.

This mangling is how the Windows linker distinguishes between functions with different calling conventions.

On non-Windows platforms (GCC/Clang):

On x86-64 (64-bit) architecture, almost all calling conventions primarily use registers (e.g., System V AMD64 ABI). GCC ignores stdcall/fastcall attributes, and name mangling follows C++ name mangling rules or simple C rules.

On x86 (32-bit) architecture, GCC does perform name mangling for cdecl functions, but the rules differ from Windows/MSVC. For example, the symbol name is the function name itself, which is the default behavior of GCC.

If extern “C” is used in C++, the compiled symbol will follow C conventions; otherwise, it will follow C++ name mangling (including parameter types).

Characteristics: Supports variable arguments (e.g., printf) because only the caller knows the actual number of parameters passed, allowing for correct stack cleanup. However, the caller must clean the stack on each call, which may increase code size.

Example

#include <stdio.h>

// __cdecl is the default for GCC compiler
int __attribute__((cdecl)) add_cdecl(int a, int b) {
    return a + b;
}

int main() {
    int x = 5, y = 10;
    // Call __cdecl function
    int result = add_cdecl(x, y);
    printf("Result of add_cdecl(%d, %d) is: %d\n", x, y, result);
    // Variable argument function is a typical example of __cdecl
    printf("This is a variadic function call: %d, %s\n", 42, "hello");
    return 0;
}

Compile with gcc and view the compiled symbols

gcc -c temp.c temp.o

nm temp.o 

0000000000000000 T add_cdecl

0000000000000018 T main

                 U printf

View assembly

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                                             ;# Caller cleans the stack
 .cfi_endproc

Compile with g++ and view the compiled symbols

g++ -c temp.c temp.o

nm temp.o 

# g++ compiler mangles the function name according to C++ rules.

0000000000000000 T _Z9add_cdeclii

0000000000000018 T main

                 U printf

Use extern to modify add_cdecl

extern "C" int __attribute__((cdecl)) add_cdecl(int a, int b) {
    return a + b;
}

Compile with g++ and view the compiled symbols

g++ -c temp.c temp.o

nm temp.o 

# After extern "C" modification, the function will generate symbols according to C conventions.

0000000000000000 T add_cdecl

0000000000000018 T main

                 U printf

Compile with VS 2026 and view the compiled symbols

// C compilation
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
_add_cdecl

// C++ compilation
cl /c temp.cpp
dumpbin /symbols Test.obj | findstr add
?add_cdecl@@YGHHH@Z (int __stdcall add_cdecl(int,int))

__stdcall

Applicable scenarios: Commonly used in Windows API (e.g., MessageBox), must be explicitly declared.

Parameter passing: Parameters are pushed onto the stack from right to left (same as __cdecl).

Stack maintenance: The callee cleans up the stack, restoring the stack pointer before returning from the called function.

Name mangling:

On Windows platform (MSVC compiler):

Symbol names are prefixed with an underscore and suffixed with @ and the total byte count of parameters, such as _add@8.

This mangling is how the Windows linker distinguishes between functions with different calling conventions.

On non-Windows platforms (GCC/Clang):

On x86-64 (64-bit) architecture, almost all calling conventions primarily use registers (e.g., System V AMD64 ABI). GCC ignores stdcall/fastcall attributes, and name mangling follows C++ name mangling rules or simple C rules.

On x86 (32-bit) architecture, GCC does perform name mangling for stdcall functions, but the rules differ from Windows/MSVC.

Characteristics: Does not support variable arguments (the callee cannot know the number of parameters, making stack cleanup impossible). Since the callee cleans the stack, the generated code is more compact, but flexibility is lower.

Example

int __attribute__((stdcall)) add_stdcall(int a, int b) {
    return a + b;
}

View assembly

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   ;  Callee cleans the stack (ret 8)
 .cfi_endproc

Compile with gcc and view the compiled symbols

gcc -m32 temp.c -c temp.o

nm temp.o 
         U _GLOBAL_OFFSET_TABLE_
00000000 T __x86.get_pc_thunk.ax
00000000 T add_stdcall
00000019 T main
         U printf

Compile with g++ and view the compiled symbols

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

Compile with VS 2026 and view the compiled symbols

// Compile C code
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
_add@8

// Compile C++
cl /c temp.cpp
dumpbin /symbols temp.obj | findstr add
?add@@YGHHH@Z (int __stdcall add(int,int))

__fastcall

Applicable scenarios: Used in performance-critical scenarios, passing some parameters through registers to reduce stack operations.

Parameter passing: The first two (or more, depending on the compiler) integer/pointer parameters are passed through registers (e.g., ECX, EDX in x86), while the remaining parameters are pushed onto the stack from right to left.

Stack maintenance: The callee cleans up the stack.

Name mangling:

On Windows platform (MSVC compiler):

Function symbol names are specially mangled, usually prefixed with @ and suffixed with @ and the total byte count of parameters. For example, int add(int a, int b) would be mangled as @add@8.

This mangling is how the Windows linker distinguishes between functions with different calling conventions.

On non-Windows platforms (GCC/Clang):

On x86-64 (64-bit) architecture, almost all calling conventions primarily use registers (e.g., System V AMD64 ABI). GCC ignores stdcall/fastcall attributes, and name mangling follows C++ name mangling rules or simple C rules.

On x86 (32-bit) architecture, GCC does perform name mangling for fastcall functions, but the rules differ from Windows/MSVC.

Characteristics: Its main optimization is passing some parameters through registers (usually the first two integer or pointer parameters) to reduce stack operations, thereby improving the efficiency of function calls.

Example

int __attribute__((fastcall)) add_fastcall(int a, int b) {
    return a + b;
}

Compile with gcc and view the compiled symbols

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

View assembly

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)  ; Store the value of ECX register (first parameter a) in local variable space on the stack
 movl %edx, -8(%ebp)  ; Store the value of EDX register (second parameter b) in local variable space on the stack
 movl -4(%ebp), %edx
 movl -8(%ebp), %eax
 addl %edx, %eax
 leave
 .cfi_restore 5
 .cfi_def_cfa 4, 4
 ret
 .cfi_endproc

Compile with g++ and view the compiled symbols

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

Compile with VS 2026 and view the compiled symbols

// C compilation
cl /c temp.c
dumpbin /symbols temp.obj | findstr add
@add@8

// C++ compilation
cl /c temp.cpp
dumpbin /symbols temp.obj | findstr add
?add@@YIHHH@Z (int __fastcall add(int,int))

Conclusion

This article only lists the commonly mentioned calling conventions; other calling conventions (e.g., __thiscall) are not enumerated here, but this is sufficient.

The main purpose of this article is to illustrate the impact of different calling conventions on the generated code symbols after compilation, which significantly affects the calling of library functions.

If the calling convention of the application does not match the calling convention used when compiling the called library, it is very likely to lead to successful compilation but failed linking errors (common in Windows), and may also encounter some inexplicable runtime exceptions.

Finally, here are some suggestions regarding calling conventions:

1. Compatibility: The caller and callee must use the same calling convention; otherwise, it may lead to stack corruption, parameter errors, and crashes.

2. Cross-compiler differences: Different compilers (e.g., MSVC, GCC, Clang) may implement certain conventions (e.g., __fastcall) differently, so caution is needed when calling across compilers.

3. Name mangling: If function name mangling is inconsistent during linking (e.g., mixing C and C++ calls), it may lead to “undefined symbol” errors, requiring the use of extern “C” to unify the mangling rules.

4. Performance vs. flexibility trade-off: __fastcall is faster but does not support variable arguments, while __cdecl is flexible but slightly slower.

Leave a Comment