BIOS and Bootloaders

Chapter 12: BIOS and Bootloaders

Introduction

The boot process is the sequence of events that occurs from pressing the power button to loading the operating system. Understanding bootloaders and firmware (BIOS/UEFI) is essential for system programmers because the bootloader is the first code you control when building an OS. This chapter explores how computers boot and how to write your own bootloader.

Why This Matters

Every operating system needs a bootloader. Whether you're creating a custom OS, embedded system firmware, or just want to understand what happens before your OS starts, bootloader knowledge is fundamental. The bootloader bridges the gap between firmware and your kernel, loading it into memory and transferring control.

How to Study This Chapter

  1. Test in emulators - Never test bootloaders on real hardware first!
  2. Understand constraints - Bootloaders run in 16-bit real mode with strict size limits
  3. Follow the chain - Firmware → bootloader → kernel
  4. Read specifications - BIOS interrupts and UEFI are well-documented

The Boot Process Overview

Power-On to OS

1. Press Power Button
   ↓
2. CPU Reset → Starts at fixed address
   ↓
3. Firmware (BIOS/UEFI) Initializes Hardware
   ↓
4. Firmware Searches for Bootable Device
   ↓
5. Firmware Loads Bootloader (First 512 bytes)
   ↓
6. Bootloader Runs (16-bit Real Mode)
   ↓
7. Bootloader Loads Kernel into Memory
   ↓
8. Bootloader Switches to Protected/Long Mode
   ↓
9. Bootloader Jumps to Kernel Entry Point
   ↓
10. Kernel Initializes and Takes Control

BIOS (Basic Input/Output System)

What is BIOS?

BIOS is firmware stored in ROM/flash on the motherboard. It:

  • Initializes hardware (CPU, RAM, devices)
  • Provides runtime services (disk I/O, video output)
  • Loads the first 512 bytes from bootable device

POST (Power-On Self Test)

When computer starts, BIOS performs POST:

  1. Test CPU
  2. Check RAM
  3. Initialize devices (keyboard, video, disk)
  4. Beep codes indicate errors

BIOS Boot Process

1. CPU starts at address 0xFFFFFFF0 (reset vector)
2. BIOS code runs from ROM
3. BIOS loads MBR (Master Boot Record) from disk
   - First 512 bytes of disk
   - Loaded at address 0x7C00
   - Must end with signature 0x55AA
4. BIOS jumps to 0x7C00
5. Bootloader executes

BIOS Interrupts

BIOS provides services via software interrupts (INT instruction).

Common BIOS Interrupts:

  • INT 0x10 - Video services
  • INT 0x13 - Disk services
  • INT 0x16 - Keyboard services

Example - Print character:

mov ah, 0x0E        ; Teletype output
mov al, 'A'         ; Character to print
int 0x10            ; Call BIOS

Master Boot Record (MBR)

The MBR is the first sector (512 bytes) of a bootable disk.

MBR Structure

+----------------------+ Offset 0
|   Boot Code          |
|   (446 bytes)        |
+----------------------+ Offset 446
| Partition Table      |
| (4 entries × 16 bytes)|
+----------------------+ Offset 510
| Boot Signature       |
| 0x55 0xAA            |
+----------------------+ 512 bytes total

Partition Table Entry

struct partition_entry {
    uint8_t  status;          // 0x80 = bootable
    uint8_t  first_chs[3];    // CHS address of first sector
    uint8_t  type;            // Partition type
    uint8_t  last_chs[3];     // CHS address of last sector
    uint32_t lba_first;       // LBA of first sector
    uint32_t num_sectors;     // Number of sectors
} __attribute__((packed));

Writing a Simple Bootloader

Minimal Bootloader (prints a message)

; boot.asm - Minimal bootloader
; Compile: nasm -f bin -o boot.bin boot.asm
; Create bootable image: dd if=boot.bin of=disk.img bs=512 count=1

