The System V Calling Convention
rdi, rsi, rdx… who passes what
- › Explain why a calling convention must exist for functions to interoperate
- › State the System V AMD64 argument registers, the return register, and the order
- › Distinguish caller-saved from callee-saved registers and say why the split matters
- › Read a function call in assembly and identify each argument and the return value
A contract nobody can skip
Yesterday call and ret moved control in and out of a function. But a function also needs
data — its arguments — and it produces a result. Where do arguments go? Which registers is a
function allowed to clobber? If the caller and the callee disagree, everything breaks. So there's a
contract: a fixed agreement on where arguments live, where the return value goes, and who is
responsible for preserving which registers. On Linux and macOS for x86-64 that contract is the
System V AMD64 ABI. Every compiled function obeys it, which is why code from different compilers
and languages can call each other at all.
Where the arguments go
Integer and pointer arguments are passed in registers, in this exact order:
| Arg # | Register | Note |
|---|---|---|
| 1st | rdi | first integer/pointer argument |
| 2nd | rsi | |
| 3rd | rdx | |
| 4th | rcx | |
| 5th | r8 | |
| 6th | r9 | sixth; further args go on the stack |
| return | rax | the function's return value comes back here |
Reading a call
Here's add(2, 3) set up and called. Watch the convention:
1 mov edi, 2 ; 1st arg -> rdi (edi = its 4-byte view, since int)2 mov esi, 3 ; 2nd arg -> rsi3 call add ; jump in; add reads edi/esi, leaves result in eax4 ; now eax = 5 ; return value is in rax/eaxYou didn't need add's source. The convention *told* you: first argument in rdi, second in rsi,
result in rax. Reading calls becomes mechanical — find the movs into rdi/rsi/rdx… before a call,
and you've found the arguments; look at rax after, and you've found the result.
Who preserves what: caller-saved vs callee-saved
There's a second half of the contract, and it prevents chaos when functions clobber registers. The registers are split into two groups:
- Callee-saved (rbx, rbp, r12–r15): a function that uses these must save and restore them, so
- the caller sees them unchanged across the call. If you hold a value in rbx across a call, it
- survives.
- Caller-saved (rax, rcx, rdx, rsi, rdi, r8–r11): a function may freely clobber these. If you
- need a value in one of them across a call, *you* must save it first (e.g. push it), because the
- callee might trash it.
push rbx in a prologue and pop rbx in the epilogue). For caller-saved, the *caller* does. Get
this wrong by hand and you get corruption bugs that look like magic — the ABI is what makes separately
compiled code safe to combine.memcpy(dst, src, n) — a 3-argument function. Before the call, which register holds
dst, which holds src, and which holds n? And where would memcpy's return value appear?Confirm the convention
Write abi.c with a function f(int a, int b, int c, int d) that returns a - b + c - d, called from main with four distinct constants. Compile at -O0 and disassemble. In main's disassembly, confirm the four arguments are loaded into rdi, rsi, rdx, rcx (in that order) before the call, and that after the call the result is read from rax. Then find f's prologue and see whether it saves any callee-saved register. Uses only the ABI rules from this lesson.
$ gcc -O0 -g abi.c -o abi$ objdump -d -M intel abi- › You matched each of the four arguments to rdi/rsi/rdx/rcx in order
- › You found the return value being taken from rax after the call
- › You can state the six integer-argument registers from memory
- › You can explain the difference between caller-saved and callee-saved