💾 Building a Bootable Floppy Disk Image with NASM & QEMU

This expanded guide will show you how to build a simple bootloader, create a floppy disk image with it, and run it using QEMU. We’ll go deeper into the code, setup tools, and next steps for extending your bootloader skills.

🧱 Step 1: Writing Your Bootloader

Your bootloader is the first code the BIOS executes after loading it into memory at 0x7c00. Here’s a simple example that prints a message using BIOS interrupts:

Simulation output will appear here...

Understanding the Code

BIOS boot flow diagram

🖥️ Step 2: Assemble the Bootloader

Use NASM to assemble the bootloader into a raw binary:

nasm -f bin boot.asm -o boot.img

This creates a 512-byte file named boot.img, which represents a floppy disk boot sector.

💽 Step 3: Create a Floppy Disk Image

A floppy disk image is a raw file that represents a floppy disk’s data. You can create a blank 1.44MB floppy image and copy your bootloader onto it:

dd if=/dev/zero of=floppy.img bs=512 count=2880
dd if=boot.img of=floppy.img conv=notrunc

floppy.img is now a full floppy disk image with your bootloader in the first sector.

Floppy disk image structure

🚀 Step 4: Boot the Image with QEMU

Use QEMU to boot your floppy disk image in a virtual machine:

qemu-system-x86_64 -fda floppy.img
QEMU running bootloader

🛠️ Step 5: Next Steps — Reading Sectors & Multi-sector Bootloader

Your bootloader is limited to 512 bytes — that’s why you might want to load more sectors from disk to expand functionality. You can use BIOS interrupt int 0x13 to read sectors from the floppy, and chain-load other code.

This unlocks possibilities for simple OS loaders, or even tiny games.

// Example BIOS disk read (in NASM)
mov ah, 0x02        ; read sectors function
mov al, 1           ; number of sectors to read
mov ch, 0           ; track 0
mov cl, 2           ; sector 2 (first after boot sector)
mov dh, 0           ; head 0
mov dl, 0           ; drive 0 (floppy)
mov bx, buffer      ; segment:offset where data will be loaded
int 0x13            ; BIOS disk service
  
⚠️ Keep your code under 512 bytes or load additional sectors carefully to avoid overwriting important data.

🖥️ TinyASM OS Bootloader Simulation

[BITS 16]
[ORG 0x7c00]

start:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    sti

    call print_dashboard

main_loop:
    mov ah, 0x00
    int 0x16
    cmp al, 'W'
    je write_app
    cmp al, 'w'
    je write_app
    cmp al, 'P'
    je paint_app
    cmp al, 'p'
    je paint_app
    cmp al, 'S'
    je shutdown
    cmp al, 's'
    je shutdown
    jmp main_loop

print_dashboard:
    mov si, dash_msg
    call print_string
    ret

write_app:
    call clear_screen
    mov si, write_header
    call print_string
    mov cx, 0
    mov dx, 5

write_loop:
    call set_cursor_pos
    mov ah, 0x00
    int 0x16
    cmp al, 0x1B
    je return_dashboard
    cmp al, 0x08
    je write_backspace
    cmp al, 0x20
    jb write_loop
    cmp al, 0x7E
    ja write_loop
    push ax
    mov ah, 0x0E
    int 0x10
    pop ax
    inc cx
    cmp cx, 80
    jne write_loop
    mov cx, 0
    inc dx
    cmp dx, 24
    jne write_loop
    jmp write_loop

write_backspace:
    cmp cx, 0
    je write_loop
    dec cx
    call set_cursor_pos
    mov ah, 0x0E
    mov al, ' '
    int 0x10
    call set_cursor_pos
    jmp write_loop

paint_app:
    call clear_screen
    mov si, paint_header
    call print_string
    mov cx, 40
    mov dx, 12

paint_loop:
    call set_cursor_pos
    mov ah, 0x00
    int 0x16
    cmp al, 0x1B
    je return_dashboard
    cmp al, 0x20
    jne paint_check_arrows

    mov ah, 0x08
    mov bh, 0
    mov dh, dl
    mov dl, cl
    int 0x10
    mov bl, al

    cmp bl, 0xDB
    je paint_erase_block

    mov ah, 0x0E
    mov al, 0xDB
    int 0x10
    jmp paint_loop

paint_erase_block:
    mov ah, 0x0E
    mov al, ' '
    int 0x10
    jmp paint_loop

paint_check_arrows:
    cmp al, 0
    jne paint_loop
    mov ah, 0
    int 0x16
    mov ah, 0x00
    int 0x16
    mov ah, 0x00

    cmp ah, 0x48
    jne paint_right_check
    cmp dx, 5
    jle paint_loop
    dec dx
    jmp paint_loop

paint_right_check:
    cmp ah, 0x4B
    jne paint_down_check
    cmp cx, 0
    jle paint_loop
    dec cx
    jmp paint_loop

paint_down_check:
    cmp ah, 0x50
    jne paint_left_check
    cmp dx, 23
    jge paint_loop
    inc dx
    jmp paint_loop

paint_left_check:
    cmp ah, 0x4D
    jne paint_loop
    cmp cx, 79
    jge paint_loop
    inc cx
    jmp paint_loop

set_cursor_pos:
    mov ah, 0x02
    mov bh, 0
    mov dh, dl
    mov dl, cl
    int 0x10
    ret

print_string:
    lodsb
    cmp al, 0
    je print_done
    mov ah, 0x0E
    int 0x10
    jmp print_string
print_done:
    ret

clear_screen:
    mov ax, 0x0600
    mov bh, 0x07
    mov cx, 0
    mov dx, 0x184F
    int 0x10
    ret

return_dashboard:
    jmp start

shutdown:
    mov si, shutdown_msg
    call print_string
    cli
    hlt
    jmp shutdown

dash_msg db 0x0D,0x0A, "=== TinyASM OS Dashboard ===", 0x0D,0x0A
         db "W: Write App", 0x0D,0x0A
         db "P: Paint App", 0x0D,0x0A
         db "S: Shut Down", 0x0D,0x0A, 0

write_header db 0x0D,0x0A, "Write App (type your text, ESC to return):", 0x0D,0x0A, 0
paint_header db 0x0D,0x0A, "Paint App (use arrows, SPACE to toggle block, ESC to return):", 0x0D,0x0A, 0
shutdown_msg db 0x0D,0x0A, "Shutting down... Goodbye!", 0x0D,0x0A, 0

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

🖥️ Simmulated Output

Welcome to TinyASM OS Dashboard! Press W for Write, P for Paint, S to Shutdown.