Writing a 16-bit Real Mode Bootloader in x86 Assembly
When you press the power button, the CPU wakes in real mode — 16-bit, no memory protection, 1MB of address space, direct BIOS services. The first code that runs is the bootloader, loaded from the first 512 bytes of the boot device.
What the BIOS Guarantees
- Reads 512 bytes from the boot device into physical address
0x7C00 - Checks the last two bytes are
0x55 0xAA(boot signature) - Jumps to
0x7C00in 16-bit real mode
That's it. Everything else is your problem.
Segment:Offset Addressing
Real mode uses segment:offset addressing:
physical_address = segment × 16 + offset
CS:IP = 0x07C0:0x0000 points to physical 0x7C00. You must set segment registers explicitly — forget DS and your data accesses produce garbage.
The Entry Point
nasm
[BITS 16]
[ORG 0x7C00]
start:
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
cld
mov si, msg_boot
call print_string
cli
hlt
print_string:
lodsb ; [SI] → AL, increment SI
test al, al
jz .done
mov ah, 0x0E ; BIOS INT 10h teletype
mov bh, 0x00
int 0x10
jmp print_string
.done:
ret
msg_boot db 'Booting...', 0x0D, 0x0A, 0
times 510 - ($ - $$) db 0
dw 0xAA55
BIOS Disk Read (INT 0x13)
nasm
load_sector:
mov ah, 0x02 ; Read sectors
mov al, 1 ; Sector count
mov ch, 0 ; Cylinder
mov cl, 2 ; Sector (1-indexed; sector 1 is the bootloader)
mov dh, 0 ; Head
mov dl, 0x80 ; First hard disk
mov bx, 0x8000 ; Load to ES:BX
int 0x13
jc disk_error ; Carry set on error
ret
Protected Mode Transition
nasm
enter_protected_mode:
lgdt [gdt_descriptor]
mov eax, cr0
or eax, 0x1
mov cr0, eax
jmp CODE_SEG:pm_entry ; far jump flushes pipeline
[BITS 32]
pm_entry:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov esp, 0x90000
; 32-bit protected mode active
Testing with QEMU
bash
nasm -f bin bootloader.asm -o bootloader.bin
qemu-system-x86_64 -drive format=raw,file=bootloader.bin
QEMU loads the first 512 bytes at 0x7C00 exactly as real hardware would — the fastest possible feedback loop for bootloader development.
Source: SimpleRealModeBootLoader on GitHub.