NANDHOO.

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.