Project 12: Chain Loading Bootloader (Multi-Boot)
Master the art of boot management by building a multi-boot loader that presents a menu of operating systems and chain-loads the selected OS’s bootloader—the exact mechanism used by GRUB, Windows Boot Manager, and every dual-boot system.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | ★★★★☆ Advanced |
| Time Estimate | 1-2 weeks |
| Language | x86 Assembly (NASM), optionally C for menu |
| Prerequisites | Project 4 (Two-Stage Bootloader), understanding of disk partitioning |
| Key Topics | MBR partition table parsing, chain loading mechanics, BIOS keyboard services (INT 16h), BIOS video services (INT 10h), bootloader relocation, disk geometry and LBA addressing |
1. Learning Objectives
By completing this project, you will:
- Parse MBR partition tables: Read and interpret the 16-byte partition entries that define how disks are organized
- Implement chain loading: Load another bootloader to 0x7C00 and transfer control with proper register state
- Build text-mode user interfaces: Create interactive menus using BIOS video (INT 10h) and keyboard (INT 16h) services
- Understand bootloader relocation: Move your code to make room for loaded boot sectors at the canonical 0x7C00 address
- Master disk addressing: Work with LBA addressing and INT 13h extended read functions
- Design boot management architecture: Understand how boot managers delegate to OS-specific bootloaders
- Debug multi-stage boot processes: Trace execution across bootloader handoffs
2. Theoretical Foundation
2.1 Core Concepts
The Chain Loading Philosophy
Chain loading is an elegant architectural pattern: instead of understanding how to boot every possible operating system, your bootloader simply loads another bootloader and lets it take over. This is how dual-boot systems work:
Chain Loading: The Boot Manager Pattern
┌─────────────────────────────────────────────────────────────────────────┐
│ YOUR CHAIN LOADER │
│ (Lives in MBR Sector 0) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. BIOS loads chain loader to 0x7C00 │
│ 2. Chain loader relocates itself to 0x0600 │
│ 3. Displays menu of bootable partitions │
│ 4. User selects partition │
│ 5. Loads selected partition's boot sector to 0x7C00 │
│ 6. Sets up BIOS-like register state │
│ 7. Jumps to 0x7C00 - loaded bootloader runs │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ BOOT MENU │ │
│ ├──────────────────────────────────────────────┤ │
│ │ [1] Windows (Partition 1) │ │
│ │ [2] Linux (Partition 2) │ │
│ │ [3] FreeDOS (Partition 3) │ │
│ └──────────────────────────────────────────────┘ │
│ │ │
│ │ User presses '2' │
│ ▼ │
└─────────────────────────────────────────────────────────────────────────┘
│
│ Load partition 2's boot sector
│ Jump to 0x7C00
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ LINUX BOOTLOADER (GRUB Stage 1) │
│ (Now running at 0x7C00) │
│ │
│ • Thinks it was loaded by BIOS normally │
│ • Loads its Stage 2, kernel, initramfs │
│ • Boots Linux │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key insight: Your chain loader doesn’t need to understand Windows boot protocol, Linux boot protocol, or any OS internals. It just loads 512 bytes and jumps. The loaded bootloader handles everything else.
MBR Partition Table Structure
The Master Boot Record (MBR) contains a partition table that defines up to four primary partitions:
MBR Sector Layout (512 bytes)
Offset Size Content
───────────────────────────────────────────────────────────────────────────
0x000 446 bytes Boot code (your chain loader lives here!)
0x1BE 16 bytes Partition Entry 1
0x1CE 16 bytes Partition Entry 2
0x1DE 16 bytes Partition Entry 3
0x1EE 16 bytes Partition Entry 4
0x1FE 2 bytes Boot Signature (0x55, 0xAA)
───────────────────────────────────────────────────────────────────────────
Partition Entry Structure (16 bytes):
┌────────┬────────────────────────────────────────────────────────────────┐
│ Offset │ Description │
├────────┼────────────────────────────────────────────────────────────────┤
│ 0 │ Bootable flag (0x80 = active/bootable, 0x00 = inactive) │
│ 1-3 │ CHS start address (head, sector, cylinder) - legacy │
│ 4 │ Partition type (0x07=NTFS, 0x83=Linux, 0x0B=FAT32, etc.) │
│ 5-7 │ CHS end address - legacy │
│ 8-11 │ LBA start sector (32-bit little-endian) │
│ 12-15 │ Sector count (32-bit little-endian) │
└────────┴────────────────────────────────────────────────────────────────┘
Common Partition Type Codes:
┌──────┬────────────────────────────────────────┐
│ 0x00 │ Empty/Unused │
│ 0x01 │ FAT12 │
│ 0x04 │ FAT16 (< 32MB) │
│ 0x05 │ Extended partition │
│ 0x06 │ FAT16 (>= 32MB) │
│ 0x07 │ NTFS / exFAT │
│ 0x0B │ FAT32 (CHS) │
│ 0x0C │ FAT32 (LBA) │
│ 0x82 │ Linux swap │
│ 0x83 │ Linux native (ext2/3/4, etc.) │
│ 0xEE │ GPT protective MBR │
│ 0xEF │ EFI System Partition │
└──────┴────────────────────────────────────────┘
The 0x80 flag: This is the “bootable” or “active” flag. In a classic MBR setup, only one partition should have this set. BIOS looks for this to determine the default boot partition, but your chain loader can ignore it and let the user choose.
Bootloader Relocation
Your chain loader faces a memory conflict: BIOS loads it to 0x7C00, but you need to load the target bootloader to the same address. Solution: relocate yourself first.
Bootloader Relocation Process
BEFORE RELOCATION: AFTER RELOCATION:
0x0000 ┌─────────────────────┐ 0x0000 ┌─────────────────────┐
│ IVT + BIOS Data │ │ IVT + BIOS Data │
0x0500 ├─────────────────────┤ 0x0500 ├─────────────────────┤
│ │ │ │
0x0600 │ (free) │ 0x0600 ├─────────────────────┤
│ │ │ YOUR CODE │
│ │ │ (relocated here) │
0x7C00 ├─────────────────────┤ 0x0800 ├─────────────────────┤
│ YOUR CODE │ │ (free) │
│ (BIOS loaded here) │ │ │
0x7E00 ├─────────────────────┤ 0x7C00 ├─────────────────────┤
│ (free) │ │ TARGET BOOTLOADER │
│ │ │ (loaded from disk) │
│ │ 0x7E00 ├─────────────────────┤
│ (free) │
Relocation Steps:
1. Copy 512 bytes from 0x7C00 to 0x0600
2. Far jump to 0x0000:0x0600 + (label offset)
3. Continue execution at new location
4. Now 0x7C00 is free for loaded boot sectors
Why 0x0600? This address is:
- Above the interrupt vector table (0x0000-0x03FF) and BIOS data area (0x0400-0x04FF)
- Low enough to leave plenty of room for loaded code
- Traditionally used for boot code relocation (Linux boot sector uses this)
Chain Load Handoff Requirements
The loaded bootloader expects a specific environment, as if BIOS had loaded it directly:
Register State for Chain Loading
BEFORE JUMPING TO 0x7C00:
┌────────────────────────────────────────────────────────────────────────┐
│ REQUIRED REGISTER STATE │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ DL = Boot drive number (0x80 = first hard disk, 0x81 = second, etc.) │
│ BIOS passed this to your bootloader, pass it on! │
│ │
│ DS = 0x0000 (Data segment = 0) │
│ ES = 0x0000 (Extra segment = 0) │
│ SS = 0x0000 (Stack segment = 0) │
│ SP = 0x7C00 (Stack pointer, grows down from bootloader) │
│ │
│ Interrupts: CLI before setting up, STI optional after │
│ │
├────────────────────────────────────────────────────────────────────────┤
│ OPTIONAL BUT HELPFUL │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ SI = Pointer to partition table entry (some bootloaders expect this) │
│ If loading partition 2's boot sector: │
│ SI = 0x7C00 + 0x1BE + 16 = 0x7CCE (second entry in MBR) │
│ │
│ Note: You may need to preserve the partition table in memory at │
│ 0x7C00+0x1BE if the loaded bootloader expects to find it there │
│ │
└────────────────────────────────────────────────────────────────────────┘
THE ACTUAL JUMP:
; Far jump to 0x0000:0x7C00
jmp 0x0000:0x7C00
; This loads CS with 0x0000 and IP with 0x7C00
; Loaded bootloader runs as if BIOS loaded it
BIOS Keyboard Services (INT 16h)
Interactive menus require keyboard input:
INT 16h Keyboard Services
┌────────────────────────────────────────────────────────────────────────┐
│ KEYBOARD INPUT FUNCTIONS │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ AH = 0x00: Read Key (Blocking) │
│ ──────────────────────────────── │
│ Input: AH = 0x00 │
│ Output: AH = Scan code │
│ AL = ASCII character (0 if extended key) │
│ Note: Waits until key is pressed - FREEZES your code! │
│ │
│ AH = 0x01: Check for Key (Non-blocking) │
│ ───────────────────────────────────── │
│ Input: AH = 0x01 │
│ Output: ZF = 1 if no key available (use JZ to check) │
│ ZF = 0 if key available │
│ AH = Scan code (if available) │
│ AL = ASCII character (if available) │
│ Note: Does NOT remove key from buffer! │
│ │
│ RECOMMENDED PATTERN: │
│ .wait_loop: │
│ mov ah, 0x01 ; Check if key available │
│ int 0x16 │
│ jz .wait_loop ; No key? Keep waiting │
│ mov ah, 0x00 ; Key available, read it │
│ int 0x16 ; AL = ASCII, AH = scan code │
│ │
├────────────────────────────────────────────────────────────────────────┤
│ USEFUL SCAN CODES │
├────────────────────────────────────────────────────────────────────────┤
│ Key ASCII Scan Code │
│ ─────────────────────────────── │
│ '1' 0x31 0x02 │
│ '2' 0x32 0x03 │
│ '3' 0x33 0x04 │
│ '4' 0x34 0x05 │
│ Enter 0x0D 0x1C │
│ Escape 0x1B 0x01 │
│ Up Arrow 0x00 0x48 │
│ Down 0x00 0x50 │
│ 'r'/'R' 0x72/52 0x13 │
│ │
└────────────────────────────────────────────────────────────────────────┘
BIOS Video Services (INT 10h)
Building a menu requires screen control:
INT 10h Video Services for Text Mode
┌────────────────────────────────────────────────────────────────────────┐
│ SCREEN MANAGEMENT │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ AH = 0x00: Set Video Mode │
│ ───────────────────────── │
│ Input: AL = Mode number │
│ Mode 0x03 = 80x25 text, 16 colors (standard VGA text) │
│ Effect: Clears screen, resets cursor to top-left │
│ │
│ AH = 0x02: Set Cursor Position │
│ ──────────────────────────── │
│ Input: BH = Page number (use 0) │
│ DH = Row (0-24) │
│ DL = Column (0-79) │
│ │
│ AH = 0x03: Get Cursor Position │
│ ──────────────────────────── │
│ Input: BH = Page number (use 0) │
│ Output: DH = Row, DL = Column │
│ │
│ AH = 0x06: Scroll Up (or Clear Screen) │
│ ───────────────────────────────────── │
│ Input: AL = Lines to scroll (0 = clear entire window) │
│ BH = Attribute for blank lines (e.g., 0x07 = white on black) │
│ CH, CL = Upper left corner (row, col) │
│ DH, DL = Lower right corner (row, col) │
│ │
│ AH = 0x09: Write Character with Attribute │
│ ───────────────────────────────────── │
│ Input: AL = Character │
│ BH = Page number (use 0) │
│ BL = Attribute (color) │
│ CX = Repeat count │
│ Note: Does NOT advance cursor! │
│ │
│ AH = 0x0E: Teletype Output │
│ ───────────────────────── │
│ Input: AL = Character │
│ BH = Page (use 0) │
│ Effect: Prints character, advances cursor, handles \n, \r, etc. │
│ │
├────────────────────────────────────────────────────────────────────────┤
│ COLOR ATTRIBUTES │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Attribute byte: [Blink][BG color 3-bit][FG color 4-bit] │
│ │
│ Bit 7 Bits 6-4 Bits 3-0 │
│ ┌─────┬───────────┬────────────┐ │
│ │Blink│ Background│ Foreground │ │
│ └─────┴───────────┴────────────┘ │
│ │
│ Colors (same for BG/FG where applicable): │
│ 0x0 = Black 0x4 = Red 0x8 = Dark Gray 0xC = Light Red │
│ 0x1 = Blue 0x5 = Magenta 0x9 = Light Blue 0xD = Light Magenta │
│ 0x2 = Green 0x6 = Brown 0xA = Light Green 0xE = Yellow │
│ 0x3 = Cyan 0x7 = White 0xB = Light Cyan 0xF = Bright White │
│ │
│ Examples: │
│ 0x07 = White on Black (default) │
│ 0x1F = Bright White on Blue (classic BIOS menu) │
│ 0x4F = Bright White on Red (error highlighting) │
│ 0x70 = Black on White (inverted/selection highlight) │
│ │
└────────────────────────────────────────────────────────────────────────┘
2.2 Why This Matters
Industry Applications:
- Dual-boot systems: Every computer running Windows + Linux uses chain loading
- Enterprise deployment: PXE boot chains to local bootloaders
- Recovery tools: Boot managers like Hiren’s BootCD chain-load various utilities
- Embedded systems: Bootloader handoffs in multi-stage boot processes
- Virtualization: Hypervisors chain-load guest OS bootloaders
Career Relevance:
- Systems Programming: Understanding boot architecture is fundamental
- Security Research: Boot-level attacks exploit chain loading trust relationships
- DevOps/SRE: Troubleshooting boot failures requires understanding chain loading
- Firmware Engineering: Multi-stage boot is the industry standard pattern
2.3 Historical Context
Chain loading has been the standard boot architecture since the early PC era:
- 1981: IBM PC introduces MBR with 446 bytes of boot code and partition table
- 1985: OS/2 Boot Manager becomes first popular graphical boot manager
- 1992: LILO (Linux Loader) popularizes chain loading for dual-boot Linux/DOS
- 1999: GRUB introduces “stage” architecture with chain loading support
- 2003: GRUB 2 becomes the de facto Linux boot manager
- 2011: UEFI standardizes, but CSM mode still chain-loads legacy bootloaders
- Today: Windows Boot Manager, GRUB, rEFInd all use chain loading principles
The MBR partition limit: The 4-entry partition table is a legacy constraint. Extended partitions (type 0x05) were invented to work around this, creating linked lists of partition entries. Modern systems use GPT (GUID Partition Table) but maintain a “protective MBR” for compatibility.
2.4 Common Misconceptions
Misconception 1: “Chain loading is complicated because you need to understand each OS” Reality: Chain loading is elegant because you DON’T need to understand each OS. You load 512 bytes and jump. The loaded bootloader handles OS-specific details.
Misconception 2: “You just jump to 0x7C00 and everything works” Reality: Register state matters. DL must contain the drive number. Segment registers should be zeroed. Some bootloaders expect SI to point to the partition entry. Getting this wrong causes mysterious crashes.
Misconception 3: “Modern systems don’t use chain loading” Reality: UEFI still chain-loads when booting legacy systems (CSM mode). Windows Boot Manager chain-loads to Windows Recovery. GRUB chain-loads to Windows.
Misconception 4: “The ‘bootable’ flag (0x80) is always used” Reality: Your boot manager can ignore the bootable flag and let users choose any partition. The flag is just a hint for simple BIOS boot code.
3. Project Specification
3.1 What You Will Build
A complete chain-loading boot manager that:
- Relocates itself to free up 0x7C00
- Parses the MBR partition table
- Displays a colorful menu of bootable partitions
- Accepts keyboard input for selection
- Loads the selected partition’s boot sector
- Chain-loads with proper register state
3.2 Functional Requirements
| Requirement | Description | Verification |
|---|---|---|
| FR1: Self-relocation | Move bootloader from 0x7C00 to 0x0600 | Debugging shows code running at new address |
| FR2: Partition parsing | Read all 4 primary partition entries | Menu displays correct partition types |
| FR3: Menu display | Show partition list with numbers and types | Visual verification in QEMU |
| FR4: Keyboard input | Accept ‘1’-‘4’ keys for selection | Correct partition loads on keypress |
| FR5: Boot sector load | Read partition’s first sector to 0x7C00 | Sector loaded, signature verified |
| FR6: Chain load | Jump to 0x7C00 with correct register state | Loaded bootloader runs successfully |
| FR7: Error handling | Display errors for invalid selections, read failures | Error messages visible |
3.3 Non-Functional Requirements
| Requirement | Description |
|---|---|
| NFR1 | Total binary size must be 512 bytes with valid boot signature |
| NFR2 | Must work in QEMU with partitioned disk image |
| NFR3 | Should work on real hardware in Legacy BIOS mode |
| NFR4 | Code must be well-commented for educational purposes |
| NFR5 | Menu must be readable (appropriate colors and layout) |
3.4 Example Usage / Output
+========================================+
| Multi-Boot Loader v1.0 |
+========================================+
| |
| [1] Partition 1 - Type: 0x83 (Linux) |
| [2] Partition 2 - Type: 0x07 (NTFS) |
| [3] Partition 3 - Type: 0x0B (FAT32) |
| [4] Partition 4 - Type: 0x00 (Empty) |
| |
| Press 1-4 to boot, R to reboot |
+========================================+
# User presses '1':
Loading partition 1 (LBA: 2048)...
Verifying boot signature...
Chain loading...
[Linux bootloader (GRUB) takes over]
3.5 Real World Outcome
Upon completion, you will have:
- A working boot manager identical in architecture to GRUB’s chain-loading mode
- Deep understanding of MBR partition tables - you can read raw disk sectors and interpret them
- Real-mode UI programming skills - menu systems, keyboard handling, screen control
- Chain-loading expertise - understanding of bootloader handoff protocols
- A portfolio piece demonstrating systems programming depth
4. Solution Architecture
4.1 High-Level Design
Chain Loading Bootloader Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ DISK LAYOUT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Sector 0 (MBR): │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 0x000-0x1BD: Your Chain Loader Code (446 bytes) │ │
│ │ 0x1BE-0x1CD: Partition Entry 1 │ │
│ │ 0x1CE-0x1DD: Partition Entry 2 │ │
│ │ 0x1DE-0x1ED: Partition Entry 3 │ │
│ │ 0x1EE-0x1FD: Partition Entry 4 │ │
│ │ 0x1FE-0x1FF: Boot Signature (0x55AA) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Partition 1 (starts at LBA X): │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ First sector: Partition boot sector (VBR) │ │
│ │ This is what gets chain-loaded │ │
│ │ Could be: GRUB Stage 1, NTFS bootloader, FAT boot code, etc. │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ EXECUTION FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ 1. BIOS │ Loads MBR to 0x7C00, DL = boot drive │
│ │ Power On │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 2. Relocate │ Copy 512 bytes from 0x7C00 to 0x0600 │
│ │ │ Far jump to new location │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 3. Parse │ Read partition table at 0x7C00 + 0x1BE │
│ │ Partitions│ (MBR still in memory from BIOS load) │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 4. Display │ Clear screen, draw menu box, list partitions │
│ │ Menu │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 5. Wait for │ Poll keyboard with INT 16h │
│ │ Input │ Validate key ('1'-'4' or 'R') │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 6. Load │ Get partition LBA from table │
│ │ Boot Sect│ Read to 0x7C00 with INT 13h │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 7. Chain │ Set DL, zero segments, set SP │
│ │ Load │ Jump to 0x0000:0x7C00 │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 Memory Layout
Memory Layout During Execution
0x00000 ┌────────────────────────────────────────────────┐
│ Interrupt Vector Table (IVT) │
0x00400 ├────────────────────────────────────────────────┤
│ BIOS Data Area (BDA) │
0x00500 ├────────────────────────────────────────────────┤
│ (Free - safe for stack) │
0x00600 ├────────────────────────────────────────────────┤
│ RELOCATED CHAIN LOADER CODE (512 bytes) │
│ Running from here after relocation │
0x00800 ├────────────────────────────────────────────────┤
│ (Free) │
│ │
│ │
│ STACK grows DOWN from 0x7C00 │
│ ↓ │
0x07C00 ├────────────────────────────────────────────────┤
│ MBR (original load location) │
│ Later: LOADED TARGET BOOTLOADER │
0x07E00 ├────────────────────────────────────────────────┤
│ (Free - above loaded bootloader) │
│ │
0x9FC00 ├────────────────────────────────────────────────┤
│ Extended BIOS Data Area (EBDA) │
0xA0000 ├────────────────────────────────────────────────┤
│ Video Memory │
0xC0000 ├────────────────────────────────────────────────┤
│ BIOS ROM │
0xFFFFF └────────────────────────────────────────────────┘
4.3 Key Components
| Component | Approx Size | Purpose |
|---|---|---|
| Entry + Relocation | ~30 bytes | Initial setup and self-copy |
| Partition Parser | ~40 bytes | Read and validate partition entries |
| Menu Display | ~100 bytes | Clear screen, draw box, list partitions |
| Keyboard Handler | ~30 bytes | Poll and validate input |
| Disk Read | ~50 bytes | INT 13h extended read |
| Chain Load | ~30 bytes | Set registers and jump |
| Strings + Data | ~80 bytes | Messages, partition type names |
| Padding + Signature | Variable | Fill to 510 + 0xAA55 |
| Total | ~446 bytes | Fits in MBR boot code area |
4.4 Data Structures
Partition Entry (in memory at 0x7C00 + 0x1BE + n*16):
; Offsets within a 16-byte partition entry
PART_STATUS equ 0 ; 0x80 = bootable, 0x00 = not
PART_CHS_START equ 1 ; 3 bytes, CHS start (legacy)
PART_TYPE equ 4 ; Partition type code
PART_CHS_END equ 5 ; 3 bytes, CHS end (legacy)
PART_LBA_START equ 8 ; 4 bytes, LBA of first sector
PART_SECTORS equ 12 ; 4 bytes, number of sectors
Disk Address Packet (DAP) for INT 13h Extended Read:
disk_address_packet:
db 0x10 ; Packet size (16 bytes)
db 0 ; Reserved, must be 0
dw 1 ; Number of sectors to read
dw 0x7C00 ; Destination offset
dw 0x0000 ; Destination segment (0x0000:0x7C00)
dd 0 ; LBA low 32 bits (filled before read)
dd 0 ; LBA high 32 bits (usually 0)
5. Implementation Guide
5.1 Development Environment Setup
# Ubuntu/Debian
sudo apt install nasm qemu-system-x86 fdisk hexdump
# macOS
brew install nasm qemu
# Verify installations
nasm --version # Should be 2.14+
qemu-system-x86_64 --version # Should be 6.0+
5.2 Project Structure
chain-loader/
├── chainboot.asm # Main bootloader source
├── Makefile # Build automation
├── create_disk.sh # Script to create test disk with partitions
├── test_boot1.asm # Simple bootloader for partition 1 testing
├── test_boot2.asm # Different bootloader for partition 2 testing
└── README.md # Documentation
Makefile:
all: chainboot.bin test_boot1.bin test_boot2.bin disk.img
chainboot.bin: chainboot.asm
nasm -f bin $< -o $@
test_boot1.bin: test_boot1.asm
nasm -f bin $< -o $@
test_boot2.bin: test_boot2.asm
nasm -f bin $< -o $@
disk.img: chainboot.bin test_boot1.bin test_boot2.bin
./create_disk.sh
run: disk.img
qemu-system-x86_64 -hda disk.img
debug: disk.img
qemu-system-x86_64 -hda disk.img -s -S &
sleep 1
gdb -ex "target remote :1234" -ex "set architecture i8086"
clean:
rm -f *.bin disk.img
5.3 The Core Question You’re Answering
“How do multi-boot systems allow users to choose between different operating systems at boot time, and what mechanisms enable one bootloader to hand off control to another?”
This project reveals the elegant simplicity of boot management: your bootloader doesn’t need to understand Windows internals or Linux boot protocols. It just:
- Parses a simple data structure (partition table)
- Presents choices to the user
- Loads 512 bytes from the selected partition
- Sets up a clean environment
- Jumps
The loaded bootloader thinks BIOS loaded it directly. This separation of concerns is why you can install any OS on any partition without changing the boot manager.
5.4 Concepts You Must Understand First
Before writing code, verify you understand these by answering the self-check questions:
| Concept | Self-Check Question | Reference |
|---|---|---|
| MBR Partition Table | Where do partition entries start in the MBR? What size is each entry? | OSDev Wiki - MBR |
| Partition Entry Format | What byte offset contains the LBA start address? How many bytes? | See section 2.1 diagrams |
| INT 13h Extended Read | What’s the Disk Address Packet structure? Why use 0x42 instead of 0x02? | OSDev Wiki - Disk Access |
| Bootloader Relocation | Why must we relocate before chain loading? What addresses are involved? | See section 2.1 |
| Register State for Handoff | What must DL contain? Why zero segment registers? | OSDev Wiki - Chain Loading |
| INT 16h Keyboard | How do you poll without blocking? How consume a key from buffer? | Ralf Brown’s INT 16h |
5.5 Questions to Guide Your Design
Relocation Strategy:
- Will you copy all 512 bytes or just your code?
- How will you handle the far jump to the new location?
- Do you need to adjust any absolute addresses after relocation?
Partition Handling:
- How will you handle empty partition entries (type 0x00)?
- Will you only show bootable partitions (0x80 flag) or all non-empty ones?
- How will you display partition type in a user-friendly way?
Menu Design:
- What color scheme will you use (consider readability)?
- How will you draw the menu border (box characters or simple dashes)?
- Will you implement highlight for selected item?
- Will you add a timeout with default selection?
Chain Loading:
- Will you verify the 0xAA55 signature before jumping?
- Will you set SI to point to the partition entry (some bootloaders need this)?
- How will you handle read errors gracefully?
5.6 Thinking Exercise
Before writing any code, perform this analysis:
1. Create a test disk image and examine its structure:
# Create 100MB disk image
dd if=/dev/zero of=test.img bs=1M count=100
# Create two partitions using fdisk
fdisk test.img << EOF
n
p
1
+40M
n
p
2
+40M
w
EOF
# Examine the MBR
hexdump -C test.img | head -40
2. Identify the partition table manually: Look at offset 0x1BE (446) in the hexdump. Each 16-byte entry should show:
- Byte 0: Status (0x00 or 0x80)
- Byte 4: Type (probably 0x83 for Linux or similar)
- Bytes 8-11: LBA start (little-endian)
3. Trace the execution flow on paper: Draw boxes for each phase:
- BIOS loads MBR to 0x7C00
- Relocate to 0x0600
- Parse partitions
- Display menu
- Wait for key
- Load selected boot sector
- Chain load
For each phase, note which registers you use and modify.
4. Design your menu layout:
+----------------------------------------+
| Multi-Boot Loader v1.0 | <- Row 0
+----------------------------------------+
| |
| [1] Partition 1 - Linux (0x83) | <- Row 3
| [2] Partition 2 - NTFS (0x07) | <- Row 4
| [3] Partition 3 - (empty) | <- Row 5 (grayed?)
| [4] Partition 4 - FAT32 (0x0B) | <- Row 6
| |
| Press 1-4 to boot, R to reboot | <- Row 8
+----------------------------------------+
5.7 Hints in Layers
Hint 1: Self-Relocation (Conceptual Direction)
The first thing your bootloader must do is copy itself elsewhere:
[BITS 16]
[ORG 0x7C00]
start:
; Disable interrupts during critical section
cli
; Set up segments for copy
xor ax, ax
mov ds, ax
mov es, ax
; Save boot drive (BIOS passes it in DL)
mov [boot_drive], dl
; Copy 512 bytes from 0x7C00 to 0x0600
mov si, 0x7C00 ; Source
mov di, 0x0600 ; Destination
mov cx, 256 ; 256 words = 512 bytes
rep movsw ; Copy!
; Far jump to new location
; This continues execution at the relocated code
jmp 0x0000:relocated
relocated:
; Now running from 0x0600!
; Set up stack
mov ss, ax ; SS = 0
mov sp, 0x7C00 ; Stack grows down from 0x7C00
sti ; Re-enable interrupts
; Continue with partition parsing...
Key insight: After rep movsw, both copies exist. The jmp 0x0000:relocated jumps to the copy at 0x0600. The relocated label must be calculated relative to the new base.
Important: We use [ORG 0x7C00] because NASM needs to know the original load address for calculating label offsets. After the jump, execution continues in the copy, but label math still works because both copies are identical.
Hint 2: Parsing and Displaying Partitions (More Specific)
After relocation, the MBR is still at 0x7C00 (BIOS left it there). Parse the partition table:
PARTITION_TABLE equ 0x7C00 + 0x1BE ; Offset of first partition entry
parse_partitions:
mov si, PARTITION_TABLE
mov cx, 4 ; 4 partition entries
mov byte [partition_count], 0
.parse_loop:
mov al, [si + 4] ; Partition type byte
test al, al ; Is it 0x00 (empty)?
jz .next_partition
; Non-empty partition found
inc byte [partition_count]
; Store partition info for menu display
; You might store: entry number, type, LBA start
.next_partition:
add si, 16 ; Next entry (16 bytes each)
loop .parse_loop
ret
partition_count: db 0
Displaying with colors:
; Set video mode and clear screen
mov ah, 0x00
mov al, 0x03 ; 80x25 color text
int 0x10
; Draw a character with color at current cursor
mov ah, 0x09
mov al, 'H' ; Character
mov bh, 0 ; Page 0
mov bl, 0x1F ; White on blue
mov cx, 1 ; Count
int 0x10
; Move cursor forward (INT 0x09 doesn't do this!)
mov ah, 0x02
mov bh, 0
mov dh, 0 ; Row
mov dl, 1 ; Column
int 0x10
Better approach - print string with color:
; SI = string, BL = color
print_string_color:
pusha
.loop:
lodsb ; Load char from [SI] into AL
test al, al ; Null terminator?
jz .done
mov ah, 0x09 ; Write char with attribute
mov bh, 0
mov cx, 1
int 0x10
; Advance cursor
push ax
mov ah, 0x03 ; Get cursor position
int 0x10
inc dl ; Column++
mov ah, 0x02 ; Set cursor position
int 0x10
pop ax
jmp .loop
.done:
popa
ret
Hint 3: Keyboard Input and Selection (Technical Details)
wait_for_selection:
; Poll for key (non-blocking)
mov ah, 0x01
int 0x16
jz wait_for_selection ; No key? Keep waiting
; Key available - read it
mov ah, 0x00
int 0x16
; AL = ASCII, AH = scan code
; Check for '1' through '4'
cmp al, '1'
jb .check_reboot ; Below '1'
cmp al, '4'
ja .check_reboot ; Above '4'
; Valid selection: '1'-'4'
sub al, '1' ; Convert to 0-3
movzx bx, al ; BX = partition index (0-3)
; Calculate offset to partition entry
; partition_entry = 0x7C00 + 0x1BE + (bx * 16)
shl bx, 4 ; bx *= 16
add bx, PARTITION_TABLE ; bx = address of entry
; Check if partition is valid (type != 0)
mov al, [bx + 4] ; Partition type
test al, al
jz .invalid_selection
; Valid! Proceed to chain load
mov si, bx ; SI = partition entry address
jmp chain_load
.check_reboot:
cmp al, 'r'
je .reboot
cmp al, 'R'
je .reboot
jmp wait_for_selection ; Invalid key, try again
.reboot:
; Warm reboot via keyboard controller
mov al, 0xFE
out 0x64, al
hlt
.invalid_selection:
; Display error, return to menu
; ...
jmp wait_for_selection
Hint 4: Loading Boot Sector and Chain Loading (Implementation)
chain_load:
; SI = pointer to partition entry
; Get LBA start from partition entry
mov eax, [si + 8] ; LBA start (32-bit)
; Set up Disk Address Packet
mov [dap_lba], eax
; Read one sector to 0x7C00
mov ah, 0x42 ; Extended read
mov dl, [boot_drive] ; Drive number
mov si, disk_address_packet
int 0x13
jc .read_error ; Error if carry set
; Verify boot signature
cmp word [0x7C00 + 510], 0xAA55
jne .invalid_signature
; Set up environment for loaded bootloader
cli
; Zero all segment registers
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ; Stack below bootloader
; DL = boot drive (already set, but ensure it's correct)
mov dl, [boot_drive]
; Optional: SI = partition entry
; Some bootloaders expect this
; mov si, (saved partition entry address)
; Jump to loaded bootloader!
jmp 0x0000:0x7C00
.read_error:
mov si, msg_read_error
mov bl, 0x4F ; White on red
call print_string_color
jmp wait_for_selection
.invalid_signature:
mov si, msg_invalid_sig
mov bl, 0x4F
call print_string_color
jmp wait_for_selection
; Disk Address Packet
disk_address_packet:
db 0x10 ; Size of packet
db 0 ; Reserved
dw 1 ; Sectors to read
dw 0x7C00 ; Offset
dw 0x0000 ; Segment
dap_lba:
dd 0 ; LBA (filled in above)
dd 0 ; LBA high (for >2TB, usually 0)
boot_drive: db 0
msg_read_error: db "Disk read error!", 0
msg_invalid_sig: db "Invalid boot signature!", 0
5.8 The Interview Questions They’ll Ask
If you put “multi-boot bootloader with chain loading” on your resume, expect these questions:
-
“Explain how chain loading works. Why can’t your bootloader just ‘call’ the other boot sector?”
Strong answer: “Chain loading loads another bootloader to 0x7C00, sets registers to mimic BIOS (DL=drive, segments=0), and jumps. The loaded bootloader runs as if BIOS loaded it directly. You can’t ‘call’ because: (1) there’s no common ABI, (2) the loaded code expects to own the entire system, (3) it assumes it’s at 0x7C00 and uses ORG-relative addressing. It’s a full handoff, not a subroutine.”
-
“Walk me through the MBR partition table structure. Where is it and what does each field mean?”
Strong answer: “The partition table is at MBR offset 446 (0x1BE), containing four 16-byte entries. Each entry has: byte 0 = bootable flag (0x80 or 0x00), bytes 1-3 = CHS start (legacy), byte 4 = type code (0x83 Linux, 0x07 NTFS), bytes 5-7 = CHS end, bytes 8-11 = LBA start (little-endian), bytes 12-15 = sector count. I use the LBA start to find where to read the partition’s boot sector.”
-
“Why do you need to relocate your bootloader before chain loading?”
Strong answer: “BIOS loads my code to 0x7C00. The target bootloader also needs to be at 0x7C00 - that’s where it expects to run. If I don’t relocate, loading the target would overwrite my running code mid-execution. I copy my 512 bytes to 0x0600, far jump there to continue execution, and now 0x7C00 is free for the target.”
-
“What register state must be set before jumping to the loaded bootloader?”
Strong answer: “DL must contain the boot drive number (0x80 for first hard disk). DS, ES, SS should be 0. SP should be set to 0x7C00 or similar safe value. Some bootloaders expect SI to point to their partition table entry. Interrupts can be disabled during setup (CLI) but some bootloaders expect them enabled. The target should see an environment identical to direct BIOS load.”
-
“How does your bootloader handle extended (logical) partitions inside an extended partition?”
Strong answer: “My basic implementation only handles the four primary partition entries. Extended partitions (type 0x05 or 0x0F) contain linked lists of Extended Boot Records (EBRs). Each EBR has its own partition table with at most 2 entries: one logical partition and optionally a link to the next EBR. Parsing these requires following the chain and recursively reading each EBR sector. GRUB handles this, but my simple bootloader doesn’t.”
-
“What happens if the user selects a partition with no valid boot sector?”
Strong answer: “I verify the 0xAA55 signature at offset 510 after loading. If missing, I display an error and return to the menu. Without this check, jumping to invalid code causes undefined behavior - likely a triple fault and reboot. Production boot managers like GRUB also check for known bootloader signatures.”
5.9 Books That Will Help
| Topic | Book & Chapter |
|---|---|
| MBR partition table structure | “File System Forensic Analysis” by Brian Carrier, Chapter 3 (Volume Analysis) - detailed MBR/GPT coverage |
| BIOS interrupt services | “The Art of Assembly Language” by Randall Hyde, Chapter 13 (BIOS Services) - INT 10h, 13h, 16h |
| Chain loading mechanics | “Operating System Concepts” by Silberschatz, Chapter 2 (Bootstrap Program) - boot process overview |
| Real mode programming | “PC Assembly Language” by Paul A. Carter, Chapter 2 - segment:offset addressing |
| Disk addressing (CHS vs LBA) | “Systems Performance” by Brendan Gregg, Chapter 9 (Disks) - storage fundamentals |
| GRUB internals | GNU GRUB Manual, Chain-loading section - gnu.org/software/grub |
| Low-level debugging | “Low-Level Programming” by Igor Zhirkov, Chapter 8 - debugging techniques |
| x86 boot architecture | Intel SDM Vol. 3A, Chapter 9 (Processor Management and Initialization) |
5.10 Implementation Phases
Phase 1: Relocation and Basic Display (Days 1-2)
Goals:
- Bootloader relocates successfully
- Displays “Hello from relocated code!”
- Clears screen with color
Tasks:
- Write self-relocation code (copy + far jump)
- Save boot drive number from DL
- Implement screen clear with INT 10h AH=0
- Implement colored string print function
- Test in QEMU
Checkpoint: QEMU shows colored message, no crashes.
Phase 2: Partition Table Parsing (Days 3-4)
Goals:
- Parse all 4 partition entries
- Display partition information
Tasks:
- Read partition entries from 0x7C00 + 0x1BE
- Check partition type for valid/empty
- Display each partition’s type code
- Handle empty partitions gracefully
Checkpoint: Menu shows “Partition 1: Type 0x83” etc.
Phase 3: Menu and Keyboard (Days 5-6)
Goals:
- Interactive menu with number key selection
- R key for reboot
Tasks:
- Draw menu box/border
- Implement keyboard polling (non-blocking)
- Validate key input (‘1’-‘4’, ‘R’)
- Highlight selected partition or show confirmation
Checkpoint: Pressing keys shows visible response.
Phase 4: Disk Read and Chain Load (Days 7-9)
Goals:
- Load selected partition’s boot sector
- Chain load successfully
Tasks:
- Build Disk Address Packet for INT 13h extended read
- Read partition’s first sector to 0x7C00
- Verify 0xAA55 signature
- Set up register state and jump
Checkpoint: Chain loading to test bootloader works.
Phase 5: Testing and Polish (Days 10-14)
Goals:
- Test with multiple partition types
- Error handling and edge cases
- Documentation
Tasks:
- Create test disk with multiple partitions
- Install different test bootloaders
- Test each selection works
- Add error messages for failures
- Test in Bochs (stricter emulation)
- Write README and comments
Checkpoint: Complete, documented, working boot manager.
6. Testing Strategy
6.1 Creating a Test Disk
#!/bin/bash
# create_disk.sh - Create a test disk with multiple partitions
# Create 100MB disk image
dd if=/dev/zero of=disk.img bs=1M count=100
# Create partitions using sfdisk (scriptable fdisk)
sfdisk disk.img << EOF
label: dos
1M,40M,83
41M,40M,7
EOF
# Write chain loader to MBR (first 446 bytes only, preserve partition table)
dd if=chainboot.bin of=disk.img bs=446 count=1 conv=notrunc
# Write test bootloader 1 to partition 1 (starts at sector 2048)
dd if=test_boot1.bin of=disk.img bs=512 seek=2048 conv=notrunc
# Write test bootloader 2 to partition 2 (starts at sector 83968)
dd if=test_boot2.bin of=disk.img bs=512 seek=83968 conv=notrunc
echo "Disk image created: disk.img"
echo "Partition 1: Linux (0x83) with test bootloader 1"
echo "Partition 2: NTFS (0x07) with test bootloader 2"
Simple test bootloader (test_boot1.asm):
[BITS 16]
[ORG 0x7C00]
start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
; Clear screen
mov ax, 0x0003
int 0x10
; Print message
mov si, msg
.print:
lodsb
test al, al
jz .halt
mov ah, 0x0E
int 0x10
jmp .print
.halt:
cli
hlt
jmp .halt
msg: db "Bootloader 1 loaded successfully!", 13, 10
db "Chain loading works!", 13, 10, 0
times 510-($-$$) db 0
dw 0xAA55
6.2 Test Cases
| Test | Input | Expected Result |
|---|---|---|
| Basic boot | Power on | Menu displays with partition list |
| Valid selection | Press ‘1’ | Partition 1 bootloader runs |
| Second partition | Press ‘2’ | Partition 2 bootloader runs |
| Empty partition | Press ‘3’ (if empty) | Error message, stays in menu |
| Invalid key | Press ‘X’ | No action, menu remains |
| Reboot | Press ‘R’ | System reboots |
| Signature check | Partition with no 0xAA55 | Error message displayed |
6.3 QEMU Testing Commands
# Standard run
qemu-system-x86_64 -hda disk.img
# With serial console (useful for debugging)
qemu-system-x86_64 -hda disk.img -nographic
# With debug output
qemu-system-x86_64 -hda disk.img -d int,cpu_reset
# With GDB server
qemu-system-x86_64 -hda disk.img -s -S
# In another terminal:
gdb -ex "target remote :1234" -ex "set architecture i8086" -ex "break *0x7c00"
6.4 Verification Checklist
- Binary is exactly 512 bytes
- Last two bytes are 0x55 0xAA
- Boots in QEMU without triple fault
- Menu displays correctly with colors
- All 4 partition slots shown (with types or “empty”)
- Keyboard input works (number keys)
- Chain load to partition 1 succeeds
- Chain load to partition 2 succeeds
- Reboot key (R) works
- Invalid partition selection shows error
- Invalid boot signature detected
- Works in Bochs (stricter emulation)
7. Common Pitfalls & Debugging
Problem 1: Triple fault immediately after chain loading
Symptoms: Select a partition, system reboots instead of running loaded bootloader.
Root causes:
- DL register not set to boot drive
- Segment registers not zeroed
- Loaded sector overwrote running code (no relocation)
- Stack collision with code
Fix:
; Ensure ALL these before jumping:
cli ; Disable interrupts
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
mov dl, [boot_drive] ; CRITICAL: DL = drive number
jmp 0x0000:0x7C00
Quick test: Add debug character output before each step. If you see all debug chars but then crash, the jump itself is the problem.
Problem 2: Partition table shows garbage
Symptoms: Menu displays wrong partition types or random data.
Root causes:
- Reading from wrong memory address
- Not accounting for offset within partition entry
- Endianness confusion with LBA values
Fix:
; Partition table starts at 0x7C00 + 0x1BE = 0x7DBE
; NOT at 0x7C00!
mov si, 0x7DBE ; First partition entry
mov al, [si + 4] ; Type is at offset 4 within entry
Quick test: Use hexdump -C disk.img | head -40 to verify partition table location and values manually.
Problem 3: Keyboard input freezes or repeats
Symptoms: Menu doesn’t respond to keys, or processes same key multiple times.
Root causes:
- Using AH=0 (blocking) instead of AH=1 (poll)
- Not consuming key after polling
- Not clearing key from buffer
Fix:
.wait:
mov ah, 0x01 ; Check if key available
int 0x16
jz .wait ; No key? Keep polling
mov ah, 0x00 ; MUST call this to consume key
int 0x16 ; Now AL = ASCII, AH = scan code
Quick test: Add a counter that increments each loop iteration, display it. Should increment slowly while no key, then show key value.
Problem 4: Loaded bootloader says “missing operating system”
Symptoms: Chain load happens, but loaded bootloader displays error.
Root causes:
- Loaded bootloader expects partition table at specific location
- SI register not pointing to partition entry
- Loaded sector is partition boot sector, not expecting to be chain-loaded
Fix: Some bootloaders (especially Windows) expect:
- Partition table still readable at 0x7DBE
- SI pointing to their partition entry
; Before jumping, ensure SI = partition entry address
mov si, [selected_partition_entry]
jmp 0x0000:0x7C00
Quick test: Use simple test bootloaders first (your own code from earlier projects). They don’t have these expectations.
Problem 5: Works in QEMU but fails on real hardware
Symptoms: Chain loader works perfectly in QEMU, crashes or misbehaves on real PC.
Root causes:
- QEMU is lenient about INT 13h extended read support
- Real BIOS may have different timing requirements
- Real hardware may not support INT 13h AH=42h
Fix:
; Check for INT 13h extensions before using them
mov ah, 0x41
mov bx, 0x55AA
mov dl, [boot_drive]
int 0x13
jc .no_extensions ; CF=1 means not supported
cmp bx, 0xAA55
jne .no_extensions
; Extensions supported, use AH=42h
jmp .extended_read
.no_extensions:
; Fall back to CHS addressing with AH=02h
; (More complex, need to convert LBA to CHS)
Quick test: Test in Bochs first - it’s stricter than QEMU about hardware emulation.
8. Extensions & Challenges
Extension 1: Timeout with Default Boot (Beginner)
Add a 5-second countdown that auto-boots the first partition:
[5] Booting partition 1 in 5 seconds...
[4] Booting partition 1 in 4 seconds...
Press any key to interrupt...
Implementation hint: Use BIOS timer tick (INT 1Ah AH=0) or count loop iterations.
Extension 2: Arrow Key Navigation (Beginner)
Instead of number keys, use Up/Down arrows to select and Enter to boot:
[ ] Partition 1 - Linux
> [*] Partition 2 - Windows <- Currently selected
[ ] Partition 3 - FreeDOS
Extension 3: Display Partition Size (Intermediate)
Show human-readable partition sizes:
[1] Partition 1 - Linux (40.0 GB)
[2] Partition 2 - NTFS (120.5 GB)
Implementation hint: Partition entry has sector count at offset 12. Multiply by 512, divide by 1024^3 for GB.
Extension 4: Extended Partition Support (Intermediate)
Parse extended partitions (type 0x05/0x0F) to show logical partitions:
[1] Partition 1 - Primary, Linux
[2] Partition 2 - Primary, NTFS
[5] Partition 5 - Logical, FAT32
[6] Partition 6 - Logical, Linux
Extension 5: Boot from Different Drives (Advanced)
Allow selection of boot drive before partition:
Select drive:
[A] Floppy (0x00)
[C] Hard Disk 1 (0x80)
[D] Hard Disk 2 (0x81)
Extension 6: Configuration Persistence (Advanced)
Save last boot selection to unused space in MBR or a dedicated sector:
Last boot: Partition 2
[Booting automatically in 3 seconds...]
Extension 7: GPT Support (Expert)
Add support for GPT (GUID Partition Table) with protective MBR detection:
Detected GPT disk
[1] EFI System Partition (100 MB)
[2] Microsoft Reserved (16 MB)
[3] Windows (C:) (500 GB)
[4] Linux / (100 GB)
9. Real-World Connections
How Professional Boot Managers Work
GRUB (GRand Unified Bootloader):
- Stage 1 lives in MBR (446 bytes), loads Stage 1.5 from gap after MBR or boot sector
- Stage 1.5 contains filesystem drivers, loads Stage 2 from /boot/grub
- Chain loading implemented in
chainloadercommand - Source:
grub-core/boot/i386/pc/boot.S,grub-core/loader/i386/pc/chainloader.c
Windows Boot Manager (bootmgr):
- MBR code loads bootmgr from system partition
- bootmgr reads BCD (Boot Configuration Data)
- Chain-loads to Windows boot loader (winload.exe)
- For multi-boot: chain-loads to other OS bootloaders
rEFInd (UEFI boot manager):
- Works in UEFI environment, not legacy BIOS
- Scans for EFI boot loaders on all partitions
- Chain-loads to selected EFI application
- Modern equivalent of what you’re building
Industry Applications
- Data Centers: PXE boot often chain-loads to local bootloaders for OS installation
- Recovery Tools: Hiren’s BootCD and similar use chain loading for multi-tool boot
- Virtualization: Hypervisors may chain-load guest OS bootloaders
- Embedded Systems: Multi-stage boots with bootloader handoffs
- Dual-boot Laptops: Every Windows/Linux dual-boot uses chain loading
10. Resources
Essential References
- OSDev Wiki - MBR - Partition table structure
- OSDev Wiki - Chain Loading - Handoff requirements
- Ralf Brown’s Interrupt List - BIOS interrupt reference
- INT 13h Reference - Disk services
- INT 16h Reference - Keyboard services
Tools
- NASM - Assembler
- QEMU - Emulator for testing
- Bochs - Emulator with detailed debugging
- fdisk/sfdisk - Partition table tools
- hexdump - Binary inspection
Tutorials
- OSDev Wiki - Bootloader - General bootloader concepts
- OSDev Wiki - Disk Access - INT 13h usage
- GNU GRUB Manual - Chain-loading - How GRUB does it
11. Self-Assessment Checklist
Understanding Check
- I can explain why chain loading is simpler than native OS booting
- I understand MBR partition table layout (offset, entry size, fields)
- I can describe what register state is needed for chain load handoff
- I understand why relocation is necessary
- I can explain the difference between INT 13h AH=02h and AH=42h
- I know why INT 16h AH=01 is needed before AH=00
Implementation Check
- Bootloader relocates successfully from 0x7C00 to 0x0600
- All four partition entries are correctly parsed
- Menu displays with readable colors and layout
- Keyboard input works correctly (no freezing or repeating)
- Selected partition’s boot sector loads to 0x7C00
- Boot signature (0xAA55) is verified
- Chain load jump sets correct register state
- Error conditions are handled gracefully
Testing Check
- Works with multiple partition types on test disk
- Chain load to test bootloaders succeeds
- Invalid selections show error messages
- Works in QEMU and Bochs
- Code is under 446 bytes (leaves room for partition table)
Interview Readiness
- Can explain chain loading concept without notes
- Can draw MBR partition table structure
- Can describe register state requirements for handoff
- Can discuss limitations (only 4 primary partitions)
- Can compare to GRUB’s approach
12. Completion Criteria
Your implementation is complete when:
- Functional Criteria:
- Bootloader displays menu with all 4 partition entries
- User can select partition with number keys (1-4)
- User can reboot with ‘R’ key
- Selected partition’s boot sector chain-loads successfully
- Invalid selections are handled gracefully
- Code Quality:
- Well-commented with explanation of each section
- Fits within 446 bytes (MBR boot code area)
- No hardcoded assumptions about partition locations
- Testing:
- Works with test disk containing multiple partition types
- Successfully chain-loads to known-good bootloaders
- Works in both QEMU and Bochs
- Documentation:
- README explains how to build and test
- Memory map and register usage documented
- Test disk creation script included
Learning Milestones:
| Milestone | Verification |
|---|---|
| 1. Partition table parsed correctly | Menu shows accurate partition types |
| 2. Menu and keyboard work | Interactive selection possible |
| 3. Chain loading succeeds | Test bootloader runs after selection |
| 4. Multiple OSes bootable | Different test bootloaders load correctly |
Previous Project: P11 - Virtual Machine Boot Process Inspector
Next Project: P13 - Network Boot (PXE) Client