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:

  1. Parse MBR partition tables: Read and interpret the 16-byte partition entries that define how disks are organized
  2. Implement chain loading: Load another bootloader to 0x7C00 and transfer control with proper register state
  3. Build text-mode user interfaces: Create interactive menus using BIOS video (INT 10h) and keyboard (INT 16h) services
  4. Understand bootloader relocation: Move your code to make room for loaded boot sectors at the canonical 0x7C00 address
  5. Master disk addressing: Work with LBA addressing and INT 13h extended read functions
  6. Design boot management architecture: Understand how boot managers delegate to OS-specific bootloaders
  7. 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:

  1. Relocates itself to free up 0x7C00
  2. Parses the MBR partition table
  3. Displays a colorful menu of bootable partitions
  4. Accepts keyboard input for selection
  5. Loads the selected partition’s boot sector
  6. 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:

  1. A working boot manager identical in architecture to GRUB’s chain-loading mode
  2. Deep understanding of MBR partition tables - you can read raw disk sectors and interpret them
  3. Real-mode UI programming skills - menu systems, keyboard handling, screen control
  4. Chain-loading expertise - understanding of bootloader handoff protocols
  5. 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:

  1. Parses a simple data structure (partition table)
  2. Presents choices to the user
  3. Loads 512 bytes from the selected partition
  4. Sets up a clean environment
  5. 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:

  1. BIOS loads MBR to 0x7C00
  2. Relocate to 0x0600
  3. Parse partitions
  4. Display menu
  5. Wait for key
  6. Load selected boot sector
  7. 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:

  1. “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.”

  2. “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.”

  3. “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.”

  4. “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.”

  5. “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.”

  6. “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:

  1. Write self-relocation code (copy + far jump)
  2. Save boot drive number from DL
  3. Implement screen clear with INT 10h AH=0
  4. Implement colored string print function
  5. 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:

  1. Read partition entries from 0x7C00 + 0x1BE
  2. Check partition type for valid/empty
  3. Display each partition’s type code
  4. 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:

  1. Draw menu box/border
  2. Implement keyboard polling (non-blocking)
  3. Validate key input (‘1’-‘4’, ‘R’)
  4. 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:

  1. Build Disk Address Packet for INT 13h extended read
  2. Read partition’s first sector to 0x7C00
  3. Verify 0xAA55 signature
  4. 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:

  1. Create test disk with multiple partitions
  2. Install different test bootloaders
  3. Test each selection works
  4. Add error messages for failures
  5. Test in Bochs (stricter emulation)
  6. 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:

  1. DL register not set to boot drive
  2. Segment registers not zeroed
  3. Loaded sector overwrote running code (no relocation)
  4. 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:

  1. Reading from wrong memory address
  2. Not accounting for offset within partition entry
  3. 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:

  1. Using AH=0 (blocking) instead of AH=1 (poll)
  2. Not consuming key after polling
  3. 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:

  1. Loaded bootloader expects partition table at specific location
  2. SI register not pointing to partition entry
  3. 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:

  1. QEMU is lenient about INT 13h extended read support
  2. Real BIOS may have different timing requirements
  3. 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 chainloader command
  • 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

  1. Data Centers: PXE boot often chain-loads to local bootloaders for OS installation
  2. Recovery Tools: Hiren’s BootCD and similar use chain loading for multi-tool boot
  3. Virtualization: Hypervisors may chain-load guest OS bootloaders
  4. Embedded Systems: Multi-stage boots with bootloader handoffs
  5. Dual-boot Laptops: Every Windows/Linux dual-boot uses chain loading

10. Resources

Essential References

Tools

Tutorials


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:

  1. 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
  2. Code Quality:
    • Well-commented with explanation of each section
    • Fits within 446 bytes (MBR boot code area)
    • No hardcoded assumptions about partition locations
  3. Testing:
    • Works with test disk containing multiple partition types
    • Successfully chain-loads to known-good bootloaders
    • Works in both QEMU and Bochs
  4. 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