x86-64 for OS Builders · stage 1 of 6

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 r8r15. 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 r12r15. 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 — x0x30, arguments in x0x7, and exception levels EL0EL3 instead 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.

That's the lesson — free to read. Create a free account to get the curated canon, the project brief, and build it yourself.

Create a free account →