Boot Process & Multiboot
What Happens When You Turn a Computer On
When you turn on a computer, it begins executing firmware stored in motherboard ROM. This code performs a power-on self-test, detects available RAM, and pre-initialises the CPU and hardware. Afterwards it looks for a bootable disk and starts booting the OS kernel.
On x86 there are two firmware standards: BIOS (Basic Input/Output System) and the newer UEFI. BIOS is old and outdated, but simple and well-supported on any x86 machine since the 1980s. We use BIOS.
BIOS Boot Sequence
- BIOS runs self-test and hardware initialisation
- BIOS looks for a bootable disk (checks the boot sector)
- BIOS loads the bootloader from the disk
- The bootloader switches the CPU from 16-bit real mode → 32-bit protected mode
- The bootloader loads the kernel into memory
- The bootloader jumps to the kernel entry point
The bootloader also queries the BIOS for memory maps and hardware information and passes that to the kernel.
The Multiboot Standard
To avoid every OS implementing its own bootloader, the Free Software Foundation created the Multiboot standard in 1995. It defines an interface between the bootloader and the OS:
Any Multiboot-compliant bootloader can load any Multiboot-compliant kernel.
The reference implementation is GNU GRUB, the most popular bootloader for Linux systems. To make a kernel Multiboot-compliant, we insert a Multiboot header at the beginning of the kernel file.
What GRUB Does For Us
- Scans the first 8KB of the kernel file for the magic number
0x1BADB002 - Reads the Multiboot header to understand what the kernel needs
- Loads the kernel into memory at address 1MB (
0x00100000) - Switches CPU to 32-bit protected mode
- Jumps to the kernel’s entry point (
_start)
The Multiboot Header
section .multiboot_header
align 4
MULTIBOOT_MAGIC equ 0x1BADB002
MULTIBOOT_FLAGS equ 0x00000003
MULTIBOOT_CHECKSUM equ -(MULTIBOOT_MAGIC + MULTIBOOT_FLAGS)
dd MULTIBOOT_MAGIC
dd MULTIBOOT_FLAGS
dd MULTIBOOT_CHECKSUM
Magic Number: 0x1BADB002
This is the special number GRUB searches for. “1 BAD B002” — someone had a sense of humour.
Flags: 0x00000003
- Bit 0 (value 1): Align loaded modules on page boundaries
- Bit 1 (value 2): Provide a memory map to the kernel
Checksum
Error detection. The formula ensures: magic + flags + checksum == 0
0x1BADB002 + 0x00000003 + checksum = 0
checksum = -(0x1BADB002 + 0x00000003)
The Stack
Before calling Rust code, the assembly bootloader must set up a stack. The CPU needs a stack for function calls, local variables, and interrupt handlers.
section .bss
align 16
stack_bottom:
resb 65536 ; 64KB stack
stack_top:
Key properties of the x86 stack:
- LIFO — Last In, First Out
- Grows downward — from high addresses to low addresses
- ESP points to the top — the most recently added item
We reserve 64KB. This was increased from an initial 16KB after we discovered that interrupt handlers need significant stack space.
The Boot Code
section .text
global _start
extern kernel_main
_start:
mov esp, stack_top ; Set up stack pointer
call kernel_main ; Jump to Rust kernel
.hang:
cli ; Disable interrupts
hlt ; Halt CPU
jmp .hang ; Loop forever if we somehow wake up
mov esp, stack_top — Set the Stack Pointer to the top of our reserved region. Remember the stack grows downward, so we start at the top.
call kernel_main — Jump into Rust. From here, the kernel controls everything.
The .hang label handles the case where kernel_main somehow returns (which it shouldn’t — it’s declared -> !). We disable interrupts and halt the CPU.
Key Assembly Instructions Used
| Instruction | Meaning |
|---|---|
mov dest, src | Copy data from src to dest |
call fn | Jump to function (saves return address on stack) |
cli | Clear Interrupts — disable all maskable interrupts |
hlt | Halt CPU until next interrupt |
sti | Set Interrupts — enable interrupts |
pusha | Push all general-purpose registers to stack |
popa | Pop all general-purpose registers from stack |
iretd | Return from interrupt (32-bit version) |
Memory Layout After Boot
0x00100000 (_kernel_start)
↓
[.multiboot] ← GRUB header (MUST be in first 8KB)
[.text] ← Code: _start, kernel_main, handlers
[.rodata] ← Constants, strings
[.data] ← Initialised globals
[.bss] ← Stack, uninitialised data
↓
0x001XXXXX (_kernel_end)
The kernel is loaded at 1MB. Everything below 1MB is either BIOS data, BIOS ROM, or hardware memory-mapped regions (like the VGA buffer at 0xB8000).