DEV Community

Aayush Gid
Aayush Gid

Posted on

Building a Bootloader from Scratch: An x86 Assembly Guide

When you press the power button, a complex, step-by-step procedure unfolds before your operating system (OS) appears. At the very core of this process lies the bootloader. This article guides you through building a simple, Stage-1 bootloader in x86 assembly that prints messages and reads a disk sector using BIOS interrupts.

What is a Bootloader?

A bootloader is the first program that executes after the system power-on sequence completes.

  • Location: It resides in the boot sector—the very first 512-byte sector of a bootable device (like a hard drive or USB).
  • Loading: The system's BIOS (Basic Input/Output System) loads this sector into memory at the specific address 0x7C00.
  • Signature: A valid boot sector must end with the signature 0xAA55.
  • Role: Its primary function is to prepare the system environment and load the "next stage" of code, which could be the OS kernel or a more advanced second-stage bootloader.

Analogy: Think of the bootloader as the table of contents of a book—it’s the first thing the system sees and it points to where the essential content (the OS) can be found.

Without a bootloader, the CPU wouldn't know where the OS is, how to load it into memory, or what instruction to execute next. The BIOS does the basic hardware checks and initialization; the bootloader takes the hand-off and directs execution.

Project Goal: Stage-1 Bootloader

You will create a minimal Stage-1 bootloader with the following sequence:

  1. BIOS loads the boot sector to 0x7C00.
  2. Bootloader prints an initial message.
  3. Bootloader uses the BIOS disk service (INT 0x13) to read a specific sector (e.g., Sector 2).
  4. Bootloader prints the contents of the newly loaded sector from memory.
  5. Bootloader halts in an infinite loop.

Tools Required

Tool Description Installation Command (Linux)
NASM The Assembler used to convert assembly code into a binary file (.bin). sudo apt install nasm
QEMU A fast and reliable system emulator for testing the bootloader. sudo apt install qemu-system
Optional: Bochs For detailed, low-level debugging. -

You'll run the final assembly code using QEMU:
qemu-system-i386 -fda boot.bin

Background Knowledge for Beginners

The Computer Boot Sequence

  1. POST: The BIOS runs the Power-On Self-Test (POST) to check hardware.
  2. Load: The BIOS loads the first 512-byte sector (the boot sector) of the bootable drive into memory at 0x7C00.
  3. Execute: The CPU jumps to 0x7C00 and begins executing the bootloader's code.
  4. Handoff: The bootloader loads the next stage of the OS or program.

Real Mode and 16-bit Basics

Upon reset, the CPU operates in 16-bit Real Mode.

  • Addressing: It uses segment:offset addressing.
  • Access: It can only access the first 1 MB of memory.
  • Registers: Key registers are 16-bit, including AX, BX, CX, DX (general purpose), SI, DI (index), and the segment registers DS, ES, SS, CS.

Segmentation and Addressing

The CPU calculates a 20-bit Physical Address using the 16-bit Segment and Offset registers:

Physical Address = Segment * 16 + offset
Enter fullscreen mode Exit fullscreen mode

Common pairs: DS:SI for string/data manipulation, ES:BX for disk buffers, and CS:IP for code execution.

BIOS Interrupts Overview

BIOS provides services through software interrupts, which are called using the int instruction. We'll focus on two:

Interrupt Purpose Example Register Setup
INT 0x10 Video services (e.g., printing characters). mov ah, 0x0E (Teletype function)
INT 0x13 Disk services (e.g., reading/writing sectors). mov ah, 0x02 (Read function)

Source Code Structure and Logic

The project is split into three modular assembly files for clarity and reusability:

File Purpose Key Function
print.asm Reusable routines for text output. print_string (using INT 0x10)
disk_read.asm Handles disk I/O with minimal error handling. read_sector (using INT 0x13)
stage1_bootloader.asm The main entry point and execution logic. Entry at 0x7C00

1. Printing Functions (print.asm)

These functions use INT 0x10, AH=0x0E (Teletype mode) to display characters.

; Print a single character in AL
print_char:
    mov ah, 0x0E    ; Teletype function
    mov bh, 0x00    ; Display page 0
    mov bl, 0x07    ; White on black color
    int 0x10
    ret

; Print a null-terminated string (DS:SI -> string)
print_string:
.print_loop:
    lodsb           ; Load byte from [DS:SI] into AL, increment SI
    cmp al, 0       ; Check for null-terminator (0)
    je .done
    call print_char
    jmp .print_loop
.done:
    ret
