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

  1. Reads 512 bytes from the boot device into physical address 0x7C00
  2. Checks the last two bytes are 0x55 0xAA (boot signature)
  3. Jumps to 0x7C00 in 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.