| name | osdev-boot-sequence |
| description | Boot sequence from power-on to kernel entry. BIOS/UEFI boot, Multiboot v1/v2, real-to-protected-to-long mode transitions, A20 gate, and higher-half kernel setup. Use when writing bootloaders, boot assembly, or kernel entry code. |
| origin | MCC |
Boot Sequence: Power-On to Kernel Entry
Complete reference for the x86/x86-64 boot process, from firmware initialization through mode transitions to kernel entry.
When to Use
- Writing a custom bootloader or boot sector
- Implementing a Multiboot v1 or v2 header for GRUB
- Performing CPU mode transitions (real -> protected -> long)
- Enabling the A20 gate
- Writing kernel entry assembly (stack setup, calling kernel_main)
- Setting up a higher-half kernel with paging
- Choosing between BIOS and UEFI boot strategies
BIOS Boot Flow
Power On
|
v
POST (Power-On Self-Test)
| Hardware diagnostics, device enumeration
v
BIOS searches boot devices (floppy, HDD, CD-ROM)
| Checks for boot signature 0x55AA at bytes 510-511
v
MBR loaded to 0x0000:0x7C00 (512 bytes)
| CPU is in 16-bit Real Mode
| DL = boot drive number
| No guaranteed register/stack state
v
Stage 1 bootloader (MBR code, max 446 bytes)
| Locates stage 2 or active partition
v
Stage 2 bootloader
| Loads kernel into memory (below 1 MiB in real mode)
| Enables A20 line
| Sets up GDT
| Switches to Protected Mode (32-bit)
v
Kernel entry (e.g., kernel_main)
Key constraints:
- MBR code area is only 446 bytes (64 bytes for partition table, 2 bytes for signature)
- Real Mode can only address ~1 MiB of memory
- BIOS interrupts (INT 13h for disk, INT 10h for video) only work in Real Mode
- No guaranteed register or stack state at MBR entry -- only DL is reliable
See references/bios-boot.md for MBR structure, INT 13h disk services, and memory map details.
UEFI Boot Flow
Power On
|
v
UEFI firmware initialization
| Platform init, memory controller, PCI, graphics
| A20 already enabled
| Protected Mode (32-bit) or Long Mode (64-bit) already active
v
Firmware locates EFI System Partition (FAT32, GPT)
| Loads PE32+ executable from \EFI\BOOT\BOOTx64.EFI (or similar)
v
UEFI application entry point called
| System Table passed as argument
| Boot Services available (memory, protocols, GOP graphics)
v
Application calls ExitBootServices()
| Firmware relinquishes control
| Only Runtime Services remain
v
Kernel entry
Key differences from BIOS:
- No 512-byte limit -- loads arbitrary-sized PE32+ executables
- CPU already in protected or long mode with flat/identity-mapped memory
- Structured protocol interface instead of interrupt-based BIOS calls
- No A20 gate or GDT setup needed -- firmware handles it
See references/uefi-boot.md for EFI System Partition, boot/runtime services, and ExitBootServices details.
Multiboot Specification
The Multiboot standard provides a uniform interface between bootloaders (GRUB) and kernels.
Multiboot 1
| Field | Value |
|---|
| Magic | 0x1BADB002 |
| Bootloader magic (in EAX) | 0x2BADB002 |
| Header location | First 8 KiB of kernel binary, 32-bit aligned |
| Checksum formula | -(MAGIC + FLAGS) (all three fields sum to zero as uint32) |
Flag bits:
| Bit | Name | Effect |
|---|
| 0 | ALIGN | Align loaded modules on page (4 KiB) boundaries |
| 1 | MEMINFO | Provide memory map in boot info structure |
| 2 | VIDEO | Provide video mode table |
Multiboot 2
| Field | Value |
|---|
| Magic | 0xE85250D6 |
| Bootloader magic (in EAX) | 0x36D76289 |
| Header location | First 8 KiB, 8-byte aligned |
| Header structure | Magic + architecture + header_length + checksum, followed by tags |
| Tag terminator | Tag with type=0, size=8 |
Boot info provided by the bootloader:
- EAX = magic number, EBX = physical pointer to boot information structure
- Memory map (usable/reserved regions)
- Framebuffer information (address, pitch, dimensions, bpp)
- Module list (additional files loaded by bootloader)
- ELF section headers
See references/multiboot-headers.md for complete header layouts, C structs, NASM examples, and linker script requirements.
Mode Transitions
Summary Table
| Mode | Bits | Max Address | Key Features |
|---|
| Real Mode | 16 | ~1 MiB (+64K HMA) | Segmented memory, BIOS interrupts, no protection |
| Protected Mode | 32 | 4 GiB | GDT/IDT, paging optional, ring-based protection |
| Long Mode | 64 | 256 TiB (48-bit) | Mandatory paging, 64-bit registers, no segmentation for data |
Real Mode -> Protected Mode
; 1. Disable interrupts
cli
; 2. Enable A20 line (see A20 section below)
; 3. Load GDT
lgdt [gdtr]
; 4. Set PE bit in CR0
mov eax, cr0
or al, 1
mov cr0, eax
; 5. Far jump to flush prefetch queue and load CS
jmp 0x08:protected_mode_entry
protected_mode_entry:
; 6. Reload data segment registers with 32-bit selectors
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
GDT minimum entries:
- Null descriptor (index 0)
- Code segment: base=0, limit=0xFFFFF, 32-bit, executable, readable, granularity=4K
- Data segment: base=0, limit=0xFFFFF, 32-bit, writable, granularity=4K
Protected Mode -> Long Mode
; 1. Disable paging if enabled (clear PG bit in CR0)
mov eax, cr0
and eax, ~(1 << 31)
mov cr0, eax
; 2. Set up 4-level page tables (PML4 -> PDPT -> PD -> PT)
; Identity-map at minimum the first few MiB
; 3. Load PML4 address into CR3
mov eax, pml4_table
mov cr3, eax
; 4. Enable PAE (bit 5 of CR4)
mov eax, cr4
or eax, (1 << 5)
mov cr4, eax
; 5. Set Long Mode Enable in EFER MSR (bit 8)
mov ecx, 0xC0000080 ; IA32_EFER
rdmsr
or eax, (1 << 8)
wrmsr
; 6. Enable paging (set PG bit in CR0) -- activates long mode
mov eax, cr0
or eax, (1 << 31)
mov cr0, eax
; 7. Far jump to 64-bit code segment
jmp 0x08:long_mode_entry
[bits 64]
long_mode_entry:
; 8. Reload segment registers, set up 64-bit stack
A20 Line
What It Is
The A20 line is the 21st address line (bit 20) on the address bus. On the original 8086, addresses wrapped around at 1 MiB. The IBM AT disabled A20 by default to preserve this wraparound for compatibility. Without A20 enabled, odd-numbered megabytes of memory are inaccessible.
Why It Matters
Any access to memory above 1 MiB requires A20 to be enabled. Protected mode and long mode are useless without it. Some BIOSes and bootloaders (GRUB) enable it for you, but you must not assume this.
Methods to Enable
| Method | Mechanism | Notes |
|---|
| BIOS INT 15h, AX=2401h | BIOS service call | Cleanest method, works on most systems |
| Keyboard controller (8042) | Write 0xD1 to port 0x64, then 0xDF to port 0x60 | Traditional method, slow due to controller polling |
| Fast A20 | Read port 0x92, set bit 1, write back | Fast but not universally supported, may cause issues |
| Port 0xEE | Read from port 0xEE | Some chipsets only |
Recommended approach: Try BIOS INT 15h first, fall back to keyboard controller, then fast A20. Always test A20 status after each attempt by comparing memory at 0x0000:0x0500 and 0xFFFF:0x0510.
Minimal Boot Assembly (Multiboot 1 + GRUB)
This is a complete, annotated boot.s for a Multiboot 1 kernel loaded by GRUB. GRUB places the CPU in 32-bit protected mode with paging disabled, interrupts disabled, and A20 enabled.
/* Multiboot 1 header constants */
.set ALIGN, 1<<0 /* align loaded modules on page boundaries */
.set MEMINFO, 1<<1 /* provide memory map */
.set FLAGS, ALIGN | MEMINFO
.set MAGIC, 0x1BADB002
.set CHECKSUM, -(MAGIC + FLAGS) /* magic + flags + checksum must equal 0 */
/* Place multiboot header early in the binary */
.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM
/* Reserve 16 KiB stack in BSS (no space in binary) */
.section .bss
.align 16
stack_bottom:
.skip 16384
stack_top:
/* Kernel entry point */
.section .text
.global _start
.type _start, @function
_start:
/*
* GRUB has loaded us into 32-bit protected mode.
* Interrupts are disabled. Paging is disabled.
* EAX = 0x2BADB002 (multiboot magic)
* EBX = physical address of multiboot info structure
*/
/* Set up the stack (grows downward on x86) */
mov $stack_top, %esp
/* Push multiboot info for kernel_main(magic, info) */
push %ebx /* multiboot info pointer */
push %eax /* multiboot magic number */
/* Call the C kernel */
call kernel_main
/* Halt if kernel_main returns */
cli
1: hlt
jmp 1b
.size _start, . - _start
Linker script (linker.ld):
ENTRY(_start)
SECTIONS
{
. = 1M; /* Load kernel at 1 MiB physical */
.text BLOCK(4K) : ALIGN(4K)
{
KEEP(*(.multiboot))
*(.text)
}
.rodata BLOCK(4K) : ALIGN(4K) { *(.rodata) }
.data BLOCK(4K) : ALIGN(4K) { *(.data) }
.bss BLOCK(4K) : ALIGN(4K) { *(COMMON) *(.bss) }
}
Build commands:
i686-elf-as boot.s -o boot.o
i686-elf-gcc -c kernel.c -o kernel.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra
i686-elf-gcc -T linker.ld -o myos.bin -ffreestanding -O2 -nostdlib boot.o kernel.o -lgcc
grub-file --is-x86-multiboot myos.bin
Higher-Half Kernel Setup
A higher-half kernel is mapped to the upper virtual address space (commonly 0xC0000000 or 0xFFFFFFFF80000000) despite being loaded to low physical memory (1 MiB).
Steps
-
Boot in physical mode: GRUB loads kernel at physical address 0x00100000 (1 MiB). Paging is off.
-
Prepare page tables in boot assembly:
- Allocate page directory and page table(s) in BSS (4 KiB aligned)
- Fill page table entries mapping kernel physical pages as present + writable
-
Identity-map low memory: Map the same page table at page directory index 0 (virtual 0x00000000). This is required because the CPU is still executing from physical addresses when paging is first enabled.
-
Map kernel in higher half: Map the same page table at page directory index 768 (virtual 0xC0000000). Index 768 because 768 * 4 MiB = 3 GiB = 0xC0000000.
-
Enable paging:
movl $(boot_page_directory - 0xC0000000), %ecx
movl %ecx, %cr3
movl %cr0, %ecx
orl $0x80010000, %ecx /* PG bit + WP bit */
movl %ecx, %cr0
-
Jump to higher-half code: Use an absolute jump to a label in the .text section linked at the higher-half virtual address.
lea 4f, %ecx
jmp *%ecx
-
Unmap identity mapping: Zero out page directory entry 0 and flush TLB.
movl $0, boot_page_directory + 0
movl %cr3, %ecx
movl %ecx, %cr3
-
Set up stack and call kernel: Stack pointer now uses higher-half virtual addresses.
Linker Script (Higher-Half)
ENTRY(_start)
SECTIONS
{
. = 0x00100000; /* Physical load address: 1 MiB */
_kernel_start = .;
/* Boot code runs at physical addresses before paging */
.multiboot.data : { *(.multiboot.data) }
.multiboot.text : { *(.multiboot.text) }
. += 0xC0000000; /* Switch to higher-half virtual addresses */
.text ALIGN(4K) : AT(ADDR(.text) - 0xC0000000)
{
*(.text)
}
.rodata ALIGN(4K) : AT(ADDR(.rodata) - 0xC0000000) { *(.rodata) }
.data ALIGN(4K) : AT(ADDR(.data) - 0xC0000000) { *(.data) }
.bss ALIGN(4K) : AT(ADDR(.bss) - 0xC0000000)
{
*(COMMON)
*(.bss)
*(.bootstrap_stack)
}
_kernel_end = .;
}
The AT() directive tells the linker the physical (load) address while . tracks the virtual (link) address.
Checklist
Before booting your kernel, verify:
Related Skills
osdev-gdt-idt -- GDT/IDT setup and interrupt handling
osdev-paging -- Virtual memory and page table management
osdev-memory-management -- Physical and virtual memory allocators
rust-patterns -- If writing your kernel in Rust