Building a Bootloader from Scratch: An x86 Assembly Guide

Aayush GidAayush Gid
7 min read

Introduction

When you press the power button, the CPU does not automatically know how to load your operating system. Instead, it follows a defined boot process. At the heart of this process is a small but powerful program: the bootloader. In this project, you will build a simple Stage-1 bootloader in x86 assembly that prints messages and reads a disk sector via BIOS interrupts.

What is a Bootloader ?

A bootloader is the first program that runs after power-on. It resides in the boot sector (the first 512-byte sector) of a bootable device and is loaded by the BIOS to 0x7C00. A valid boot sector ends with the signature 0xAA55. Its role is to prepare the system and load the next stage (e.g., an OS or another bootloader).

Analogy

Think of the bootloader as the table of contents of a book—it’s the first thing you see and it points the system to what comes next.

Why Bootloaders Matter ?

Without a bootloader, the CPU would not know where the operating system is stored, how to load it into memory, or what to execute next. The BIOS performs basic hardware initialization and loads the boot sector; the bootloader then takes over and directs execution.

What You Will Build ?

You will create a Stage-1 bootloader that prints a message, uses BIOS disk services (INT 0x13) to read one sector (not the boot sector), and then prints the loaded sector’s contents.

  • BIOS → loads boot sector to 0x7C00

  • Bootloader → reads Sector 2 using INT 0x13

  • Bootloader → prints data from memory

Tools Required

  • NASM (assembler): Linux sudo apt install nasm, macOS brew install nasm, Windows: download NASM.

  • QEMU (emulator): Linux sudo apt install qemu-system, macOS brew install qemu, Windows: QEMU downloads.

  • Optional: Bochs for detailed low-level debugging.

qemu-system-i386 -fda boot.bin

Background Knowledge (For Beginners)

How a Computer Boots

  • POST: BIOS performs hardware checks.

  • BIOS loads the first 512-byte sector to 0x7C00.

  • CPU jumps to 0x7C00 and executes the bootloader.

  • Bootloader prepares and loads the next stage or OS.

Real Mode and 16-bit Basics

At reset, the CPU runs in 16-bit Real Mode with segment:offset addressing and access to the first 1 MB. Common registers include AX, BX, CX, DX, SI, DI, BP, SP, and segment registers DS, ES, SS, CS with IP as the instruction pointer.

Segmentation and Addressing

Physical Address = (Segment × 16) + Offset.

Common pairs: DS:SI for strings/data, ES:BX for disk/memory buffers, CS:IP for code.

BIOS Interrupts Overview

INT 0x10 (video) for printing characters; INT 0x13 (disk) for sector I/O. These services are invoked via the int instruction with function parameters in registers.

; Print 'A'
mov ah, 0x0E
mov al, 'A'
int 0x10

; Read one sector
mov ah, 0x02
mov al, 0x01
mov ch, 0x00
mov cl, 0x02
mov dh, 0x00
mov dl, 0x00
mov bx, 0x0500
mov es, 0x0000
int 0x13

Boot Sector Rules and Disk Structure

A boot sector is 512 bytes and must end with 0xAA55. Legacy BIOS uses CHS (Cylinder, Head, Sector) addressing; sector numbering starts at 1. Modern disks use LBA, but we will use CHS with BIOS services in Real Mode.

Source Code Overview

You can access the full project repository on GitHub: https://github.com/aayush598/basic-bootloader-assembly

Structure

asm/
├── print.asm              # BIOS-based text output
├── disk_read.asm          # Reads sectors via INT 13h
└── stage1_bootloader.asm  # Entry point boot sector
  • print.asm provides reusable print_char and print_string using INT 0x10.

  • disk_read.asm implements read_sector using INT 0x13 with minimal error handling.

  • stage1_bootloader.asm initializes segments, prints a message, reads sector 2 into memory (e.g., 0x0500), prints its contents, then loops forever.

Printing Functions (print.asm)

Why Reusable Print Routines

Separating printing keeps the bootloader focused, enables reuse for messages and errors, and makes future enhancements (like colors) localized.

print_char (INT 0x10 Teletype)

print_char:
    ; AL = character
    mov ah, 0x0E
    mov bh, 0x00
    mov bl, 0x07
    int 0x10
    ret

print_string (Null-Terminated Strings)

print_string:
    ; DS:SI -> null-terminated string
.print_loop:
    lodsb
    cmp al, 0
    je .done
    call print_char
    jmp .print_loop
.done:
    ret

Disk Reading Function (disk_read.asm)

INT 0x13, AH=0x02

INT 0x13 (AH=0x02) reads sectors using CHS.

Inputs:

  • AL (count)

  • CH/CL/DH (location)

  • DL (drive)

  • ES:BX (destination)

  • CF clear on success, set on failure

Implementation and Error Handling

read_sector:
    ; ES:BX dest, DL drive, CH cyl, DH head, CL sector (1-based)
    mov ah, 0x02
    mov al, 0x01
    int 0x13
    jc .fail
    ret
.fail:
    mov si, read_error_msg
    call print_string
    jmp $

read_error_msg db "Disk Read Error", 0

Important

Sector numbering starts at 1. Using CL=0 will fail. Keep the bootloader at 0x7C00 and load data (e.g., sector 2) into a safe buffer like 0x0500.

Bootloader Logic (stage1_bootloader.asm)

Entry, Segments, and Message

[BITS 16]
[ORG 0x7C00]
start:
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov si, msg
    call print_string
msg db "Reading sector 2...", 0

Read Sector 2 → 0x0000:0x0500, Print, Loop

    mov ax, 0x0000
    mov es, ax
    mov bx, 0x0500
    mov dl, 0x00
    mov ch, 0x00
    mov cl, 0x02
    mov dh, 0x00
    call read_sector
    mov si, 0x0500
    call print_string
    jmp $

%include "asm/print.asm"
%include "asm/disk_read.asm"

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

How It All Fits Together

Start ↓ BIOS loads sector 1 at 0x7C00 ↓ Bootloader prints a message ↓ Sets CHS and buffer ↓ Calls `read_sector` to load sector 2 at 0x0500 ↓ Prints data ↓ Halts in an infinite loop

Running the Project

Assemble with NASM

nasm -f bin asm/stage1_bootloader.asm -o boot.bin

Run in QEMU

qemu-system-x86_64 -boot a -fda build/os_image.img

Expected output: first line from the bootloader (“Reading sector 2...”), followed by the contents of sector 2. If you see “Disk Read Error”, the read failed or the target sector is empty.

Modify and Rebuild

  • Change the boot message by editing msg and reassembling.

  • Change the sector by updating mov cl, 0x02 to another valid sector.

  • Write your own string to sector 2 and print it from 0x0500.

Conclusion

Building a bootloader from scratch provides invaluable insights into how computers work at the lowest level. You've learned about BIOS interrupts, real-mode addressing, and the boot process that every computer follows when it starts up.

This knowledge forms the foundation for more advanced topics like kernel development, device drivers, and system-level programming. The principles you've learned here apply whether you're working with embedded microcontrollers or modern server systems.

Appendix

Full Source Listings

; print.asm
[BITS 16]
print_char:
    mov ah, 0x0E
    mov bh, 0x00
    mov bl, 0x07
    int 0x10
    ret

print_string:
.print_loop:
    lodsb
    cmp al, 0
    je .done
    call print_char
    jmp .print_loop
.done:
    ret
; disk_read.asm
[BITS 16]
read_sector:
    mov ah, 0x02
    mov al, 0x01
    int 0x13
    jc .fail
    ret
.fail:
    mov si, read_error_msg
    call print_string
    jmp $
read_error_msg db "Disk Read Error", 0
; stage1_bootloader.asm
[BITS 16]
[ORG 0x7C00]
start:
    xor ax, ax
    mov ds, ax
    mov es, ax

    mov si, msg
    call print_string

    mov ax, 0x0000
    mov es, ax
    mov bx, 0x0500

    mov dl, 0x00
    mov ch, 0x00
    mov cl, 0x02
    mov dh, 0x00
    call read_sector

    mov si, 0x0500
    call print_string
    jmp $

msg db "Reading sector 2...", 0

%include "asm/print.asm"
%include "asm/disk_read.asm"

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

Quick BIOS Interrupt Reference

  • INT 0x10, AH=0x0E: Teletype output (AL=char, BH=page, BL=color).

  • INT 0x13, AH=0x02: Read sectors (AL=count, CH/CL/DH=CHS, DL=drive, ES:BX=buffer).

  • INT 0x16, AH=0x00: Wait for keypress (returns key in AL).

Glossary

  • Bootloader: First code executed from the boot sector.

  • Sector: Smallest disk unit (typically 512 bytes).

  • CHS: Cylinder-Head-Sector addressing used by legacy BIOS.

  • Segment:Offset: Real-mode addressing scheme (e.g., DS:SI, ES:BX).

  • BIOS: Firmware that initializes hardware and loads the boot sector.

  • Boot Signature (0xAA55): Required 2-byte marker at end of the boot sector.

Resources for Further Learning

1
Subscribe to my newsletter

Read articles from Aayush Gid directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aayush Gid
Aayush Gid