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 0x100000x17E00.

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.