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 inrax/x0/a0. floatordouble→ 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 (
x8on ARM64;rdi/a0as a hidden first argument on System V and RISC-V;rcxon Windows). - A member function →
thisis the hidden first argument. - A Linux system call → not the function convention: number in
rax/x8/a7, arguments in their own registers (r10for the fourth on x86-64), thensyscall/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.
Section
Microsoft x64 (Windows)
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.
Example
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.
Section
Classes and member functions
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.
Section
System calls on Linux
Almost the function registers, but not quite, and a trap instruction instead of a call.
Section
Calling across C++ and assembly
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.