Registers, modes & the calling convention
You'll learn: the CPU's working state, the three modes an x86-64 machine can be in, and why a PC still boots as if it were 1981.
The registers
Sixteen 64-bit general-purpose registers: rax rbx rcx rdx rsi rdi rbp rsp and
r8–r15. Each is addressable at narrower widths — rax / eax / ax /
al — which is how 64-, 32-, 16- and 8-bit code share the same silicon.
One trap worth knowing now: writing to a 32-bit register zeroes the upper 32
bits, but writing to a 16- or 8-bit one leaves the rest alone. So mov eax, 5
clears the top half of rax while mov ax, 5 does not.
Beyond those: rip (the instruction pointer, not directly writable),
rflags (carry, zero, sign, overflow — and IF, the interrupt flag that
decides whether the CPU listens to hardware at all), and the segment registers
cs ds es fs gs ss, which mean much less than they used to.
Three modes, because of history
An x86-64 CPU can run in three modes, and it boots into the oldest:
Real mode (16-bit). What the CPU wakes up in, for compatibility with the
8086. 16-bit registers, no memory protection, and addresses formed as
segment × 16 + offset — giving 20 bits, so 1 MiB total. BIOS services are
available here as software interrupts, which is the reason the boot code that
queries the memory map must run in real mode: INT 15h/E820 exists nowhere else.
Protected mode (32-bit). Adds privilege levels, memory protection and a 4 GiB address space. Segmentation becomes descriptor-based — the segment registers now hold selectors into a descriptor table rather than raw addresses.
Long mode (64-bit). The modern mode: 64-bit registers, a vast address space, and segmentation almost entirely retired — but paging is mandatory. You cannot enter long mode without page tables already built, which is why the bootloader has to construct them before it can hand off.
The consequence: a bootloader's job is essentially a staged escape from 1981. Start in real mode, use BIOS while you still can, switch to protected mode, build page tables, switch to long mode, then jump to a kernel that never thinks about any of this again.
The stack
rsp points at the top of the stack, which grows downward. push
decrements then stores; pop loads then increments. call pushes the return
address; ret pops it. rbp conventionally anchors the current frame, which is
what lets a debugger walk a backtrace.
Two things a kernel must remember: the stack is just memory you designated, so
someone has to set rsp to point at real memory before any call happens —
one of the first things boot code does. And nothing checks for overflow. Run off
the end and you silently corrupt whatever is below.
The calling convention
Rust and C agree on the System V AMD64 ABI, and knowing the first two slots explains a lot of kernel code:
| Argument | 1 | 2 | 3 | 4 | 5 | 6 | return |
|---|---|---|---|---|---|---|---|
| Register | rdi |
rsi |
rdx |
rcx |
r8 |
r9 |
rax |
Callee-saved: rbx rbp r12–r15. Everything else the callee may clobber.
This is why a kernel entry point looks the way it does:
extern "C" fn _start(boot: *const BootInfo, magic: u64) -> !
The bootloader puts a pointer in rdi and a magic number in rsi, then jumps.
extern "C" tells the compiler to read its arguments from exactly those
registers. The "contract" between bootloader and kernel is, quite literally, two
registers.
vs. ARM64: the same ideas with different names —
x0–x30, arguments inx0–x7, and exception levelsEL0–EL3instead of rings. ARM64 has no legacy 16-bit mode to escape, which is why its boot path is dramatically shorter and there's no equivalent of the long-mode dance.