Skip to content

Calling Conventions and ABI — Function Calls in Compilers

DodaTech Updated 2026-06-23 7 min read

In this tutorial, you'll learn about Calling Conventions and ABI. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Calling conventions define the low-level protocol for function calls — how arguments are passed, how return values are delivered, which registers must be preserved, and how the stack is managed — forming the Application Binary Interface (ABI) contract between caller and callee.

What You'll Learn & Why It Matters

In this tutorial, you will learn how calling conventions work, the major conventions (System V AMD64, ARM AAPCS, Microsoft x64), how to implement function prologues and epilogues, and how to design custom conventions. Understanding calling conventions is essential for writing compiler backends, implementing foreign function interfaces, and debugging at the assembly level.

Real-world use: The Rust compiler implements the Rust ABI with custom calling conventions that enable its ownership model, while also supporting C FFI through the System V AMD64 ABI. Durga Antivirus Pro uses calling convention knowledge to reconstruct function boundaries and arguments from stripped executables.

Prerequisites

You should understand code generation basics and register allocation. Familiarity with C programming and assembly language is assumed.

The Call Stack and Stack Frame

When a function is called, a new stack frame is created to hold local variables, saved registers, and return address.

graph TD
    subgraph "Stack Frame Layout (x86-64)"
        A["High Addresses"]
        B["Caller's Frame"]
        C["Return Address"]
        D["Saved RBP"]
        E["Local Variables"]
        F["Callee-Saved Regs"]
        G["Argument Build Area"]
        H["Low Addresses (RSP)"]
    end
    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G
    G --> H
    style C fill:#FF9800,color:#fff
    style D fill:#2196F3,color:#fff
    style E fill:#4CAF50,color:#fff

System V AMD64 Calling Convention

The most common convention on Linux and Unix systems uses register-first argument passing.

class SystemVABI:
    def __init__(self):
        self.int_arg_regs = ['rdi', 'rsi', 'rdx', 'rcx', 'r8', 'r9']
        self.float_arg_regs = ['xmm0', 'xmm1', 'xmm2', 'xmm3',
                               'xmm4', 'xmm5', 'xmm6', 'xmm7']
        self.return_reg_int = 'rax'
        self.return_reg_float = 'xmm0'
        self.callee_saved = ['rbx', 'rbp', 'r12', 'r13', 'r14', 'r15']

    def generate_call(self, func_name, arg_types):
        int_reg_idx = 0
        float_reg_idx = 0
        stack_args = []
        instructions = []

        for arg_type in arg_types:
            if arg_type == 'int' and int_reg_idx < len(self.int_arg_regs):
                reg = self.int_arg_regs[int_reg_idx]
                instructions.append(f"  mov {reg}, arg{int_reg_idx}")
                int_reg_idx += 1
            elif arg_type == 'float' and float_reg_idx < len(self.float_arg_regs):
                reg = self.float_arg_regs[float_reg_idx]
                instructions.append(f"  movsd {reg}, farg{float_reg_idx}")
                float_reg_idx += 1
            else:
                stack_args.append(arg_type)

        instructions.append(f"  call {func_name}")
        if stack_args:
            instructions.append(f"  add rsp, {len(stack_args) * 8}")
        instructions.append("  ; return value in rax/xmm0")
        return instructions

    def generate_function_prologue(self, local_var_size=0):
        instrs = ["  push rbp", "  mov rbp, rsp"]
        if local_var_size > 0:
            instrs.append(f"  sub rsp, {local_var_size}")
        return instrs

    def generate_function_epilogue(self):
        return ["  mov rsp, rbp", "  pop rbp", "  ret"]

abi = SystemVABI()
call = abi.generate_call('foo', ['int', 'int', 'int', 'float', 'int'])
for instr in call:
    print(instr)

Expected output:

  mov rdi, arg0
  mov rsi, arg1
  mov rdx, arg2
  movsd xmm0, farg3
  mov rcx, arg4
  call foo
  ; return value in rax/xmm0

ARM AAPCS Calling Convention

The ARM Architecture Procedure Call Standard uses four registers for integer arguments.

class AAPCS:
    def __init__(self):
        self.int_arg_regs = ['r0', 'r1', 'r2', 'r3']
        self.callee_saved = ['r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11']
        self.return_reg = 'r0'

    def generate_prologue(self):
        return ["  push {fp, lr}", "  mov fp, sp",
                "  ; sp must remain 8-byte aligned"]

    def generate_call(self, func_name, num_int_args):
        instructions = []
        for i in range(min(num_int_args, 4)):
            instructions.append(f"  mov r{i}, arg{i}")
        if num_int_args > 4:
            for i in range(4, num_int_args):
                instructions.append(f"  ldr r4, =arg{i}")
                offset = (i - 4) * 4
                instructions.append(f"  str r4, [sp, #{offset}]")
        instructions.append(f"  bl {func_name}")
        return instructions

    def generate_epilogue(self):
        return ["  mov sp, fp", "  pop {fp, lr}", "  bx lr"]

arm_abi = AAPCS()
call = arm_abi.generate_call('bar', 5)
for instr in call:
    print(instr)

