Case Study 38-1: MinOS Boot Sequence — From Power-On to Shell Prompt
Introduction
The boot sequence of an x86-64 system is one of the most intricate pieces of hardware-software interaction in computing. A chain of handoffs — each piece of code setting up the environment for the next — carries execution from the chaos of power-on to a stable, protected, paged, interrupted-driven operating environment. This case study traces every step of MinOS's boot sequence with exact states, addresses, and register values at each transition.
Stage 0: Power-On
The CPU resets into a specific initial state:
CS: 0xF000 (hidden base: 0xFFFF0000)
IP: 0xFFF0
EIP: 0x0000FFF0
RIP: 0x00000000FFFFFFF0 (near top of 4GB, BIOS ROM)
RFLAGS: 0x00000002
All other registers: 0
CR0: 0x00000010 (PE=0: real mode, PG=0: no paging)
The first instruction the CPU fetches is at physical address 0xFFFFFFF0 — a jump to the BIOS start. On a modern QEMU x86-64 with SeaBIOS, this is a far jump to the BIOS initialization code.
The BIOS performs POST (Power-On Self Test), initializes hardware, and then looks for a bootable device. For MinOS, that is a disk image with 0x55AA at the end of the first sector.
Stage 1: BIOS Loads the Bootloader
The BIOS reads the first sector (512 bytes) of the disk into physical memory at 0x7C00 and executes it. At this moment:
CPU mode: 16-bit real mode
CS: 0x0000, IP: 0x7C00
Physical execution address: 0x7C00
DL: 0x80 (boot drive number, provided by BIOS)
Memory: no page tables, 1MB usable (with wraparound above 0xFFFFF without A20)
The BIOS has initialized essential hardware: PIC (Programmable Interrupt Controller), PIT (timer), keyboard, and the basic ACPI/APIC tables are discoverable but not yet set up.
Stage 2: Bootloader Execution (0x7C00)
The MinOS bootloader runs in 16-bit real mode. Steps in order:
2a: Segment Setup
[ORG 0x7C00]
xor ax, ax
mov ds, ax ; DS = 0
mov es, ax ; ES = 0 (for INT 13h buffer)
mov ss, ax
mov sp, 0x7C00 ; Stack below our code
Physical addresses: segment * 16 + offset. With all segments 0, all offsets are physical addresses.
2b: A20 Enable
in al, 0x92 ; Read port 0x92
or al, 0x02 ; Set bit 1 (A20 enable)
out 0x92, al ; Write back
After this, address line 20 is unmasked. Memory access to addresses like 0x100000 (1MB) no longer wraps to 0x000000.
2c: Load Kernel (INT 13h)
The bootloader uses BIOS INT 13h extended read to load the kernel from disk sectors 1-63 into memory at 0x10000:
INT 13h AH=0x42: Extended Read
DL = 0x80 (drive 0)
DS:SI = Disk Address Packet (DAP):
Size: 0x10
Count: 63 sectors
Buffer: 0x0000:0x10000 → physical 0x10000
LBA: 1 (start from sector 1)
After INT 13h completes: 63 × 512 = 32,256 bytes of kernel code and data are at physical addresses 0x10000–0x17E00.
2d: Load Temporary GDT
lgdt [gdtr_temp]
; gdtr_temp describes a 3-entry GDT:
; 0x00: null
; 0x08: 32-bit code (base=0, limit=4GB)
; 0x10: 32-bit data
The GDT is required before entering protected mode.
2e: Switch to 32-bit Protected Mode
mov eax, cr0
or eax, 1 ; Set PE (Protection Enable) bit
mov cr0, eax
jmp 0x08:protected_32 ; Far jump flushes pipeline, reloads CS
State at protected_32 entry:
CPU mode: 32-bit protected mode
CS: 0x08 (kernel code 32-bit)
CR0: bit 0 (PE) = 1
2f: Set Up Paging (for Long Mode)
Page tables must exist before enabling long mode. The bootloader creates a minimal identity mapping of the first 2MB using 2MB pages:
PML4[0] → PDPT
PDPT[0] → PD
PD[0] → 2MB page at physical 0x00000000 (present, writable, PS=huge)
Four 4KB pages total for the page table structures. Physical addresses used: 0x1000 (PML4), 0x2000 (PDPT), 0x3000 (PD).
; Load PML4 into CR3
mov eax, 0x1000 ; PML4 base
mov cr3, eax
; Enable PAE (required for long mode)
mov eax, cr4
or eax, (1 << 5) ; PAE bit
mov cr4, eax
2g: Enable Long Mode
; Set LME (Long Mode Enable) in EFER MSR
mov ecx, 0xC0000080 ; EFER MSR address
rdmsr
or eax, (1 << 8) ; LME bit
wrmsr
; Enable paging → activates long mode (LME + PG = active LM)
mov eax, cr0
or eax, 0x80000000 ; PG bit
mov cr0, eax
; Far jump to 64-bit code
jmp 0x08:0x10000 ; jump to kernel entry at 0x10000
Stage 3: Kernel Entry (0x10000)
The kernel entry in entry.asm takes over in 64-bit long mode:
CPU mode: 64-bit long mode (IA-32e)
RIP: 0x10000
CS: 0x08 (from the temporary GDT, 32-bit descriptor)
CR0: PE=1, PG=1
CR3: 0x1000 (minimal identity map)
3a: Load 64-bit GDT
lea rax, [rel gdt64]
mov [rel gdtr64 + 2], rax ; patch GDT base address
lgdt [rel gdtr64]
The 64-bit GDT has proper 64-bit descriptors (L=1) and includes the TSS descriptor.
3b: Reload Segment Registers
mov ax, 0x10 ; kernel data descriptor
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Reload CS via retfq (far return):
push 0x08
lea rax, [rel .reload_cs]
push rax
retfq
After this, all segment registers use 64-bit descriptors.
3c: Set Up Kernel Stack
lea rsp, [kernel_stack_top] ; 8KB stack in kernel BSS
3d: Zero BSS
lea rdi, [rel bss_start]
lea rcx, [rel bss_end]
sub rcx, rdi
xor al, al
rep stosb
Uninitialized global variables in the kernel are zeroed.
3e: Call kernel_main
call kernel_main ; Never returns
hlt
Stage 4: kernel_main() Initialization
Execution is now in C, 64-bit mode, with a valid stack:
4.1 vga_init(): Physical 0xB8000 is mapped (identity map covers it)
First output: "[MinOS] Kernel starting..."
4.2 serial_init(): UART at I/O port 0x3F8, 9600 baud, 8N1
4.3 gdt_init(): Proper GDT with TSS entry, TSS loaded via `ltr 0x28`
4.4 idt_init(): 256 IDT gates set up
PIC remapped: IRQ0→0x20, IRQ8→0x28
`lidt [idtr]`
`sti` ← INTERRUPTS NOW ENABLED
4.5 pmm_init(): Bitmap covers 64MB of RAM
Marks 0x00000000–0x17FFF as "used" (BIOS + kernel)
Marks 0x18000–0x3FFFFFF as "free"
4.6 vmm_init(): Extends page tables to map all kernel pages
4.7 timer_init(): PIT programmed: outb(0x43, 0x36), outb(0x40, divisor)
4.8 keyboard_init(): Enables IRQ1 in PIC
4.9 scheduler_init():Creates process 0 (idle) and process 1 (shell)
4.10 shell_init(): Prints shell banner
Returns to idle loop
Stage 5: Normal Operation — Shell Prompt
After kernel_main() reaches the idle loop (hlt):
- The PIT fires IRQ0 every 10ms
- The keyboard handler fires on keypress
- The scheduler switches between idle and shell processes
When you type a command and press Enter:
1. Each keypress fires IRQ1 → keyboard_handler → scancode → ASCII → shell_input(c)
2. On Enter: shell_execute(cmd_buf) → prints result → new prompt
3. Screen output via VGA driver at 0xB8000
4. Cursor updated via CRT controller ports 0x3D4/0x3D5
The Complete Picture
Power-on → BIOS (0xFFFFFFF0) → BIOS POST
↓
BIOS loads MBR → Bootloader (0x7C00) → Real mode
↓
A20 enable → Load kernel → Set up paging → Enable long mode
↓
Kernel entry (0x10000) → 64-bit long mode
↓
Load GDT64 → Setup stack → Zero BSS → Call kernel_main
↓
vga_init → serial_init → gdt_init → idt_init (sti!) → pmm_init
↓
vmm_init → timer_init → keyboard_init → scheduler_init → shell_init
↓
Idle loop (hlt) ← driven by timer and keyboard interrupts
↓
Shell prompt: "minOS> "
From power-on to shell prompt: approximately 50 milliseconds of real time, 15 major stages, and every instruction understood at the assembly level.
This is what it means to have no magic in your system.