Project 1: Bare Metal “Hello World” (No OS)
Build a 512-byte boot sector that prints text in real mode with no OS, no libc, and no runtime.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Beginner (low-level detail heavy) |
| Time Estimate | 6-10 hours |
| Main Programming Language | x86 Assembly (NASM) |
| Alternative Programming Languages | GAS syntax, or C for stage-2 (not for boot sector) |
| Coolness Level | Very High: you boot raw metal |
| Business Potential | Low, but foundational |
| Prerequisites | Basic x86 assembly, hex/bitwise reasoning |
| Key Topics | BIOS boot, real mode, VGA text memory, boot signatures |
1. Learning Objectives
By completing this project, you will:
- Explain how BIOS loads a boot sector and why it must be 512 bytes.
- Write a correct real-mode program that runs at 0x7C00.
- Output text using both BIOS interrupts and direct VGA memory.
- Demonstrate how CPU mode constraints shape your code and data layout.
2. All Theory Needed (Per-Concept Breakdown)
Boot Sector Execution in x86 Real Mode
Fundamentals
When an x86 CPU resets, it does not start in the rich 64-bit environment you use every day. It starts in 16-bit real mode and hands control to firmware (BIOS or UEFI CSM). The BIOS reads exactly one 512-byte sector from the boot device into physical address 0x7C00 and then jumps to that address. That is the boot sector. It must end in the signature 0x55AA or the BIOS will refuse to boot it. Real mode uses segmented addressing, limited memory, and 16-bit instructions, so every byte matters. You must place code, data, and the signature in a tiny fixed-size payload. You also must assume that no runtime exists: there is no stack unless you set one, no interrupts unless you handle them, and no libraries. This is the smallest possible executable format on x86.
Deep Dive into the concept
A boot sector is both a file format and a hardware contract. The BIOS reads LBA 0 (the first sector of the disk) into 0x7C00, sets CS:IP to 0x0000:0x7C00 (or equivalent), and starts executing. The CPU is in real mode, which means memory addresses are computed as segment * 16 + offset, and the default operand size is 16 bits. There is no paging, no privilege separation, and no memory protection. That simplifies the environment but constrains your program. If you accidentally assemble 32-bit instructions, the CPU will interpret them differently and can jump into nowhere.
The 512-byte size is not arbitrary. It matches the legacy disk sector size, and the BIOS boot contract is hard-coded for that size. The last two bytes must be 0x55AA. That signature is how the BIOS distinguishes a bootable sector from random data. This means your actual code and data can only occupy 510 bytes. If you include strings, tables, or padding, you must do it intentionally. In practice, you reserve space for the signature by padding with zeros and then writing the two-byte signature.
You also must consider how the BIOS leaves the machine. The initial register values are not fully specified. CS:IP points to your code, but DS, ES, and SS might not be what you expect. A robust boot sector explicitly sets DS and ES to zero or to match CS and sets up a small stack below 0x7C00 (often at 0x7C00 with a downward-growing stack). You also need to decide how to produce output. BIOS provides an interrupt (INT 0x10, AH=0x0E) that prints characters to the screen in text mode, which is simple but slow. Alternatively, you can write directly to VGA text memory at 0xB8000, which is faster and gives you full control over colors and cursor position. Direct VGA writes are also a first exposure to memory-mapped I/O, where hardware devices appear as simple memory addresses.
Real mode also has interrupts enabled by default. If you do not handle them, the BIOS handlers are still present, so you can use BIOS services. But if you disable interrupts or overwrite the IVT (interrupt vector table), you can easily hang or reboot. The cleanest approach for this project is to keep interrupts enabled and use BIOS services for printing. After printing, you should halt the CPU with HLT in an infinite loop. This prevents the BIOS from falling through into random memory and rebooting.
The boot sector is a minimal but complete example of systems programming: you manage the CPU state, the memory layout, and the I/O model explicitly. The constraints force you to understand alignment, instruction sizes, and the boundary between firmware and your code. Those same constraints show up again in real kernels, just at a larger scale. When you later build a multi-stage bootloader, you will keep the same invariants, but you will add disk reads, mode switches, and kernel loading. This project is the atomic unit of that larger pipeline.
How this fit on projects
You will use this knowledge directly in Section 3.1 (building the boot image), Section 3.7 (demo), and Section 5.10 Phase 1. It also connects to Project 2 and Project 14, which expand the boot flow.
Definitions & key terms
- Boot sector: The first 512 bytes of a bootable disk, loaded by BIOS at 0x7C00.
- 0x55AA signature: The magic bytes at the end of the boot sector that signal bootable code.
- Real mode: 16-bit execution mode with segmented addressing and 1 MB limit.
- INT 0x10: BIOS video service interrupt used for text output.
- VGA text buffer: Memory-mapped screen buffer at 0xB8000 in text mode.
Mental model diagram (ASCII)
Disk LBA 0 (512 bytes)
+-----------------------------+
| boot code (<=510 bytes) |
| ... |
| 0x55 0xAA signature |
+-----------------------------+
|
v
BIOS loads to 0x7C00 and jumps
How it works (step-by-step)
- BIOS reads sector 0 into memory at 0x7C00.
- BIOS checks bytes 510-511 for 0x55AA.
- BIOS sets CS:IP to the boot code entry point.
- Boot sector sets DS/ES/SS and initializes a stack.
- Boot sector writes characters (INT 0x10 or 0xB8000).
- Boot sector halts in a loop to avoid fall-through.
Minimal concrete example
; nasm -f bin boot.asm -o boot.bin
org 0x7C00
bits 16
start:
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
mov si, msg
.print:
lodsb
test al, al
jz .done
mov ah, 0x0E
int 0x10
jmp .print
.done:
hlt
jmp .done
msg db "Hello, OS!", 0
times 510-($-$$) db 0
dw 0xAA55
Common misconceptions
- “I can use 32-bit instructions.” In real mode, those opcodes decode differently and can crash.
- “The BIOS sets up my stack.” It does not. You must set SS:SP yourself.
- “Any 512-byte file is bootable.” The signature must be present and correct.
Check-your-understanding questions
- Why is the boot sector exactly 512 bytes?
- What does the BIOS check before jumping to your code?
- Why must you set DS and SS explicitly?
- What is the difference between BIOS teletype output and VGA memory writes?
Check-your-understanding answers
- The BIOS reads a single 512-byte sector; that is the fixed boot contract.
- It checks for the 0x55AA signature at bytes 510-511.
- Register values are not guaranteed, and incorrect segments break memory references.
- BIOS teletype uses firmware services; VGA writes directly target the screen buffer.
Real-world applications
- BIOS boot paths for legacy devices and recovery media.
- Bootable firmware diagnostics and manufacturing test fixtures.
- Tiny bootloaders for embedded x86 systems.
Where you’ll apply it
- This project: Section 3.1, Section 3.7, and Section 5.10 Phase 1.
- Also used in: Project 2: Bootloader that Loads a Kernel, Project 14: xv6 Operating System Study.
References
- “Operating Systems: Three Easy Pieces” (Boot/CPU intro)
- “Write Great Code, Vol. 2” (x86 real mode and segmentation)
- Intel SDM Vol. 3A (Processor initialization)
Key insights
A boot sector is not a program in a file system; it is a hardware protocol with strict size and state constraints.
Summary
You only get 510 bytes, real mode, and uncertain registers. If you can still print a message reliably, you have understood the firmware-to-CPU contract.
Homework/Exercises to practice the concept
- Modify the code to print two lines using BIOS teletype.
- Implement direct VGA writes with color attributes.
- Add a tiny hex print routine for debugging register values.
Solutions to the homework/exercises
- Add
\r\nafter the first line or update cursor via BIOS. - Write 2-byte pairs (char + attribute) to 0xB8000.
- Convert a nibble to ASCII by adding ‘0’ or ‘A’-10.
3. Project Specification
3.1 What You Will Build
You will build a 512-byte boot sector that runs in x86 real mode, prints a message, and halts cleanly. It will be a raw binary image that you can boot directly in QEMU. It does not load a kernel, does not access disk beyond the first sector, and does not rely on any OS services beyond BIOS interrupts.
3.2 Functional Requirements
- Bootability: The image must contain the 0x55AA signature and boot in QEMU.
- Output: The image must print a visible message in VGA text mode.
- Stability: After printing, the CPU must halt in a loop without rebooting.
- Size: The final binary must be exactly 512 bytes.
3.3 Non-Functional Requirements
- Performance: Output should appear instantly (<1 second) in QEMU.
- Reliability: Must boot consistently across multiple QEMU runs.
- Usability: Build is a single command (nasm) with clear output.
3.4 Example Usage / Output
$ nasm -f bin boot.asm -o boot.bin
$ stat -c "%s" boot.bin
512
$ qemu-system-x86_64 -drive format=raw,file=boot.bin
[QEMU]
Hello, OS!
3.5 Data Formats / Schemas / Protocols
- Boot sector: 512 bytes total, last two bytes are 0x55AA (little-endian).
3.6 Edge Cases
- Incorrect signature (should not boot).
- Image size not 512 bytes (BIOS refuses to boot).
- Using 32-bit instructions (hang or reboot).
3.7 Real World Outcome
3.7.1 How to Run (Copy/Paste)
nasm -f bin boot.asm -o boot.bin
qemu-system-x86_64 -drive format=raw,file=boot.bin
3.7.2 Golden Path Demo (Deterministic)
- Use the provided source exactly; no randomness is involved.
- The output should be the single line
Hello, OS!followed by a blinking cursor.
3.7.3 If CLI: exact terminal transcript
$ nasm -f bin boot.asm -o boot.bin
$ stat -c "%s" boot.bin
512
$ qemu-system-x86_64 -drive format=raw,file=boot.bin
[QEMU window]
Hello, OS!
_
Failure demo (deterministic):
$ printf '\0' | dd of=boot.bin bs=512 count=1
$ qemu-system-x86_64 -drive format=raw,file=boot.bin
[QEMU window]
Boot failed: not a bootable disk
Exit codes:
0success2build error (assembler)3runtime boot failure (QEMU non-zero)
4. Solution Architecture
4.1 High-Level Design
BIOS -> 0x7C00 boot sector -> print -> halt
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Boot entry | Initialize segments and stack | Use DS=ES=SS=0 for simplicity |
| Output routine | Print characters | BIOS INT 0x10 for clarity |
| Halt loop | Stop execution | HLT + JMP |
4.3 Data Structures (No Full Code)
; No complex structures; only a null-terminated string.
msg db "Hello, OS!", 0
4.4 Algorithm Overview
Key Algorithm: BIOS teletype loop
- Load next character from string.
- If zero, stop.
- Call INT 0x10 to print.
- Repeat.
Complexity Analysis:
- Time: O(n) for n characters
- Space: O(1) additional
5. Implementation Guide
5.1 Development Environment Setup
sudo apt-get install nasm qemu-system-x86
5.2 Project Structure
project-root/
|-- boot.asm
`-- Makefile
5.3 The Core Question You’re Answering
“What is the smallest possible program that a CPU will execute after reset, and why does it require exact layout and signatures?”
5.4 Concepts You Must Understand First
- BIOS boot contract and 0x55AA signature.
- Real-mode addressing and segment:offset math.
- Memory-mapped VGA text buffer.
5.5 Questions to Guide Your Design
- Will you rely on BIOS output or direct VGA writes?
- Where will your stack live relative to 0x7C00?
- How will you keep your code within 510 bytes?
5.6 Thinking Exercise
Compute the address for VGA cell (row 3, col 10). Verify the byte offset and two-byte character layout.
5.7 The Interview Questions They’ll Ask
- Why must the boot sector end with 0x55AA?
- Why does real mode limit memory to 1 MB?
- How does the BIOS know where to load your code?
5.8 Hints in Layers
Hint 1: Print a single character with INT 0x10.
Hint 2: Add a null-terminated string and loop.
Hint 3: Add a halt loop using HLT and JMP.
5.9 Books That Will Help
| Topic | Book | Chapter | |——-|——|———| | x86 boot basics | Write Great Code, Vol. 2 | 3 | | Real mode execution | Intel SDM | Vol. 3A | | Memory-mapped I/O | CS:APP | 6 |
5.10 Implementation Phases
Phase 1: Bootable sector (2 hours)
Goals: assemble 512-byte image, boot in QEMU. Tasks: write minimal loop, add signature, test size. Checkpoint: QEMU loads without reboot.
Phase 2: Output (2-3 hours)
Goals: print string using BIOS teletype. Tasks: implement loop, test output. Checkpoint: message appears exactly once.
Phase 3: Polish (1-2 hours)
Goals: clean halt, optional VGA direct write. Tasks: add HLT loop, optional color text. Checkpoint: stable output and no reboot.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Output method | BIOS INT 0x10 vs VGA memory | BIOS INT 0x10 | Simpler and more portable | | Stack location | 0x7C00 vs 0x7000 | 0x7C00 | Minimal setup for tiny program |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|———-|———|———-|
| Boot tests | Ensure BIOS loads image | QEMU boot on fresh image |
| Output tests | Verify correct message | Visual check + capture screenshot |
| Size tests | Validate 512 bytes | stat or ls -l |
6.2 Critical Test Cases
- Correct signature: Verify last two bytes are 0x55AA.
- Exact size: Image size is 512 bytes.
- No reboot: QEMU keeps running after output.
6.3 Test Data
Expected output: "Hello, OS!"
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|——–|———|———-|
| Missing signature | QEMU reboots | Add 0x55AA at bytes 510-511 |
| Wrong mode instructions | Hang or reboot | Force bits 16 and avoid 32-bit ops |
| No stack setup | Random crashes | Initialize SS:SP |
7.2 Debugging Strategies
- Use QEMU
-d intto trace BIOS and interrupts. - Add a tiny hex printer to observe register values.
7.3 Performance Traps
Not applicable; size and correctness matter more than speed.
8. Extensions & Challenges
8.1 Beginner Extensions
- Clear the screen before printing.
- Print with color attributes using VGA memory.
8.2 Intermediate Extensions
- Read a second sector and display its contents.
- Implement a tiny hex dump routine.
8.3 Advanced Extensions
- Implement a minimal stage-2 loader (project 2 prep).
- Add keyboard input via BIOS INT 0x16.
9. Real-World Connections
9.1 Industry Applications
- Boot diagnostics and recovery environments.
- Embedded x86 systems with custom firmware paths.
9.2 Related Open Source Projects
- SeaBIOS: Legacy BIOS implementation.
- coreboot: Open-source firmware with custom payloads.
9.3 Interview Relevance
- Boot process questions in systems and kernel interviews.
10. Resources
10.1 Essential Reading
- “Operating Systems: Three Easy Pieces” - Boot overview
- “Write Great Code, Vol. 2” - Real mode and segmentation
10.2 Video Resources
- “x86 Boot Process” lecture (university OS course)
10.3 Tools & Documentation
- NASM: assembler
- QEMU: virtualization and debug flags
10.4 Related Projects in This Series
- Project 2: Multi-stage loader and protected mode
- Project 14: Real kernel boot path study
11. Self-Assessment Checklist
11.1 Understanding
- I can explain why the boot sector must be 512 bytes.
- I can describe real mode and segmentation.
- I can explain why a missing signature prevents booting.
11.2 Implementation
- The image boots consistently in QEMU.
- The message is printed exactly once.
- The binary is exactly 512 bytes.
11.3 Growth
- I documented the boot flow in my notes.
- I can explain this project in an interview.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Boot sector prints a message and halts.
- Correct signature and exact size.
- Build and run steps documented.
Full Completion:
- Includes both BIOS and VGA output variants.
- Includes a small debugging routine.
Excellence (Going Above & Beyond):
- Adds keyboard input or a second-sector read.
- Cleanly explains all register setup decisions.