BITS 16                     ; 16-bit real mode
ORG 0x7C00                  ; BIOS loads us here

start:
    ; Set up segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00          ; Stack grows downward from bootloader

    ; Print message
    mov si, message
    call print_string

    ; Halt
    cli
    hlt

print_string:
    lodsb                   ; Load byte from SI into AL, increment SI
    or al, al               ; Check if zero (null terminator)
    jz .done
    mov ah, 0x0E            ; BIOS teletype function
    int 0x10                ; Print character
    jmp print_string
.done:
    ret

message db 'Hello, Bootloader!', 13, 10, 0  ; 13=CR, 10=LF, 0=null

; Pad to 510 bytes and add boot signature
times 510-($-$$) db 0
dw 0xAA55                   ; Boot signature (little-endian)

Testing with QEMU

# Assemble bootloader
nasm -f bin -o boot.bin boot.asm

# Create disk image
dd if=/dev/zero of=disk.img bs=512 count=2880  # 1.44MB floppy size
dd if=boot.bin of=disk.img bs=512 count=1 conv=notrunc

# Run in QEMU
qemu-system-x86_64 -drive file=disk.img,format=raw

Loading from Disk (BIOS INT 0x13)

Reading Sectors

; Read sectors using INT 0x13, AH=0x02
; Returns: CF=0 on success, CF=1 on error

load_sectors:
    mov ah, 0x02            ; Read sectors function
    mov al, [num_sectors]   ; Number of sectors to read
    mov ch, 0               ; Cylinder 0
    mov cl, 2               ; Start from sector 2 (sector 1 is MBR)
    mov dh, 0               ; Head 0
    mov dl, [drive_number]  ; Drive number (0x00=floppy, 0x80=HDD)
    mov bx, 0x1000          ; Destination buffer (ES:BX)
    int 0x13                ; BIOS disk interrupt
    jc .error               ; Jump if carry flag set (error)
    ret

.error:
    mov si, disk_error_msg
    call print_string
    cli
    hlt

disk_error_msg db 'Disk read error!', 13, 10, 0
num_sectors db 5
drive_number db 0x80

LBA to CHS Conversion

Modern disks use LBA (Logical Block Addressing), but BIOS INT 0x13 (AH=02) requires CHS.

; Convert LBA to CHS
; Input: AX = LBA sector number
; Output: CH = cylinder, CL = sector, DH = head

lba_to_chs:
    push bx
    push ax

    ; Sector = (LBA % sectors_per_track) + 1
    xor dx, dx
    div word [sectors_per_track]
    inc dx                  ; Sectors are 1-based
    mov cl, dl              ; CL = sector

    ; Head = (LBA / sectors_per_track) % heads
    xor dx, dx
    div word [heads]
    mov dh, dl              ; DH = head

    ; Cylinder = (LBA / sectors_per_track) / heads
    mov ch, al              ; CH = cylinder (low 8 bits)

    pop ax
    pop bx
    ret

sectors_per_track dw 18     ; Typical for 1.44MB floppy
heads dw 2

Extended Read (LBA Support)

; INT 0x13, AH=0x42 - Extended Read (LBA)
; More modern, supports LBA directly

extended_read:
    mov ah, 0x42            ; Extended read function
    mov dl, [drive_number]  ; Drive number
    mov si, dap             ; DS:SI = Disk Address Packet
    int 0x13
    jc .error
    ret

.error:
    ; Handle error
    ret

; Disk Address Packet (DAP)
dap:
    db 0x10                 ; Size of DAP (16 bytes)
    db 0                    ; Reserved
    dw 1                    ; Number of sectors to read
    dw 0x1000               ; Offset of buffer
    dw 0                    ; Segment of buffer
    dq 1                    ; LBA start sector (sector 1)

Real Mode to Protected Mode

Bootloaders must switch from 16-bit real mode to 32-bit protected mode.

Memory Map in Real Mode

