Build an OS from Scratch · stage 2 of 19

Boot stub → protected mode → first Rust

You'll build: the first two links of the boot chain — a 512-byte 16-bit assembly stub (stage0) that prepares the machine, and a 32-bit Rust bootloader that it hands control to.

How an x86 PC boots

When a BIOS PC powers on, the CPU is in 16-bit real mode — the same instruction set as a 1978 8086, with 1 MiB of addressable memory and direct access to BIOS services. The BIOS finds a bootable disk, loads its first 512-byte sector (the boot sector, or MBR) to physical address 0x7C00, and jumps there. That's it. Everything else is our job.

We can't write that first sector in Rust: rustc has no 16-bit code generator. So the only assembly in this whole course is a tiny stub, stage0.asm, that does the few things which must happen in 16-bit real mode, then switches the CPU into 32-bit protected mode and jumps into Rust.

stage0 must, in order:

  1. Collect the memory map. The BIOS INT 15h, AX=0xE820 service reports the machine's RAM regions. It's only callable in real mode, so we grab it now and stash it in low memory for the Rust stages to read later.
  2. Load the rest off disk. Using INT 13h (also real-mode-only), read the 32-bit Rust bootloader and the kernel from disk into memory. After we leave real mode there is no BIOS disk service — so we load everything up front.
  3. Enable the A20 line. A legacy quirk: address bit 20 is forced to 0 until you enable it, so memory above 1 MiB would wrap. One out to port 0x92.
  4. Enter protected mode. Load a flat 32-bit GDT, set CR0.PE, and far-jump to reload CS. The CPU now decodes 32-bit instructions and can address 4 GiB.
  5. Jump to Rust at a fixed address (0x8000).

From 0x8000 onward it's Rust — #![no_std], #![no_main], one _start that the linker pins to the load address. In this stage the Rust bootloader just proves it's alive: it drives the serial port itself (the kernel isn't running yet) and prints [PASS] pm_rust.

vs. Linux: the shape is identical. Linux's real-mode entry lives in arch/x86/boot/ (header.S, main.c) — it too gathers the E820 map (via INT 15h), enables A20, and switches to protected mode in pmjump.S, before decompressing and entering the kernel proper. GRUB/the boot protocol play the role our stage0 plays. We're building a miniature of the real thing.

Why a separate 32-bit target

The kernel is built for x86_64-unknown-none. The bootloader runs before long mode, in 32-bit protected mode, so it needs its own target: os/boot/x86-none-32.json — a 32-bit x86 ELF, no MMX/SSE (the FPU/vector units aren't set up this early), panic = "abort". It's built with build-std (there's no prebuilt core for this target) and -Zjson-target-spec.

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 →