The rulebook the instruction set leaves out
From the last three guides you can read a register like rax, you know that `call` pushes a return address and jumps, that `ret` pops it back, and that the stack pointer rsp marks the top of the stack at the instruction level. But notice what the instruction set never told you. When you write `f(3, 4)` in C and it compiles to a `call`, where do 3 and 4 go? Into rax? Onto the stack? In what order? And after `f` returns, where do you find its result? The hardware genuinely does not care — `call` just jumps. Any consistent choice would work, as long as the caller and the callee agree on it.
That agreement is the calling convention: a fixed, written-down set of rules for how a function is called and how it returns. It nails down which registers carry the first argument, the second, and so on; which register holds the return value; how the stack must be arranged at the moment of the `call`; and — the subtle part we will spend most time on — who is responsible for preserving each register across the call. None of it is enforced by the CPU. It is a convention in the literal sense: a treaty everyone follows so that code written by different people, at different times, in different files, still fits together.
Where arguments and results live
Here is the concrete part you can hold in your head. Under System V AMD64, the first six integer or pointer arguments travel in registers, in a fixed order: rdi, rsi, rdx, rcx, r8, r9. So in `f(3, 4)`, the compiler moves 3 into rdi and 4 into rsi before the `call`, and `f` simply reads them there — no memory traffic at all. A seventh and eighth argument would spill onto the stack, pushed in reverse order so that the first stack argument sits closest to the top. The return value comes back in rax. Floating-point arguments and results ride a separate set of registers (xmm0 through xmm7), which keeps the integer registers free for the common case.
This is exactly why a small function can be astonishingly cheap. For `r = add(3, 4)`, the call site is just `mov edi, 3` then `mov esi, 4` (loading the low 32 bits of rdi and rsi) and `call add`; inside, `add` does `lea eax, [rdi+rsi]` and `ret`, leaving the answer in rax — no memory touched beyond the `call`/`ret` pair. Contrast that with crossing into the kernel, which the next rung covers — a system call is not an ordinary function call and does not use this convention; it traps into the kernel with its own argument-register rules and a far higher cost. Keeping that boundary crisp matters: `call add` stays in your process at user level, whereas a `syscall` instruction changes privilege.
Caller-saved vs callee-saved: the heart of it
Now the subtle rule that trips up everyone reading their first disassembly. There are only so many registers, and both a function and the function it calls want to use them. So who is allowed to clobber what? The convention divides the registers into two camps. Caller-saved (also called volatile) registers — rax, rcx, rdx, rsi, rdi, r8 through r11 — may be freely overwritten by any function you call. If you had a value in one of these and you still need it after a `call`, you, the caller, must save it first (push it, or stash it somewhere) and restore it afterward. Callee-saved (non-volatile) registers — rbx, rbp, and r12 through r15 — come with the opposite promise: any function that wants to use one must save its old contents on entry and restore them before returning, so that from the caller's point of view they appear untouched.
Think of it as two ways to split the chore of preservation, and the split exists purely to avoid wasted work. Caller-saved registers are perfect for short-lived scratch values: if the caller has nothing worth keeping, nobody spends an instruction saving anything. Callee-saved registers are perfect for values a function holds across many calls — a loop counter it keeps in rbx, say — because it saves rbx just once on entry rather than around every inner `call`. This is the caller-saved versus callee-saved distinction, and the moment it clicks, disassembly stops looking like noise: those `push rbx` / `pop rbx` pairs bracketing a function are it honoring the callee-saved promise.
The prologue and epilogue: the convention in code
All of this becomes visible in the stereotyped instructions the compiler emits at the start and end of nearly every function — the prologue and epilogue. The prologue sets up the function's stack frame: classically `push rbp` (save the caller's base pointer — note rbp is callee-saved, so we must), then `mov rbp, rsp` (anchor rbp to the current top so local variables can be addressed at fixed offsets like `[rbp - 8]`), then `sub rsp, N` (carve out N bytes of space for locals by lowering rsp). The epilogue is the exact mirror that the previous guide on the stack pointed at: it restores rsp and rbp and hands control back. Here it is end to end.
Prologue (function entry):
push rbp ; save caller's rbp (it is callee-saved)
mov rbp, rsp ; rbp now points at the frame base
sub rsp, 0x10 ; reserve 16 bytes for locals
push rbx ; save rbx too, if we will use it
... ; body: locals at [rbp - 8], etc.
Epilogue (function exit):
pop rbx ; restore rbx
mov rsp, rbp ; discard locals (or: leave)
pop rbp ; restore caller's rbp
ret ; pop return address, jump backRead those two blocks together and the whole convention is right there in front of you. The prologue's `push rbp` and `push rbx` are the callee-saved promise being paid. The `sub rsp, 0x10` is local storage. The epilogue restores each saved register in reverse order (last pushed, first popped — the stack is LIFO) and finishes with `ret`, which pops the return address `call` left and jumps home. Modern compilers at `-O2` often omit the rbp dance entirely (freeing rbp as a general register and addressing locals off rsp directly), and may fold `mov rsp,rbp; pop rbp` into the single `leave` instruction — so do not be surprised when an optimized function has a leaner prologue than this textbook one.
From convention up to the ABI
The calling convention is the part you watch in action, but it is one clause of a larger contract: the ABI, the Application Binary Interface. Where an API is a source-level agreement (function names, parameter types — what you write against), an ABI is the binary-level agreement: how it all looks once compiled to machine code. The ABI fixes the calling convention, and also the exact size and alignment of every type (`is long 8 bytes? where does padding go in a struct?`), how a struct is laid out and whether it is returned in registers or via a hidden pointer, the format of the object files, and which register holds the return address. It is the layer that lets a `.o` from one compiler link against a `.o` from another.
This is the quiet reason so much of the software world speaks C. The C ABI on a given platform is stable and documented, so a library compiled years ago in C still links and calls correctly today, and Rust, Python, Go, and others reach across to C libraries by speaking that same ABI — not C the language, but the binary treaty C pinned down. Stability is also why breaking an ABI is a serious event: change a struct's layout or a function's argument registers in a shipped shared library, and every program built against the old version mis-reads the new one — passing arguments to the wrong places, reading fields at wrong offsets — without a single compiler error, because no source ever changed.
So hold the layering straight, because muddling it is the classic confusion. The instruction set says what a single instruction does. The calling convention says how to pass arguments and who preserves which register across a `call`. The ABI is the full binary contract that includes the convention plus type layout and file formats. And none of it is checked by the CPU at run time — get it wrong and there is no exception, only a wrong answer or a crash. That is exactly the skill the final guide of this rung builds: with this contract in mind, you will read the disassembly of your own compiled code and watch the convention play out, instruction by instruction.