Master bootloader development: BIOS boot process, MBR structure, real mode assembly, disk I/O with BIOS interrupts, loading a kernel, and transitioning to protected/long mode.
Offset Size Description
0x000 446 Bootloader code
0x1BE 64 Partition table (4 × 16 bytes)
0x1FE 2 Boot signature (0x55, 0xAA)
[BITS 16] ; 16-bit real mode
[ORG 0x7C00] ; BIOS loads us here
start:
; Set up segment registers
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ; Stack below bootloader
; Print "Hello"
mov si, message
call print_string
; Halt
cli ; Disable interrupts
hlt ; Halt CPU
print_string:
mov ah, 0x0E ; BIOS teletype function
.loop:
lodsb ; Load byte from SI into AL
test al, al ; Check for null terminator
jz .done
int 0x10 ; BIOS video interrupt
jmp .loop
.done:
ret
message: db "Hello from bootloader!", 0
; Pad to 510 bytes and add boot signature
times 510 - ($ - $$) db 0
dw 0xAA55 ; Boot signature
# Assemble and test
nasm -f bin boot.asm -o boot.bin
qemu-system-x86_64 -drive format=raw,file=boot.bin
INT 10h provides BIOS video services—your interface to the screen before any OS is loaded.
| AH | Function | Parameters | Returns |
|---|---|---|---|
| 0x00 | Set Video Mode | AL = mode (0x03=text, 0x13=VGA) | - |
| 0x02 | Set Cursor Position | BH=page, DH=row, DL=col | - |
| 0x03 | Get Cursor Position | BH=page | DH=row, DL=col |
| 0x0E | Teletype Output | AL=char, BL=color | - |
| 0x13 | Write String | ES:BP=string, CX=len, DH/DL=pos | - |
; Video services examples
bits 16
org 0x7C00
; Set 80x25 text mode
mov ah, 0x00
mov al, 0x03 ; 80x25 color text
int 0x10
; Set cursor to row 5, column 10
mov ah, 0x02
mov bh, 0 ; Page 0
mov dh, 5 ; Row
mov dl, 10 ; Column
int 0x10
; Print character 'A' in light green
mov ah, 0x0E
mov al, 'A'
mov bl, 0x0A ; Light green
int 0x10
; Print string using INT 10h function 13h
mov ah, 0x13
mov al, 1 ; Mode: update cursor
mov bh, 0 ; Page 0
mov bl, 0x0F ; White on black
mov cx, msg_len ; String length
mov dh, 10 ; Row 10
mov dl, 20 ; Column 20
mov bp, message ; ES:BP = string
int 0x10
message: db "Boot OK!"
msg_len equ $ - message
INT 13h is the BIOS disk interface. Use it to load your kernel from disk into memory.
| AH | Function | Key Parameters |
|---|---|---|
| 0x00 | Reset Disk | DL = drive (0=floppy, 0x80=HDD) |
| 0x02 | Read Sectors | AL=count, CH=cyl, CL=sector, DH=head, DL=drive, ES:BX=buffer |
| 0x03 | Write Sectors | Same as 0x02 |
| 0x08 | Get Drive Parameters | DL=drive; Returns geometry |
| 0x42 | Extended Read (LBA) | DS:SI = DAP (Disk Address Packet) |
; Read sectors using CHS (Cylinder-Head-Sector)
read_sectors_chs:
; Parameters: Read 2 sectors starting from sector 2
mov ah, 0x02 ; Read sectors function
mov al, 2 ; Number of sectors
mov ch, 0 ; Cylinder 0
mov cl, 2 ; Starting sector (1-based!)
mov dh, 0 ; Head 0
mov dl, 0x80 ; Drive 0x80 = first hard disk
mov bx, 0x1000 ; ES:BX = buffer (0x0000:0x1000)
int 0x13
jc .disk_error ; Carry flag set on error
cmp al, 2 ; Verify sectors read
jne .disk_error
ret
.disk_error:
; Handle read failure
mov si, disk_err_msg
call print_string
hlt
; Modern method: LBA Read (supports drives > 8GB)
read_sectors_lba:
mov ah, 0x42 ; Extended read
mov dl, 0x80 ; Drive
mov si, dap ; DS:SI = Disk Address Packet
int 0x13
jc .disk_error
ret
align 4
dap: ; Disk Address Packet (DAP)
db 16 ; Size of DAP (16 bytes)
db 0 ; Reserved
dw 4 ; Number of sectors to read
dw 0x1000 ; Offset (buffer)
dw 0x0000 ; Segment (buffer)
dq 1 ; Starting LBA (sector 1 = second sector)
A real bootloader loads the kernel from disk into memory, then jumps to it. Here's a two-stage approach:
Memory Map During Boot:
0x00000 - 0x003FF Interrupt Vector Table (IVT)
0x00400 - 0x004FF BIOS Data Area
0x00500 - 0x07BFF Free (usable for stack)
0x07C00 - 0x07DFF Boot Sector (512 bytes) ← Stage 1
0x07E00 - 0x7FFFF Free (usable for Stage 2 + kernel)
0x80000 - 0x9FFFF Extended BIOS Data Area
0xA0000 - 0xBFFFF Video Memory
0xC0000 - 0xFFFFF BIOS ROM
; stage1.asm - Boot sector that loads stage 2
bits 16
org 0x7C00
STAGE2_ADDR equ 0x7E00 ; Load stage 2 right after boot sector
STAGE2_SECTORS equ 16 ; 16 sectors = 8KB stage 2
_start:
; Setup segments
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
; Save boot drive
mov [boot_drive], dl
; Print loading message
mov si, loading_msg
call print_string
; Load stage 2 from disk
mov ah, 0x02 ; Read sectors
mov al, STAGE2_SECTORS ; Sector count
mov ch, 0 ; Cylinder 0
mov cl, 2 ; Start at sector 2 (sector 1 = boot sector)
mov dh, 0 ; Head 0
mov dl, [boot_drive]
mov bx, STAGE2_ADDR ; ES:BX = destination
int 0x13
jc disk_error
; Jump to stage 2
jmp STAGE2_ADDR
disk_error:
mov si, error_msg
call print_string
hlt
print_string:
lodsb
test al, al
jz .done
mov ah, 0x0E
int 0x10
jmp print_string
.done:
ret
boot_drive: db 0
loading_msg: db "Loading stage 2...", 13, 10, 0
error_msg: db "Disk error!", 0
times 510 - ($ - $$) db 0
dw 0xAA55
stage1.asmAll Platforms (flat binary — no OS-specific linking)
nasm -f bin stage1.asm -o stage1.bin
qemu-system-x86_64 -drive format=raw,file=stage1.bin
; stage2.asm - More room for kernel loading, A20, etc.
bits 16
org 0x7E00
KERNEL_ADDR equ 0x10000 ; Load kernel at 64KB mark
stage2_start:
mov si, stage2_msg
call print_string
; Enable A20 line (access memory > 1MB)
call enable_a20
; Load kernel (example: 64 sectors = 32KB)
mov ah, 0x02
mov al, 64
mov ch, 0
mov cl, 18 ; After stage 2 sectors
mov dh, 0
mov dl, 0x80
mov bx, KERNEL_ADDR
int 0x13
; Setup GDT and enter protected mode...
; (See Mode Transition section)
jmp $
enable_a20:
; Fast A20 via port 0x92
in al, 0x92
or al, 2
out 0x92, al
ret
stage2_msg: db "Stage 2 loaded!", 13, 10, 0
stage2.asmAll Platforms (flat binary — combine with stage1 to run)
nasm -f bin stage2.asm -o stage2.bin
cat stage1.bin stage2.bin > os.img
qemu-system-x86_64 -drive format=raw,file=os.img
Transitioning from real mode to protected/long mode is the bootloader's most critical task. Here's the complete sequence:
Mode Transition Checklist:
☐ 1. Disable interrupts (CLI)
☐ 2. Enable A20 gate (access > 1MB)
☐ 3. Load Global Descriptor Table (LGDT)
☐ 4. Set CR0.PE = 1 (Protected Mode Enable)
☐ 5. Far jump to flush prefetch queue
☐ 6. Reload segment registers with GDT selectors
For 64-bit Long Mode, also:
☐ 7. Enable PAE (CR4.PAE = 1)
☐ 8. Setup 4-level page tables
☐ 9. Load CR3 with PML4 address
☐ 10. Enable Long Mode (EFER.LME = 1)
☐ 11. Enable Paging (CR0.PG = 1)
☐ 12. Far jump to 64-bit code segment
; Complete Real → Protected Mode transition
bits 16
enter_protected_mode:
cli ; 1. Disable interrupts
; 2. Enable A20 (keyboard controller method)
call .wait_ready
mov al, 0xAD ; Disable keyboard
out 0x64, al
call .wait_ready
mov al, 0xD0 ; Read output port
out 0x64, al
call .wait_data
in al, 0x60 ; Get current value
push ax
call .wait_ready
mov al, 0xD1 ; Write output port
out 0x64, al
call .wait_ready
pop ax
or al, 2 ; Set A20 bit
out 0x60, al
call .wait_ready
mov al, 0xAE ; Enable keyboard
out 0x64, al
jmp .a20_done
.wait_ready:
in al, 0x64
test al, 2
jnz .wait_ready
ret
.wait_data:
in al, 0x64
test al, 1
jz .wait_data
ret
.a20_done:
; 3. Load GDT
lgdt [gdt_descriptor]
; 4. Enable Protected Mode
mov eax, cr0
or eax, 1
mov cr0, eax
; 5. Far jump (flushes pipeline, loads CS)
jmp 0x08:protected_entry
bits 32
protected_entry:
; 6. Reload segment registers
mov ax, 0x10 ; Data segment selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x90000 ; Stack in free memory
; Now in 32-bit Protected Mode!
; Continue to long mode setup or jump to kernel...
jmp pm_kernel_entry
; GDT for Protected Mode
align 8
gdt_start:
dq 0 ; Null descriptor
gdt_code:
dw 0xFFFF, 0x0000 ; Limit, Base 0:15
db 0x00 ; Base 16:23
db 10011010b ; Access: Code, readable
db 11001111b ; Flags + Limit 16:19
db 0x00 ; Base 24:31
gdt_data:
dw 0xFFFF, 0x0000
db 0x00
db 10010010b ; Access: Data, writable
db 11001111b
db 0x00
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; Size - 1
dd gdt_start ; Address
QEMU is essential for bootloader development. It provides fast iteration and powerful debugging capabilities.
# Build and run bootloader
nasm -f bin bootloader.asm -o boot.bin
# Run in QEMU (legacy BIOS)
qemu-system-x86_64 -drive format=raw,file=boot.bin
# Create proper disk image with kernel
cat boot.bin kernel.bin > os.img
# Pad to floppy size (optional)
truncate -s 1440k os.img
qemu-system-x86_64 -fda os.img
# Hard disk emulation
qemu-system-x86_64 -hda os.img
# Start QEMU paused, waiting for GDB connection
qemu-system-x86_64 -drive format=raw,file=boot.bin -s -S
# In another terminal, connect GDB
gdb -ex "target remote :1234" \
-ex "set architecture i8086" \
-ex "break *0x7c00" \
-ex "continue"
# Useful GDB commands for bootloader debugging:
(gdb) x/10i $pc # Disassemble at current position
(gdb) info registers # Show all registers
(gdb) x/16xb 0x7c00 # Examine memory (hex bytes)
(gdb) stepi # Step one instruction
(gdb) nexti # Step over (skip calls)
(gdb) break *0x7c20 # Breakpoint at address
| Option | Description |
|---|---|
-s | Start GDB server on port 1234 |
-S | Pause CPU at startup (wait for GDB) |
-d int | Log interrupts to console |
-d cpu,exec | Log CPU state and executed blocks |
-D qemu.log | Write debug output to file |
-nographic | Disable graphical output, use serial |
-serial mon:stdio | Redirect serial to terminal |
-m 512M | Set RAM size to 512MB |
-monitor stdio | QEMU monitor in terminal |
# Press Ctrl+Alt+2 in QEMU window for monitor, or use -monitor stdio
(qemu) info registers # Show CPU registers
(qemu) info mem # Show memory mappings
(qemu) xp /10i 0x7c00 # Disassemble memory
(qemu) x /16xb 0x7c00 # Examine memory
(qemu) gdbserver # Enable GDB server
(qemu) stop # Pause emulation
(qemu) cont # Continue emulation
(qemu) q # Quit QEMU
-s -Sx/2xb 0x7dfe)