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.
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:
[BITS 16]
- tells NASM to assemble for 16-bit mode (real mode)[ORG 0x7c00]
- sets code origin, where BIOS loads the bootloadermov si, msg
- loads message string pointerprint_string
- loop that prints each character using BIOS int 0x10
times 510-($-$$) db 0
- pads the bootloader to 510 bytes (total boot sector size is 512 bytes)dw 0xAA55
- mandatory boot signatureUse 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.
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.
Use QEMU to boot your floppy disk image in a virtual machine:
qemu-system-x86_64 -fda floppy.img
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
[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