+-------------------+ 0x100000 (1 MB)
|   Extended Mem    |
+-------------------+ 0xC0000
|   Video BIOS      |
+-------------------+ 0xA0000
|   Video Memory    |
+-------------------+ 0x9FC00
|   EBDA            |
+-------------------+ 0x7E00
|   Free            |
+-------------------+ 0x7C00
|   Bootloader      | (512 bytes)
+-------------------+ 0x7A00
|   Free            |
+-------------------+ 0x0500
|   BIOS Data Area  |
+-------------------+ 0x0000
|   IVT             |
+-------------------+

Enabling A20 Line

The A20 line must be enabled to access memory above 1 MB.

enable_a20:
    ; Try BIOS method first
    mov ax, 0x2401
    int 0x15
    jnc .done

    ; Try keyboard controller method
    call wait_8042
    mov al, 0xD1            ; Command: Write output port
    out 0x64, al

    call wait_8042
    mov al, 0xDF            ; Enable A20
    out 0x60, al

    call wait_8042
.done:
    ret

wait_8042:
    in al, 0x64
    test al, 2              ; Check if input buffer full
    jnz wait_8042
    ret

Setting Up GDT (Global Descriptor Table)

; GDT for protected mode
gdt_start:
    ; Null descriptor
    dq 0

    ; Code segment descriptor
    dw 0xFFFF               ; Limit (low)
    dw 0x0000               ; Base (low)
    db 0x00                 ; Base (middle)
    db 10011010b            ; Access: present, ring 0, code, exec/read
    db 11001111b            ; Flags: 4KB granularity, 32-bit
    db 0x00                 ; Base (high)

    ; Data segment descriptor
    dw 0xFFFF               ; Limit (low)
    dw 0x0000               ; Base (low)
    db 0x00                 ; Base (middle)
    db 10010010b            ; Access: present, ring 0, data, read/write
    db 11001111b            ; Flags: 4KB granularity, 32-bit
    db 0x00                 ; Base (high)

gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1  ; Size
    dd gdt_start                 ; Address

Switching to Protected Mode

BITS 16

switch_to_pm:
    cli                         ; Disable interrupts

    lgdt [gdt_descriptor]       ; Load GDT

    ; Enable protected mode
    mov eax, cr0
    or eax, 1                   ; Set PE (Protection Enable) bit
    mov cr0, eax

    ; Far jump to flush pipeline and enter protected mode
    jmp 0x08:protected_mode     ; 0x08 = code segment selector

BITS 32
protected_mode:
    ; Set up segment registers
    mov ax, 0x10                ; 0x10 = data segment selector
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

    mov ebp, 0x90000            ; Set up stack
    mov esp, ebp

    ; Call kernel (loaded at 0x1000)
    call 0x1000

    ; If kernel returns, halt
    cli
    hlt

Complete Two-Stage Bootloader

Stage 1: MBR Bootloader

; stage1.asm - MBR bootloader (fits in 512 bytes)
BITS 16
ORG 0x7C00

start:
    ; Set up segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    ; Save boot drive
    mov [boot_drive], dl

    ; Print loading message
    mov si, msg_loading
    call print_string

    ; Load stage 2 from disk
    mov ah, 0x02                ; Read sectors
    mov al, 8                   ; Read 8 sectors (4 KB)
    mov ch, 0                   ; Cylinder 0
    mov cl, 2                   ; Start at sector 2
    mov dh, 0                   ; Head 0
    mov dl, [boot_drive]
    mov bx, 0x7E00              ; Load stage2 right after stage1
    int 0x13
    jc disk_error

    ; Jump to stage 2
    jmp 0x7E00

disk_error:
    mov si, msg_error
    call print_string
    cli
    hlt

print_string:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0E
    int 0x10
    jmp print_string
.done:
    ret

boot_drive db 0
msg_loading db 'Loading...', 13, 10, 0
msg_error db 'Disk error!', 13, 10, 0

times 510-($-$$) db 0
dw 0xAA55