Expected output:

  mov r0, arg0
  mov r1, arg1
  mov r2, arg2
  mov r3, arg3
  ldr r4, =arg4
  str r4, [sp, #0]
  bl bar

Callee-Saved vs Caller-Saved Registers

This distinction determines which registers must be preserved across function calls.

class RegisterSaveManager:
    def __init__(self, convention='systemv'):
        mapping = {
            'systemv': ['rbx', 'rbp', 'r12', 'r13', 'r14', 'r15'],
            'aapcs': ['r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11'],
        }
        self.callee_saved = mapping[convention]
        self.live_registers = set()

    def mark_live(self, reg):
        self.live_registers.add(reg)

    def generate_save_code(self):
        to_save = [r for r in self.callee_saved if r in self.live_registers]
        if not to_save:
            return []
        return [f"  push {', '.join(to_save)}"]

    def generate_restore_code(self):
        to_save = [r for r in self.callee_saved if r in self.live_registers]
        if not to_save:
            return []
        to_save.reverse()
        return [f"  pop {', '.join(to_save)}"]

    def should_spill_before_call(self, reg):
        caller_saved = {
            'systemv': ['rax', 'rcx', 'rdx', 'rsi', 'rdi', 'r8', 'r9', 'r10', 'r11'],
            'aapcs': ['r0', 'r1', 'r2', 'r3'],
        }
        return reg in caller_saved

manager = RegisterSaveManager('systemv')
manager.mark_live('rbx')
manager.mark_live('r12')
manager.mark_live('r13')

for instr in manager.generate_save_code():
    print(instr)
for reg in ['rbx', 'rcx', 'rdx', 'r12']:
    status = "caller-saved" if manager.should_spill_before_call(reg) else "callee-saved"
    print(f"  {reg}: {status}")

Expected output:

  push rbx, r12, r13
  rbx: callee-saved
  rcx: caller-saved
  rdx: caller-saved
  r12: callee-saved

Common Errors in Calling Convention Implementation

Error 1: Stack Misalignment

The System V AMD64 ABI requires RSP to be 16-byte aligned before a call. Pushing an odd number of registers or missing alignment adjustment causes SSE instructions to fault.

Error 2: Wrong Argument Register

Passing a float in an integer register or vice versa causes silent data corruption. The ABI specifies separate register classes for integer and floating-point arguments.

Error 3: Missing Callee-Saved Save

If the callee modifies a callee-saved register without saving it, the caller's values are corrupted, causing subtle bugs that appear only after specific call chains.

Error 4: Incorrect Return Value Handling

Aggregate return values (structs larger than 16 bytes) are returned via a hidden pointer argument. Ignoring this convention causes stack corruption or incorrect results.

Error 5: Variadic Function Mismatch

Variadic functions (like printf) on x86-64 require AL to contain the number of vector register arguments. Failing to set AL correctly causes argument misalignment.

Practice Questions

Question 1

What is the difference between callee-saved and caller-saved registers?

Show answer Callee-saved registers must be preserved by the function being called; if modified, they must be saved and restored. Caller-saved registers can be freely modified by the callee, so the caller must save them if their values are needed after the call.

Question 2

How does the System V AMD64 ABI pass the first six integer arguments?

Show answer They are passed in registers rdi, rsi, rdx, rcx, r8, r9 in that order. Additional arguments are passed on the stack, pushed right-to-left so the first stack argument is at the lowest address.

Question 3

Why must the stack be 16-byte aligned before a call on x86-64?

Show answer The System V AMD64 ABI requires 16-byte alignment because many SSE instructions (like movdqa) require 16-byte aligned memory access. The call pushes an 8-byte return address, making RSP = 8 mod 16 at function entry.

Challenge

Implement a complete ABI code generator for a simple language with functions, struct parameters, and variadic arguments. Support both System V AMD64 and ARM AAPCS. Generate prologue, argument marshalling, call sequences, and epilogue.

FAQ

What is the purpose of a platform ABI?

The platform ABI ensures that code compiled by different compilers, languages, and versions can interoperate. It standardizes calling conventions, type layouts, name mangling, and Exception Handling.

How do custom calling conventions like Rust's affect performance?

Rust's custom ABI passes some arguments in registers that the C ABI passes on the stack, using niche optimization to encode enum variants without extra storage. This improves performance at the cost of C interoperability.

What is the relationship between calling conventions and security?

Calling conventions directly affect security: stack frame layout determines buffer overflow exploit feasibility, return address protection (shadow stacks), and register clearing for information leakage prevention.

graph LR
    A[Function Signature] --> B[Argument Classification]
    B --> C[Register Assignment]
    B --> D[Stack Assignment]
    C --> E[Prologue Generation]
    D --> E
    E --> F[Call Sequence]
    F --> G[Epilogue Generation]
    style E fill:#2196F3,color:#fff,stroke-width:2px

Summary

Calling conventions define the binary protocol for function calls, specifying argument passing, register preservation, and stack management. The System V AMD64 and ARM AAPCS conventions use register-first strategies for performance, with clear distinctions between callee-saved and caller-saved registers. Correct ABI implementation is essential for compiler correctness and cross-language interoperability.


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro