Building a Bootloader from Scratch: An x86 Assembly Guide


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
, macOSbrew install nasm
, Windows: download NASM.QEMU (emulator): Linux
sudo apt install qemu-system
, macOSbrew 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
andprint_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
OSDev Wiki - Comprehensive OS development resource
NASM Documentation - Assembly language reference
Intel Software Developer Manual - CPU architecture reference
Ralf Brown's Interrupt List - BIOS interrupt reference
Subscribe to my newsletter
Read articles from Aayush Gid directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