Enter fullscreen mode Exit fullscreen mode

2. Disk Reading Function (disk_read.asm)

This function uses INT 0x13, AH=0x02 to read one sector.

Register Value Description
AH 0x02 Function: Read Sector(s)
AL 0x01 Number of sectors to read
CH Cylinder (0-based)
CL Sector (1-based)
DH Head (0-based)
DL Drive (0x00 for floppy, 0x80 for hard disk)
ES:BX Destination buffer address

Important: Sector numbering starts at 1.

read_sector:
    ; Prerequisites: ES:BX (dest), DL (drive), CH/DH/CL (CHS)
    mov ah, 0x02
    mov al, 0x01
    int 0x13
    jc .fail        ; Jump if Carry Flag (CF) is set (failure)
    ret
.fail:
    mov si, read_error_msg
    call print_string
    jmp $           ; Halt forever on error
read_error_msg db "Disk Read Error", 0
Enter fullscreen mode Exit fullscreen mode

3. Bootloader Entry Point (stage1_bootloader.asm)

This is the main logic. We initialize segment registers, print the message, then configure the parameters for read_sector.

Parameter Value Description
ES 0x0000 Destination segment (Data to be loaded at 0x0000:0x0500)
BX 0x0500 Destination offset (Safe memory buffer)
CL 0x02 Sector 2 (The sector we are reading)
[BITS 16]
[ORG 0x7C00]
start:
    ; 1. Initialize segment registers to 0
    xor ax, ax
    mov ds, ax
    mov es, ax

    ; 2. Print initial message
    mov si, msg
    call print_string

    ; 3. Configure and call read_sector to load Sector 2 to 0x0500
    mov ax, 0x0000
    mov es, ax          ; Destination Segment ES=0x0000
    mov bx, 0x0500      ; Destination Offset BX=0x0500
    mov dl, 0x00        ; Drive 0 (Floppy)
    mov ch, 0x00        ; Cylinder 0
    mov cl, 0x02        ; Sector 2
    mov dh, 0x00        ; Head 0
    call read_sector

    ; 4. Print the loaded data (at 0x0500)
    mov si, 0x0500
    call print_string

    ; 5. Loop forever (halt)
    jmp $

msg db "Reading sector 2...", 0
%include "asm/print.asm"
%include "asm/disk_read.asm"

; Boot sector padding and signature
times 510 - ($ - $$) db 0
dw 0xAA55
Enter fullscreen mode Exit fullscreen mode

Running the Project

1. Assemble with NASM

This command converts the assembly code into a raw 512-byte binary file (boot.bin).

nasm -f bin asm/stage1_bootloader.asm -o boot.bin
Enter fullscreen mode Exit fullscreen mode

2. Run in QEMU

QEMU emulates the hardware (BIOS, CPU, disk). The -fda flag tells QEMU to load our binary as the floppy disk image, which the BIOS will then boot from.

qemu-system-i386 -boot a -fda boot.bin
Enter fullscreen mode Exit fullscreen mode

Expected Output:

The first line will be "Reading sector 2..." from the bootloader itself, followed immediately by the (potentially garbled) data contained within the actual Sector 2 of the virtual disk image.

Conclusion

By successfully building this basic bootloader, you've gained invaluable, low-level insight into the computer's startup process. You've directly interacted with the BIOS via interrupts, worked with real-mode addressing, and understood the critical hand-off from firmware to software.

This fundamental knowledge is the building block for all system-level development, from writing device drivers to developing a fully-fledged operating system.

Appendix: Quick BIOS Interrupt Reference

Interrupt AH Purpose Key Registers
INT 0x10 0x0E Teletype Output AL (Character), BL (Color)
INT 0x13 0x02 Read Sectors AL (Count), ES:BX (Buffer), CH/CL/DH (CHS)
INT 0x16 0x00 Wait for Keypress Returns key code in AL

Glossary

  • Bootloader: The program loaded by the BIOS to initialize the OS.
  • Sector: The smallest addressable unit of disk storage (512 bytes).
  • CHS: Cylinder-Head-Sector, the legacy addressing scheme for disk I/O.
  • Boot Signature (0xAA55): The required 2-byte marker at the end of the boot sector.
  • Real Mode: The 16-bit operating mode of the x86 CPU at reset.

GitHub Repo Link : https://github.com/aayush598/basic-bootloader-assembly

Top comments (1)

Collapse
 
fcojperez profile image
Francisco Perez • Edited

Pretty cool!!!, I hope I can find a spare time to try it. Thanks for sharing