Boot Sequence & Protected Mode
From Power-On to Your Code
When you hit the power button, here’s what actually happens before a single line of your kernel runs:
- CPU resets to real mode — 16-bit, 1MB addressable, no paging, no protection
- BIOS/UEFI runs from ROM, initializes hardware, locates boot device
- GRUB loads from the MBR/EFI partition, reads its config, loads your kernel ELF
- Multiboot handoff — GRUB jumps to your
_startwitheax = 0x2BADB002(magic) andebxpointing to the multiboot info struct - Your code runs — still in 32-bit protected mode, paging disabled, interrupts disabled
The Multiboot Header
GRUB needs to find your kernel. You embed a magic header in the binary:
; boot.asm
section .multiboot
align 4
dd 0x1BADB002 ; magic
dd 0x00000003 ; flags: align modules, provide memory map
dd -(0x1BADB002 + 3) ; checksum: magic + flags + checksum = 0
GRUB scans the first 8KB of the ELF for this signature. If it matches and the checksum is valid, GRUB treats the file as a multiboot kernel.
Setting Up the Stack
GRUB doesn’t give you a stack. Your very first job is establishing one:
section .bss
align 16
stack_bottom:
resb 16384 ; 16KB initial stack
stack_top:
section .text
global _start
_start:
mov esp, stack_top ; point stack register to top (stacks grow down)
push ebx ; save multiboot info pointer before we clobber registers
call kmain
cli
hlt ; should never reach here
Entering 32-bit Protected Mode
GRUB actually puts you in protected mode already — you don’t need to do the real-mode → protected-mode transition manually (that was the old days with raw BIOS boots).
But you do need to set up your own GDT (Global Descriptor Table) immediately, because GRUB’s GDT is temporary and you can’t trust its segment descriptors.
// kmain.c — first C code that runs
void kmain(uint32_t multiboot_magic, multiboot_info_t *mbi) {
gdt_init(); // set up our own GDT and reload segment registers
idt_init(); // set up interrupt descriptor table
pic_init(); // remap PIC IRQs above CPU exceptions (32+)
mem_init(mbi); // parse multiboot memory map
paging_init(); // enable virtual memory
scheduler_init();
// ... never returns
}
The GDT
The GDT defines memory segments. In 64-bit long mode, segments are mostly flat (base 0, limit max), but we still need them for privilege level separation. Minimum viable GDT for a 32-bit kernel:
struct gdt_entry {
uint16_t limit_low;
uint16_t base_low;
uint8_t base_mid;
uint8_t access; // present, DPL, type
uint8_t granularity; // limit_high + flags
uint8_t base_high;
} __attribute__((packed));
// Index 0: null (required by x86)
// Index 1: kernel code — DPL 0, executable, readable
// Index 2: kernel data — DPL 0, writable
// Index 3: user code — DPL 3, executable, readable
// Index 4: user data — DPL 3, writable
After filling the table, load it with lgdt and reload segment registers:
lgdt [gdt_descriptor]
; Reload CS via far jump — only way to update code segment
jmp 0x08:.reload_cs
.reload_cs:
mov ax, 0x10 ; kernel data segment selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
What Comes Next
With a valid GDT and stack established, we can set up the IDT to handle interrupts and exceptions — without that, any fault (divide by zero, bad memory access) will triple-fault and silently reset the machine.