Stage 2: Extended Bootloader

; stage2.asm - Stage 2 bootloader (can be larger)
BITS 16
ORG 0x7E00

stage2_start:
    mov si, msg_stage2
    call print_string

    ; Enable A20
    call enable_a20

    ; Load kernel from disk (LBA sector 10)
    mov si, msg_loading_kernel
    call print_string

    mov ah, 0x42                ; Extended read
    mov dl, [boot_drive]
    mov si, dap
    int 0x13
    jc kernel_load_error

    ; Switch to protected mode
    cli
    lgdt [gdt_descriptor]

    mov eax, cr0
    or eax, 1
    mov cr0, eax

    jmp 0x08:protected_mode_start

kernel_load_error:
    mov si, msg_kernel_error
    call print_string
    cli
    hlt

enable_a20:
    ; (implementation from earlier)
    ret

print_string:
    ; (implementation from earlier)
    ret

boot_drive db 0x80
msg_stage2 db 'Stage 2 loaded', 13, 10, 0
msg_loading_kernel db 'Loading kernel...', 13, 10, 0
msg_kernel_error db 'Kernel load error!', 13, 10, 0

dap:
    db 0x10                     ; Size
    db 0                        ; Reserved
    dw 32                       ; Number of sectors (16 KB kernel)
    dw 0x1000                   ; Offset
    dw 0                        ; Segment
    dq 10                       ; LBA start sector

gdt_start:
    dq 0                        ; Null descriptor
    ; Code segment
    dw 0xFFFF, 0x0000
    db 0x00, 10011010b, 11001111b, 0x00
    ; Data segment
    dw 0xFFFF, 0x0000
    db 0x00, 10010010b, 11001111b, 0x00
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

BITS 32
protected_mode_start:
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

    mov esp, 0x90000

    ; Jump to kernel entry point
    jmp 0x1000

UEFI (Unified Extensible Firmware Interface)

UEFI is the modern replacement for BIOS.

UEFI vs BIOS

FeatureBIOSUEFI
Boot Mode16-bit real mode32-bit or 64-bit protected mode
Disk FormatMBRGPT (GUID Partition Table)
Bootloader Size512 bytes (MBR)No limit (EFI executable)
File SystemN/AFAT32/FAT16
ServicesINT interruptsFunction calls (API)
Graphical UILimitedFull GUI support

UEFI Boot Process

1. Power On
   ↓
2. UEFI Firmware Initializes
   ↓
3. Firmware Reads GPT Partition Table
   ↓
4. Finds EFI System Partition (ESP)
   ↓
5. Loads EFI Bootloader (e.g., \EFI\BOOT\BOOTX64.EFI)
   ↓
6. Bootloader Runs (Already in Protected/Long Mode!)
   ↓
7. Bootloader Loads Kernel
   ↓
8. Bootloader Calls ExitBootServices()
   ↓
9. Jumps to Kernel

Simple UEFI Bootloader (C)

#include <efi.h>
#include <efilib.h>

EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
    InitializeLib(ImageHandle, SystemTable);

    Print(L"Hello from UEFI Bootloader!\n");

    // Load kernel file
    EFI_STATUS status;
    EFI_FILE_PROTOCOL *root, *kernel_file;
    EFI_LOADED_IMAGE *loaded_image;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *file_system;

    // Get loaded image protocol
    status = uefi_call_wrapper(BS->HandleProtocol, 3,
                               ImageHandle,
                               &LoadedImageProtocol,
                               (void**)&loaded_image);

    // Open file system
    status = uefi_call_wrapper(BS->HandleProtocol, 3,
                               loaded_image->DeviceHandle,
                               &FileSystemProtocol,
                               (void**)&file_system);

    // Open root directory
    status = uefi_call_wrapper(file_system->OpenVolume, 2,
                               file_system, &root);

    // Open kernel file
    status = uefi_call_wrapper(root->Open, 5,
                               root,
                               &kernel_file,
                               L"\\kernel.bin",
                               EFI_FILE_MODE_READ,
                               0);

    if (EFI_ERROR(status)) {
        Print(L"Failed to open kernel!\n");
        return status;
    }

    // Read kernel into memory
    UINTN kernel_size = 1024 * 1024;  // 1 MB
    void *kernel_buffer;
    status = uefi_call_wrapper(BS->AllocatePool, 3,
                               EfiLoaderData,
                               kernel_size,
                               &kernel_buffer);

    status = uefi_call_wrapper(kernel_file->Read, 3,
                               kernel_file, &kernel_size, kernel_buffer);

    Print(L"Kernel loaded at 0x%lx (%lu bytes)\n", kernel_buffer, kernel_size);

    // Exit boot services
    UINTN map_key;
    status = uefi_call_wrapper(BS->ExitBootServices, 2,
                               ImageHandle, map_key);

    // Jump to kernel
    void (*kernel_entry)(void) = (void (*)(void))kernel_buffer;
    kernel_entry();

    return EFI_SUCCESS;
}

Key Concepts

  • BIOS is legacy firmware providing 16-bit services
  • MBR is the first 512 bytes, contains boot code and partition table
  • Bootloader runs in real mode, loads kernel, switches to protected mode
  • INT 0x13 provides BIOS disk I/O
  • A20 line must be enabled to access > 1 MB memory
  • GDT defines segments for protected mode
  • UEFI is modern firmware with 32/64-bit mode and full OS-like environment
  • Two-stage bootloaders overcome 512-byte MBR size limit

Common Mistakes

  1. Forgetting boot signature - Must be 0x55AA at offset 510
  2. Wrong ORG directive - BIOS loads at 0x7C00
  3. Stack not initialized - Set SP before using stack
  4. Segments not set - Initialize DS, ES, SS in bootloader
  5. Not saving drive number - DL contains boot drive on entry
  6. Testing on real hardware - Always test in emulator first!
  7. Exceeding 512 bytes - Stage 1 must fit in MBR

Debugging Tips

  • Use QEMU with -d - Enable debug logging
  • Add print statements - Print characters to verify execution
  • Check boot signature - hexdump -C boot.bin | tail
  • Verify disk reads - Print loaded data
  • Single-step in debugger - qemu -s -S + GDB
  • Test each stage - Verify stage 1 before writing stage 2
  • Use Bochs - Better debugging features than QEMU

Mini Exercises

  1. Write a bootloader that prints your name
  2. Modify bootloader to read keyboard input
  3. Load a second sector from disk and execute it
  4. Implement color text output using video memory (0xB8000)
  5. Create a bootloader that loads a 4 KB kernel
  6. Write code to detect available memory
  7. Implement a simple bootloader menu
  8. Switch to protected mode and print a message
  9. Create a UEFI bootloader that prints to console
  10. Parse and display MBR partition table

Review Questions

  1. What is the boot signature and where is it located?
  2. At what address does BIOS load the MBR?
  3. What is the A20 line and why must it be enabled?
  4. What's the difference between BIOS and UEFI?
  5. How do you switch from real mode to protected mode?

Reference Checklist

By the end of this chapter, you should be able to:

  • Explain the boot process from power-on to kernel
  • Understand BIOS and its interrupts
  • Know the structure of MBR
  • Write a minimal bootloader
  • Load sectors from disk using INT 0x13
  • Enable the A20 line
  • Set up GDT for protected mode
  • Switch from real mode to protected mode
  • Understand UEFI boot process
  • Test bootloaders in QEMU

Next Steps

Now that you understand bootloaders and how to load a kernel into memory, the next chapter explores emulation and QEMU. You'll learn how to use QEMU for kernel development, debugging, and testing system software safely without risking hardware.


Key Takeaway: Bootloaders bridge firmware and operating systems. Understanding the boot process and writing bootloaders teaches you about real mode, protected mode, and the lowest-level system initialization.