Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way values travel into and out of a call

Argument registers by convention, the stack for the overflow, floating-point registers, a hidden pointer for a big struct, this in the first argument register, system-call registers, and inline-asm constraints. The ABI and the value's type decide.

Ask what kind of call it is, then what kind of value.

  • An ordinary function call → the platform's calling convention. Integers and pointers go in argument registers in order (System V: rdi rsi rdx rcx r8 r9; Windows: rcx rdx r8 r9; ARM64: x0–x7; RISC-V: a0–a7), and the result comes back in rax / x0 / a0.
  • float or double → the floating-point registers (xmm0…, d0/v0…, fa0…), counted separately on System V, ARM64 and RISC-V but sharing positions on Windows.
  • More arguments than registers → the rest go on the stack, in order, at fixed offsets above the return address (on Windows, above the 32-byte shadow space too).
  • A struct result too big for two registers → the caller passes a hidden pointer to space for it (x8 on ARM64; rdi / a0 as a hidden first argument on System V and RISC-V; rcx on Windows).
  • A member function → this is the hidden first argument.
  • A Linux system call → not the function convention: number in rax / x8 / a7, arguments in their own registers (r10 for the fourth on x86-64), then syscall / svc #0 / ecall.
  • A few instructions of inline assembly → you name the registers with operand constraints and the compiler moves values in and out.

The calling conventions

Formal. Across a call, a callee-saved register must hold the same value on return as on entry (the callee saves and restores it if it uses it); a caller-saved register may hold anything on return (a caller that needs its value afterwards saves it before the call).

ABI arguments (integer) result callee-saved
System V AMD64 (Linux, macOS x86-64) rdi, rsi, rdx, rcx, r8, r9 rax (rdx:rax) rbx, rbp, r12–r15, rsp
Microsoft x64 (Windows) rcx, rdx, r8, r9 (plus 32 reserved stack bytes) rax rbx, rbp, rdi, rsi, r12–r15, rsp
ARM64 (Linux, Windows, Apple) x0–x7 x0 (x0:x1) x19–x28, x29, sp
RISC-V (Linux) a0–a7 a0 (a0:a1) s0–s11, sp

Every register not listed as callee-saved (and not reserved, like ARM64's x18 on Apple and Windows) is caller-saved, including all argument and result registers and the return-address register x30 / ra.

In plain English. Some registers are promised to survive a call, and any function that borrows one has to put it back. The rest may be wiped by any call, so if you still need one, keep a copy somewhere safe first.

Example. On x86-64 Linux, a value kept in rbx survives call f; one kept in rcx may not. A function that uses r12 starts with push r12 and ends with pop r12 before ret.

Why this rule exists. Splitting the registers lets short functions use the caller-saved ones with no saving at all, while long-lived values sit in callee-saved ones and cost a save only in functions that need them.

Six argument registers, rax back, and the callee-saved set.

Four argument registers, 32 bytes of shadow space, no red zone.

Eight argument registers; Apple puts variadic arguments on the stack.

Section

RISC-V LP64D

a0–a7, and a 32-bit int kept sign-extended in its register.

When registers aren't enough

Stack arguments, small structs in register pairs, big ones through a hidden pointer.

An argument that does not fit in the argument registers and is passed in memory on the stack instead.
How a large struct comes back from a function - the caller passes a hidden address and the function writes the struct there.
32 bytes a Windows caller must leave free on the stack just above the return address, where the called function may store its four register arguments.

Example

A Windows caller with nothing to push still does sub rsp, 40: 32 bytes of shadow space plus 8 for alignment.

Other kinds of value

A double argument is in xmm0 or d0, never in rdi or x0.

this arrives as the hidden first argument.

Calls that aren't ordinary calls

Formal. On Linux, a system call puts its system call number and up to six arguments in registers, executes the system call instruction, and receives one result:

ISA number arguments 1-6 instruction result
x86-64 rax rdi, rsi, rdx, r10, r8, r9 syscall rax
ARM64 x8 x0-x5 svc #0 x0
RISC-V a7 a0-a5 ecall a0

A result from −4095 to −1 is an error: the negated error code (−errno). On x86-64, syscall overwrites rcx (with the return address) and r11 (with the flags), which is why argument 4 uses r10 instead of rcx. Numbers are fixed per ISA: write is 1 and exit is 60 on x86-64; write is 64 and exit is 93 on ARM64 and RISC-V.

In plain English. To ask the Linux kernel for something, put the number of the service in one register and the details in the next few, then run the one special instruction that switches into kernel mode. The answer comes back in a register, and a small negative answer means it failed. The registers are almost the function-call ones; on x86-64 the fourth detail moves from rcx to r10.

Example. Write 6 bytes from msg to standard output (file 1) on x86-64 Linux: mov eax, 1 / mov edi, 1 / lea rsi, [rip + msg] / mov edx, 6 / syscall. Afterwards rax holds 6, the number of bytes written. On ARM64 the same call is mov x8, #64, mov x0, #1, the address in x1, mov x2, #6, svc #0.

Why this rule exists. The kernel can't trust the program's stack or its calling convention, so it takes everything in registers it reads the moment it is entered. Linux promises never to renumber its system calls, so programs that use these numbers keep working on every later kernel. That promise is Linux's own; the other operating systems this course covers make no such promise.

Almost the function registers, but not quite, and a trap instruction instead of a call.

extern "C" fixes the name, not the registers; the convention does that.

Section

Inline assembly

Operand constraints tell the compiler which registers carry values in and out.