Understanding C++ Calling Conventions: cdecl and fastcall
A calling convention is a standardized, low-level protocol that
governs how functions exchange data with their callers at the
machine-code level. In C++, calling conventions define how parameters
are passed, how values are returned, which CPU registers are used, and
whether the caller or the callee is responsible for cleaning up the call
stack. This article explains what calling conventions are and details
how traditional conventions like cdecl and
fastcall handle register usage and stack management.
What is a Calling Convention?
When a C++ program calls a function, the compiler must translate that call into machine instructions. For this transfer of execution to succeed, both the calling function (the caller) and the receiving function (the callee) must adhere to the same contract. This contract is the calling convention.
A calling convention dictates:
- Argument Placement: Whether parameters are placed into general-purpose CPU registers or pushed onto the execution stack.
- Evaluation Order: The order in which arguments are evaluated and placed onto the stack (typically right-to-left or left-to-right).
- Stack Cleanup: Whether the caller or the callee restores the stack pointer to its previous position after the function completes.
- Register Preservation: Which registers the callee must preserve (callee-saved/non-volatile) and which registers the callee may overwrite (caller-saved/volatile).
The cdecl Calling
Convention
The cdecl (C declaration) convention is the historic
default for C and C++ programs on 32-bit x86 architectures.
Register Usage in
cdecl
Under cdecl, general-purpose registers are
not used to pass arguments. Instead:
- Argument Storage: All function arguments are pushed onto the call stack in right-to-left order.
- Return Values: Simple return types (integers,
pointers) are returned via a register, typically
EAXin 32-bit architectures. Floating-point numbers are returned in the floating-point register stack (ST(0)). - Volatile Registers: Registers
EAX,ECX, andEDXare designated as caller-saved. If the caller needs their contents preserved across the call, it must save them to the stack. - Stack Management: The caller is
responsible for cleaning up the stack after the function returns (by
adjusting the stack pointer, e.g.,
add esp, N). Because the caller handles cleanup,cdeclnatively supports variadic functions, such asprintf, where the callee does not know ahead of time how many arguments were passed.
The fastcall
Calling Convention
The __fastcall convention was created to optimize
function invocation speed by minimizing costly memory access operations
on the stack.
Register Usage in
fastcall
Under fastcall, general-purpose CPU registers are
directly allocated for the initial parameters:
- Argument Storage via Registers: In typical 32-bit
x86 implementations (such as Microsoft's compiler), the first two
arguments that fit within 32 bits are placed directly into the
ECXandEDXregisters. - Stack Overflow: Any remaining arguments beyond the
first two are pushed onto the stack in right-to-left order, identical to
cdecl. - Return Values: Like
cdecl, return values are placed inEAX(orST(0)for floating-point values). - Stack Management: Unlike
cdecl, the callee typically restores the stack pointer using a return instruction that pops bytes (e.g.,ret N). Because the callee must know the exact number of bytes pushed to clean up the stack,fastcalldoes not support variadic functions.
By keeping the initial arguments in ECX and
EDX, the CPU avoids writing those arguments to RAM (the
stack) and reading them back immediately inside the function body,
yielding faster execution for short, frequently called functions.
Architectural Context: x86 vs. x86-64
On 32-bit systems, developers frequently chose between
__cdecl, __stdcall, and
__fastcall to balance performance and binary size.
On modern 64-bit architectures (x86-64), these traditional 32-bit conventions are largely unified into standardized Application Binary Interfaces (ABIs):
- Microsoft x64 ABI: Uses a 4-register fast-call
convention where the first four arguments are passed in
RCX,RDX,R8, andR9(orXMM0–XMM3for floating-point). - System V AMD64 ABI (Linux/macOS): Passes up to six
integer/pointer arguments via registers:
RDI,RSI,RDX,RCX,R8, andR9, and up to eight floating-point arguments viaXMM0–XMM7.
In modern 64-bit C++, explicit keywords like __cdecl or
__fastcall are typically ignored or mapped directly to the
underlying 64-bit ABI, making aggressive register usage the default
behavior across all standard function calls.