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 EAX in 32-bit architectures. Floating-point numbers are returned in the floating-point register stack (ST(0)).
  • Volatile Registers: Registers EAX, ECX, and EDX are 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, cdecl natively supports variadic functions, such as printf, 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 ECX and EDX registers.
  • 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 in EAX (or ST(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, fastcall does 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, and R9 (or XMM0–XMM3 for floating-point).
  • System V AMD64 ABI (Linux/macOS): Passes up to six integer/pointer arguments via registers: RDI, RSI, RDX, RCX, R8, and R9, and up to eight floating-point arguments via XMM0–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.