Project 1: The 512-Byte Hello World Bootloader
Write the very first code that runs when a computer boots - a minimal bootloader that displays “Hello, Bare Metal!” using BIOS interrupts, all within exactly 512 bytes.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | ★★☆☆☆ Beginner |
| Time Estimate | Weekend (6-12 hours) |
| Language | x86 Assembly (NASM) |
| Prerequisites | Basic hexadecimal, willingness to read x86 assembly |
| Key Topics | BIOS boot process, Real Mode, MBR structure, INT 10h video services, segment:offset addressing |
1. Learning Objectives
After completing this project, you will be able to:
- Explain what happens in the first milliseconds after a computer powers on
- Write x86 real mode assembly that executes without any operating system
- Understand the Master Boot Record (MBR) structure and boot signature requirements
- Use BIOS interrupts for basic I/O (video output via INT 10h)
- Debug bare-metal code using QEMU and its monitor
- Initialize CPU segment registers and set up a stack in real mode
- Create bootable disk images and test them in emulators
2. Theoretical Foundation
2.1 Core Concepts
The Reset Vector and CPU Initialization
When you press the power button, the CPU doesn’t start executing your code immediately. Here’s the sequence:
- Power Supply Stabilization: The PSU converts AC to DC (3.3V, 5V, 12V rails) and waits for voltages to stabilize before sending the “Power Good” signal
- CPU Reset: All registers are set to known values. The instruction pointer points to the Reset Vector (0xFFFFFFF0 on x86)
- BIOS Execution: The CPU begins executing BIOS code from firmware ROM
- POST (Power-On Self Test): BIOS tests CPU, memory, and basic hardware
- Boot Device Search: BIOS reads the first sector (512 bytes) from the boot device to address 0x7C00
- Boot Signature Check: BIOS verifies bytes 510-511 are 0x55 0xAA
- Handoff: BIOS jumps to 0x7C00 - YOUR CODE NOW RUNS!
Power Button → PSU Stabilizes → CPU Reset → BIOS POST → Load MBR → Check 0xAA55 → JMP 0x7C00
↑
Your bootloader!
x86 Real Mode
When the BIOS hands control to your bootloader, the CPU is in 16-bit Real Mode - a compatibility mode that behaves like the original 8086 from 1978.
Real Mode Characteristics:
- 16-bit registers (AX, BX, CX, DX, SI, DI, BP, SP)
- Segment registers (CS, DS, ES, SS) for memory addressing
- Maximum 1MB addressable memory (20-bit addresses)
- No memory protection - any code can access any memory
- No privilege levels - everything runs at maximum privilege
- Direct hardware access via IN/OUT instructions
- BIOS interrupts available for I/O
Memory Addressing (Segment:Offset):
Physical Address = (Segment × 16) + Offset
Example: CS=0x07C0, IP=0x0000
Physical = 0x07C0 × 16 + 0x0000 = 0x7C00
Example: DS=0x0000, SI=0x7C50
Physical = 0x0000 × 16 + 0x7C50 = 0x7C50
The Master Boot Record (MBR)
The MBR is the first 512 bytes of a bootable disk:
┌─────────────────────────────────────────────────────────────┐
│ Offset │ Size │ Content │
├────────┼───────┼────────────────────────────────────────────┤
│ 0x000 │ 446 │ Bootstrap Code (YOUR BOOTLOADER) │
│ 0x1BE │ 16 │ Partition Entry 1 (optional) │
│ 0x1CE │ 16 │ Partition Entry 2 (optional) │
│ 0x1DE │ 16 │ Partition Entry 3 (optional) │
│ 0x1EE │ 16 │ Partition Entry 4 (optional) │
│ 0x1FE │ 2 │ Boot Signature (0x55, 0xAA) │
└────────┴───────┴────────────────────────────────────────────┘
Critical Rule: Without the 0xAA55 signature at bytes 510-511, the BIOS will NOT execute your code.
2.2 Why This Matters
This project matters because:
-
Security Foundation: Every security vulnerability at boot level is catastrophic. Rootkits, bootkits, and firmware malware can hide from all OS-level security. Understanding boot is understanding the root of trust.
-
Cloud Computing: Every EC2 instance, every container host, every virtual machine boots. Understanding this is understanding infrastructure at its core.
-
Embedded Systems: Your car’s ECU, your router, your smart TV - all boot through code someone wrote. This is the foundation of embedded programming.
-
Debugging Power: When Windows won’t start, understanding boot is the difference between fixing it and reinstalling. You become the person who can solve “impossible” problems.
-
Interview Advantage: “I wrote a bootloader from scratch” instantly sets you apart. It demonstrates deep systems understanding that most programmers never achieve.
2.3 Historical Context
1981 - The Original IBM PC: The BIOS boot process was designed for the IBM PC. The 512-byte sector size came from standard floppy disk formats. The 0x7C00 load address was chosen to leave room for BIOS data structures (IVT at 0x0000, BDA at 0x0400) and provide space for the bootloader’s stack.
Why 0x7C00?: This address equals 32KB - 1KB. IBM engineers wanted the bootloader at a 1KB boundary, leaving 31KB below for BIOS use and allowing the bootloader to use the memory immediately below (from 0x0500 to 0x7BFF, about 30KB) for its own purposes.
Why 0xAA55?: This “magic number” (10101010 01010101 in binary) was chosen as a simple pattern that’s unlikely to appear by accident in uninitialized memory. The byte order (0x55 first, then 0xAA) is due to x86’s little-endian architecture.
2.4 Common Misconceptions
Misconception 1: “The bootloader runs in a normal environment”
- Reality: There is NO operating system, NO standard library, NO runtime. You have raw CPU instructions and BIOS services.
printf()doesn’t exist.
Misconception 2: “I can just write C code”
- Reality: C code needs a stack (which you must set up), initialized data segments (which don’t exist), and typically a runtime. Pure C bootloaders require an assembly stub.
Misconception 3: “Memory addresses are virtual”
- Reality: In real mode, all addresses are physical. There’s no MMU, no paging, no translation. Address 0x7C00 is physical RAM location 0x7C00.
Misconception 4: “The segment registers are initialized”
- Reality: BIOS does NOT guarantee segment register values. DS, ES, SS might contain garbage. You MUST initialize them.
Misconception 5: “Debugging is like normal debugging”
- Reality: There’s no GDB, no stack traces, no printf debugging. A bug typically manifests as a frozen screen or immediate reboot. You debug with QEMU monitor, hex dumps, and careful reasoning.
3. Project Specification
3.1 What You Will Build
A complete bootloader that:
- Fits in exactly 512 bytes
- Sets up segment registers and stack correctly
- Prints “Hello, Bare Metal!” to the screen using BIOS video services
- Halts the CPU cleanly after printing
- Contains the required 0xAA55 boot signature
3.2 Functional Requirements
- Bootloader must boot successfully in QEMU
- Display the message “Hello, Bare Metal!” on screen
- Cursor should remain visible after the message
- System should halt (not reboot or execute garbage)
- Must work with
qemu-system-x86_64 -drive format=raw,file=boot.bin
3.3 Non-Functional Requirements
- File size must be EXACTLY 512 bytes
- Last two bytes must be 0x55 0xAA (little-endian 0xAA55)
- Code must be position-independent OR use ORG 0x7C00 correctly
- Should work on QEMU, Bochs, and VirtualBox
- Should theoretically work on real hardware (Legacy/CSM boot)
3.4 Example Usage / Output
# Assemble the bootloader
$ nasm -f bin boot.asm -o boot.bin
# Verify size and signature
$ ls -l boot.bin
-rw-r--r-- 1 user user 512 Jan 15 10:30 boot.bin
$ hexdump -C boot.bin | tail -n 1
000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa
# Run in QEMU
$ qemu-system-x86_64 -drive format=raw,file=boot.bin
Expected QEMU window:
Hello, Bare Metal!
_
The cursor blinks on a black screen with your message.
3.5 Real World Outcome
When you complete this project:
- You will have written the very first code that runs on a (virtual) machine
- You will understand what “bare metal” programming actually means
- You will be able to explain the boot process to anyone
- You will have a foundation for all subsequent bootloader projects
- You will have resume-worthy evidence of low-level systems knowledge
4. Solution Architecture
4.1 High-Level Design
┌─────────────────────────────────────────────────────────────┐
│ BOOT SEQUENCE │
├─────────────────────────────────────────────────────────────┤
│ │
│ BIOS │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ BOOTLOADER (512 bytes at 0x7C00) │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ 1. Initialize Segment Registers (DS=ES=SS=0) │ │
│ │ └── XOR AX, AX; MOV DS, AX; MOV ES, AX; etc. │ │
│ │ │ │
│ │ 2. Set Up Stack (SS:SP = 0:0x7C00) │ │
│ │ └── MOV SP, 0x7C00 (stack grows DOWN) │ │
│ │ │ │
│ │ 3. Print Loop │ │
│ │ └── Load message address into SI │ │
│ │ └── For each character: │ │
│ │ └── LODSB (load byte, advance SI) │ │
│ │ └── Check for null terminator │ │
│ │ └── INT 10h, AH=0Eh (teletype output) │ │
│ │ │ │
│ │ 4. Halt │ │
│ │ └── HLT; JMP $ (infinite loop) │ │
│ │ │ │
│ │ 5. Data │ │
│ │ └── "Hello, Bare Metal!", 0 │ │
│ │ │ │
│ │ 6. Padding + Signature │ │
│ │ └── TIMES 510-($-$$) DB 0 │ │
│ │ └── DW 0xAA55 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 Key Components
- Segment Initialization Block: Sets DS, ES, SS to known values (typically 0)
- Stack Setup: Points SP to a safe location below 0x7C00
- Print Routine: Uses LODSB instruction to iterate through null-terminated string
- BIOS Video Call: INT 10h with AH=0Eh for teletype output
- Halt Loop: HLT instruction in an infinite loop
- Message Data: Null-terminated ASCII string
- Padding: Zeros to reach byte 510
- Boot Signature: The magic 0xAA55 word
4.3 Data Structures
Message String:
msg: db 'Hello, Bare Metal!', 0
; H e l l o , B a r e M e t a l ! \0
; 48 65 6C 6C 6F 2C 20 42 61 72 65 20 4D 65 74 61 6C 21 00
MBR Layout:
Offset Contents
0x0000 JMP instruction (skip over data if any) or start of code
... Your bootloader code (~440 bytes usable)
0x01F0 Message string (if placed at end)
0x01FE 0x55 (first byte of signature)
0x01FF 0xAA (second byte of signature)
4.4 Algorithm Overview
ALGORITHM: Print String in Real Mode
1. CLI (disable interrupts while setting up stack)
2. XOR AX, AX
3. MOV DS, AX ; DS = 0 for data access
4. MOV ES, AX ; ES = 0
5. MOV SS, AX ; SS = 0
6. MOV SP, 0x7C00 ; Stack below bootloader
7. STI (re-enable interrupts)
8. MOV SI, message_address
9. LOOP:
a. LODSB ; AL = [DS:SI], SI++
b. OR AL, AL ; Test for zero
c. JZ DONE ; If zero, exit loop
d. MOV AH, 0x0E ; BIOS teletype function
e. MOV BH, 0 ; Page 0
f. INT 0x10 ; Call BIOS
g. JMP LOOP
10. DONE:
a. HLT ; Halt CPU
b. JMP DONE ; In case of interrupt, halt again
5. Implementation Guide
5.1 Development Environment Setup
Required Tools:
# macOS
brew install nasm qemu
# Ubuntu/Debian
sudo apt update
sudo apt install nasm qemu-system-x86
# Fedora/RHEL
sudo dnf install nasm qemu-system-x86
# Verify installation
nasm -version # Should show NASM version 2.14+
qemu-system-x86_64 --version # Should show QEMU version 6.0+
Optional Tools:
# Hex viewer/editor
brew install hexdump # macOS (usually pre-installed)
apt install xxd # Linux
# Bochs (more accurate than QEMU, better for debugging)
brew install bochs
apt install bochs bochs-x
5.2 Project Structure
bootloader-hello/
├── boot.asm # Main bootloader source
├── Makefile # Build automation
├── boot.bin # Compiled binary (generated)
└── README.md # Your notes
Simple Makefile:
all: boot.bin
boot.bin: boot.asm
nasm -f bin boot.asm -o boot.bin
run: boot.bin
qemu-system-x86_64 -drive format=raw,file=boot.bin
debug: boot.bin
qemu-system-x86_64 -drive format=raw,file=boot.bin -monitor stdio
clean:
rm -f boot.bin
.PHONY: all run debug clean
5.3 The Core Question You’re Answering
“What is the very first code that runs when a computer boots, and how does it work without an operating system?”
This project answers the fundamental question of how a computer transitions from powered-off hardware to running code. You’ll discover:
- How the BIOS/firmware finds and executes your code
- Why the first sector of a disk is special (Master Boot Record)
- How to interact with hardware when there’s no OS, no standard library, and no runtime
- What “bare metal” programming actually means in practice
This is the foundation of understanding every boot process, from embedded systems to modern UEFI systems.
5.4 Concepts You Must Understand First
Before writing code, ensure you can answer these self-assessment questions:
Memory Addressing at Boot (0x7C00):
- Q: Why does the BIOS load the bootloader to 0x7C00 specifically?
- A: Historical convention from IBM PC (32KB - 1KB for boot sector)
x86 Real Mode Operation:
- Q: What are the limitations of real mode?
- A: 1MB addressable memory, 16-bit registers, no memory protection
Segment:Offset Addressing:
- Q: What’s the physical address of 0x07C0:0x0000?
- A: (0x07C0 × 16) + 0x0000 = 0x7C00
BIOS Interrupts:
- Q: How do you print a character using BIOS?
- A: INT 10h with AH=0Eh, character in AL
MBR Structure:
- Q: Why is the boot signature 0xAA55?
- A: BIOS checks these bytes to verify sector is bootable (little-endian: bytes are 0x55, 0xAA)
5.5 Questions to Guide Your Design
Memory and Addressing:
- How does
ORG 0x7C00affect label addresses in your code? - Why must you initialize segment registers (DS, ES, SS)?
- Where should you place your stack, and why?
BIOS Interaction:
- How do you print characters using INT 10h?
- What state does the BIOS leave the system in when it jumps to 0x7C00?
Code Structure:
- How do you ensure your bootloader is exactly 512 bytes?
- Should your bootloader halt, hang, or loop after printing?
Debugging:
- If nothing appears on screen, what could be wrong?
5.6 Thinking Exercise
Before writing any code, trace through the boot process mentally:
-
Power-On (T=0): CPU starts executing BIOS code from firmware ROM
- BIOS Boot Search (T=2 seconds):
- BIOS reads first sector (512 bytes) to memory address 0x7C00
- BIOS checks if bytes 510-511 equal 0x55, 0xAA
- If yes: Jump to 0x7C00
- Your Code Executes (T=2.1 seconds):
- CPU instruction pointer (IP) = 0x7C00, Code Segment (CS) = 0x0000
- If your first instruction is segment setup, trace what each register becomes
- If
msgis at offset 30 from start, SI will load 0x7C00 + 30 = 0x7C1E
- Printing “Hello”:
- You load ‘H’ (0x48) into AL
- You call INT 10h with AH=0Eh
- BIOS writes ‘H’ to video memory, cursor advances
Draw the memory map:
Address Range | Contents
-----------------|---------------------------------
0x0000 - 0x03FF | BIOS Interrupt Vector Table
0x0400 - 0x04FF | BIOS Data Area
0x0500 - 0x7BFF | Free (your stack can go here)
0x7C00 - 0x7DFF | Your bootloader (512 bytes)
0x7E00 - 0x9FFFF | Free
0xA0000 - 0xFFFFF| Video memory, BIOS ROM, etc.
5.7 Hints in Layers
Use these progressive hints only when stuck. Try each level before moving to the next.
Hint 1: Starting with the Skeleton
Your bootloader needs this basic structure:
[BITS 16] ; We're in 16-bit Real Mode
[ORG 0x7C00] ; BIOS loads us here
start:
; Initialize segment registers here
; Set up stack here
; Your code to print message here
; Infinite loop or halt here
message: db 'Hello, Bare Metal!', 0
; Padding and boot signature here
Hint 2: Setting Up Segments
At boot, segment registers have undefined values:
start:
xor ax, ax ; AX = 0
mov ds, ax ; DS = 0 (data segment)
mov es, ax ; ES = 0 (extra segment)
mov ss, ax ; SS = 0 (stack segment)
mov sp, 0x7C00 ; Stack grows DOWN from 0x7C00
Hint 3: Printing Loop
Print each character using INT 10h:
mov si, message ; SI points to string
print_loop:
lodsb ; Load byte at [SI] into AL, increment SI
cmp al, 0 ; Check for null terminator
je done ; If zero, we're done
mov ah, 0x0E ; Teletype output function
mov bh, 0 ; Page 0
int 0x10 ; Call BIOS video service
jmp print_loop ; Repeat for next character
done:
hlt ; Halt the CPU
jmp done ; If interrupts wake CPU, halt again
Hint 4: Padding and Signature
Ensure exactly 512 bytes with signature at end:
times 510-($-$$) db 0 ; Pad with zeros to byte 510
dw 0xAA55 ; Boot signature (little-endian)
Understanding the syntax:
$: Current position$$: Start of section (with ORG, equals 0x7C00)$-$$: Bytes written so far510-($-$$): Remaining bytes to reach 510times: Repeat instructiondw 0xAA55: Write word (2 bytes) in little-endian
Hint 5: Debugging with QEMU
# Does it boot?
qemu-system-x86_64 -drive format=raw,file=boot.bin
# Is code running? (add at start: jmp start)
# If QEMU hangs, code is executing
# Test single character:
start:
mov ah, 0x0E
mov al, 'A'
int 0x10
jmp $
# Inspect with QEMU monitor:
qemu-system-x86_64 -drive format=raw,file=boot.bin -monitor stdio
(qemu) info registers
(qemu) x/20i 0x7C00 # Disassemble
(qemu) x/512xb 0x7C00 # Hex dump
Hint 6: Complete Working Example (Last Resort)
[BITS 16]
[ORG 0x7C00]
start:
; Initialize segments
cli ; Disable interrupts while setting up stack
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ; Stack grows down from bootloader
sti ; Re-enable interrupts
; Print message
mov si, msg
.loop:
lodsb
or al, al ; Check for null (sets ZF if AL=0)
jz .done
mov ah, 0x0E ; Teletype function
mov bh, 0 ; Page 0
mov bl, 0x07 ; Light gray color
int 0x10
jmp .loop
.done:
hlt
jmp .done
msg: db 'Hello, Bare Metal!', 0
times 510-($-$$) db 0
dw 0xAA55
5.8 The Interview Questions They’ll Ask
Basic Understanding
- “What is the boot signature and why is it required?”
- Good Answer: 0xAA55 (stored as 0x55 0xAA in little-endian) at bytes 510-511. BIOS checks this to verify the sector is bootable. Without it, BIOS won’t execute the code.
- Red Flag: “It’s just a magic number” (no understanding of purpose)
- “Why is the bootloader loaded at 0x7C00 specifically?”
- Good Answer: Historical convention from original IBM PC. Located 32KB - 1KB into memory, leaving room below for BIOS data structures.
- Red Flag: “I don’t know, that’s just where it goes”
- “What CPU mode does the bootloader run in?”
- Good Answer: 16-bit Real Mode. No memory protection, only 1MB addressable, segment:offset addressing.
- Follow-up: “How do you switch to Protected Mode?” (Load GDT, set PE bit in CR0)
Technical Details
- “Explain segment:offset addressing. What’s the physical address of 0x07C0:0x0010?”
- Good Answer: Physical = (Segment × 16) + Offset = (0x07C0 × 16) + 0x0010 = 0x7C10
- Insight: Multiple segment:offset pairs can point to same physical address
- “How do you print to the screen in a bootloader?”
- Good Answer: BIOS INT 10h, AH=0Eh (teletype output). Or write directly to video memory at 0xB8000.
- Red Flag: “Use printf” (shows lack of bare-metal understanding)
- “What happens if you forget to set the Data Segment (DS) register?”
- Good Answer: DS might contain garbage, so data reads will access wrong memory, causing incorrect behavior or crashes.
- Red Flag: “It will still work” (it won’t)
- “Why do you need
ORG 0x7C00directive?”- Good Answer: Tells assembler where code will run, so it calculates label addresses correctly. Without it, addresses are wrong.
- Alternative: Set segment registers to 0x07C0 and use ORG 0.
Problem-Solving
- “Your bootloader compiles but nothing appears on screen. Debugging steps?”
- Good Answer:
- Verify 512-byte size and 0xAA55 signature with hexdump
- Add infinite loop at start to verify execution
- Test INT 10h with single character
- Check DS register initialization
- Use QEMU monitor to inspect registers
- Good Answer:
- “How would you load a second stage bootloader from disk?”
- Good Answer: Use BIOS INT 13h (disk services). AH=02h reads sectors. Specify cylinder, head, sector, and destination buffer.
- “What’s the maximum size of a single-stage bootloader?”
- Good Answer: 446 bytes of code (with partition table), or 510 bytes (without). Must reserve 2 bytes for 0xAA55.
5.9 Books That Will Help
| Topic | Book | Specific Chapter/Section | Why It Helps |
|---|---|---|---|
| x86 Assembly Basics | “Low-Level Programming” by Igor Zhirkov | Chapter 2: Assembly Language | Covers registers, instructions, NASM syntax |
| Real Mode and Segmentation | “PC Assembly Language” by Paul A. Carter | Chapter 1 (Sections 1.3-1.4) | Explains segment:offset with examples. Free online. |
| BIOS Interrupts | “The Art of Assembly Language” by Randall Hyde | Chapter 12: Interrupts | How software interrupts work |
| Boot Process | “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron | Chapter 7, Section 7.9 | How code gets loaded and executed |
| MBR and Disk Layout | “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau | Chapter 40 | Disk sectors and boot sectors |
| Practical Tutorial | “Writing a Simple Operating System from Scratch” by Nick Blundell | Pages 1-20 | Step-by-step bootloader with diagrams |
Online Resources:
- OSDev Wiki - “Boot Sequence”, “Real Mode”, “Memory Map”
- Ralf Brown’s Interrupt List - Complete BIOS reference
- NASM Tutorial - Beginner-friendly
5.10 Implementation Phases
Phase 1: Minimal Boot (2 hours)
- Create boot.asm with just signature
- Verify QEMU boots without “No bootable device” error
- Milestone: QEMU window opens
Phase 2: Code Execution (2 hours)
- Add infinite loop at 0x7C00
- Verify QEMU hangs (not reboots)
- Milestone: Code is executing
Phase 3: Single Character (2 hours)
- Add INT 10h to print ‘A’
- Milestone: ‘A’ appears on screen
Phase 4: String Printing (2 hours)
- Add message and print loop
- Initialize segment registers
- Milestone: “Hello, Bare Metal!” appears
Phase 5: Polish (2 hours)
- Add clean halt loop
- Test on multiple emulators
- Document and comment code
5.11 Key Implementation Decisions
-
ORG 0x7C00 vs Segment Setup: Using ORG 0x7C00 with DS=0 is simpler than ORG 0 with DS=0x07C0. Choose ORG 0x7C00.
-
Stack Location: Stack at 0x7C00 growing down is safe (30KB of space). Alternative: 0x9FC00 (just below EBDA).
-
Interrupt State: CLI before stack setup prevents interrupts during vulnerable period. STI after is optional for this project.
-
Color Attribute: BL=0x07 (light gray on black) is safe default. BL=0x0F (white on black) is brighter.
-
Halt vs Loop:
HLTin a loop is power-efficient.JMP $works but wastes CPU cycles.
6. Testing Strategy
6.1 Unit Testing
Since bootloaders can’t use traditional unit tests, use incremental verification:
; Test 1: Signature only
times 510 db 0
dw 0xAA55
; Expected: QEMU opens window
; Test 2: Infinite loop
start: jmp start
times 510-($-$$) db 0
dw 0xAA55
; Expected: QEMU hangs (code executing)
; Test 3: Single char
mov ah, 0x0E
mov al, 'X'
int 0x10
jmp $
; Expected: 'X' on screen
6.2 Integration Testing
# Test 1: File size
ls -l boot.bin | awk '{print $5}'
# Expected: 512
# Test 2: Signature check
xxd boot.bin | tail -n 1 | grep "55 aa"
# Expected: Match found
# Test 3: QEMU boot
qemu-system-x86_64 -drive format=raw,file=boot.bin 2>&1 | grep -i error
# Expected: No errors
# Test 4: Alternative emulators
qemu-system-i386 -drive format=raw,file=boot.bin
bochs -q -f bochsrc.txt
6.3 Debugging Techniques
QEMU Monitor Commands:
qemu-system-x86_64 -drive format=raw,file=boot.bin -monitor stdio
(qemu) info registers # Show CPU state
(qemu) x/20i 0x7C00 # Disassemble 20 instructions
(qemu) x/512xb 0x7C00 # Hex dump bootloader
(qemu) xp/2xb 0x7DFE # Check signature location
GDB with QEMU:
# Terminal 1
qemu-system-x86_64 -s -S -drive format=raw,file=boot.bin
# Terminal 2
gdb
(gdb) target remote :1234
(gdb) break *0x7C00
(gdb) continue
(gdb) info registers
(gdb) x/20i $eip
Bochs Debugging:
bochs -q
<bochs:1> break 0x7C00
<bochs:2> c
<bochs:3> r # Show registers
<bochs:4> u 0x7C00 # Disassemble
7. Common Pitfalls & Debugging
Problem 1: Black screen - nothing happens, QEMU hangs
- Root Cause: Code executing but not producing output
- Fix: Add infinite loop as last instruction
- Quick Test:
mov byte [0xB8000], 'X'should show X in corner
Problem 2: QEMU says ‘No bootable device’
- Root Cause: Missing or incorrect boot signature
- Fix:
hexdump -C boot.bin | tail -n 1must show55 aa - Quick Test:
ls -l boot.binmust show 512 bytes
Problem 3: Garbage characters instead of message
- Root Cause: DS not initialized
- Fix: Add
xor ax, ax; mov ds, axat start - Quick Test: Print single character first
Problem 4: Only first character prints
- Root Cause: Loop logic broken or string not null-terminated
- Fix: Ensure
db 'message', 0(trailing zero) - Quick Test: Check with
hexdumpthat string ends with 00
Problem 5: System reboots immediately
- Root Cause: No halt loop, CPU executes garbage
- Fix: End with
hlt; jmp $-1 - Quick Test: Add HLT at every exit point
Problem 6: NASM error ‘symbol undefined’
- Root Cause: Typo in label name
- Fix: Generate listing file:
nasm -l boot.lst boot.asm - Quick Test: Check case sensitivity of labels
Problem 7: File not exactly 512 bytes
- Root Cause: Wrong padding calculation
- Fix: Use
times 510-($-$$) db 0thendw 0xAA55 - Quick Test: Add/remove code, recheck size
Problem 8: Works in QEMU but not real hardware
- Root Cause: Real hardware more strict about timing/initialization
- Fix: Test with Bochs first, then VirtualBox
- Quick Test: Ensure Legacy/CSM mode in BIOS
8. Extensions & Challenges
Extension 1: Colored Output
- Use BL register to set foreground/background colors
- Colors: 0=black, 1=blue, 2=green, 4=red, 7=white, 14=yellow
- Challenge: Print “Hello” in different colors
Extension 2: Clear Screen First
- Use INT 10h, AH=00h to set video mode (clears screen)
- Or INT 10h, AH=06h to scroll window
Extension 3: Print at Specific Position
- Use INT 10h, AH=02h to set cursor position
- Center the message on screen
Extension 4: Keyboard Input
- Use INT 16h, AH=00h to wait for keypress
- Print the pressed key
- Challenge: Simple echo loop
Extension 5: Print Numbers
- Convert binary to ASCII digits
- Display memory address 0x7C00 as “7C00”
- Challenge: Print in decimal instead of hex
Extension 6: Blinking Cursor
- Write a character, wait, write space, wait
- Challenge: Implement using PIT (timer) instead of busy loop
9. Real-World Connections
GRUB Stage 1: Your bootloader is essentially GRUB’s boot.img - the minimal code that fits in the MBR and loads the rest.
MS-DOS Boot Sector: The original DOS boot sector did exactly this - load IO.SYS from disk and execute it.
Windows Boot Manager: bootmgr is loaded by similar code in the Windows MBR (or EFI loader for UEFI systems).
Linux: The kernel’s arch/x86/boot/header.S contains similar real-mode setup code.
Secure Boot: Modern systems add cryptographic verification at this stage - the UEFI firmware validates signatures before executing boot code.
10. Resources
Primary References
- Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 1
- NASM Manual (https://www.nasm.us/doc/)
- OSDev Wiki (https://wiki.osdev.org)
Online Tutorials
- OSDev Wiki: Babystep1 (https://wiki.osdev.org/Babystep1)
- Writing a Simple Operating System by Nick Blundell (free PDF)
- Bran’s Kernel Development Tutorial
Source Code Examples
- https://github.com/cfenollosa/os-tutorial
- https://github.com/mame82/LockBootloader
- GRUB source code: boot.S
11. Self-Assessment Checklist
Before moving to Project 2, verify:
- Can you explain why the bootloader loads at 0x7C00?
- Can you calculate a physical address from segment:offset?
- Can you describe what INT 10h, AH=0Eh does?
- Can you explain why segment registers must be initialized?
- Can you write the bootloader without looking at hints?
- Can you debug a non-printing bootloader systematically?
- Can you explain the MBR structure to a colleague?
- Can you modify the message and rebuild successfully?
- Have you tested on at least two emulators (QEMU + Bochs/VirtualBox)?
12. Submission / Completion Criteria
Your implementation is complete when:
boot.binis exactly 512 byteshexdump -C boot.bin | tail -n 1shows55 aaat the end- QEMU displays “Hello, Bare Metal!” and cursor blinks
- System halts cleanly (doesn’t reboot)
- Works with
qemu-system-x86_64 -drive format=raw,file=boot.bin - Works with
qemu-system-i386 -drive format=raw,file=boot.bin - Code is commented explaining each section
- You can rebuild from scratch without referencing hints
- You can explain the code to someone else
Congratulations! You’ve written the very first code that runs when a computer boots. This is the foundation of all systems programming. Proceed to Project 2 to learn about memory detection.