Chapter 13: Emulation and QEMU
Introduction
Emulation allows you to run software designed for one architecture on another, or to create virtual machines for testing. QEMU (Quick Emulator) is the most important tool for OS development and system programming. It lets you test bootloaders, kernels, and system software safely without risking hardware damage, and provides powerful debugging capabilities.
Why This Matters
System programming is dangerous. A bug in a bootloader or kernel can brick hardware, corrupt disks, or cause data loss. Emulators provide a safe sandbox for development. QEMU is essential for kernel developers, embedded systems engineers, and anyone working on low-level software. It supports multiple architectures (x86, ARM, RISC-V, etc.) and integrates with debuggers.
How to Study This Chapter
- Install QEMU - Get hands-on experience immediately
- Start simple - Boot a basic bootloader before complex kernels
- Use debugging features - Learn QEMU monitor and GDB integration
- Test on multiple architectures - Try x86, x64, and ARM emulation
What is Emulation?
Emulation vs Virtualization
Emulation: Simulates different hardware architecture
- Software simulates CPU instructions
- Can run ARM code on x86 host
- Slower (instruction translation overhead)
- Example: QEMU in emulation mode
Virtualization: Runs code on same architecture with hardware support
- Uses CPU virtualization extensions (Intel VT-x, AMD-V)
- Near-native performance
- Example: QEMU with KVM, VirtualBox, VMware
Types of Emulators
- Full System Emulation: Emulates complete computer (CPU, RAM, devices)
- User Mode Emulation: Runs foreign architecture binaries on host OS
- Hardware-Assisted Virtualization: Uses CPU extensions for speed
Installing QEMU
Linux (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install qemu-system-x86 qemu-system-arm qemu-utils
macOS
brew install qemu
Windows
Download from qemu.org or use WSL2.
Verify Installation
qemu-system-x86_64 --version
QEMU Basics
Running a Bootable Image
# Create a floppy disk image
dd if=/dev/zero of=floppy.img bs=512 count=2880 # 1.44 MB
# Write bootloader to image
dd if=boot.bin of=floppy.img bs=512 count=1 conv=notrunc
# Boot the image in QEMU
qemu-system-x86_64 -drive file=floppy.img,format=raw,if=floppy
# Alternative: Use as hard disk
qemu-system-x86_64 -drive file=disk.img,format=raw
Common QEMU Options
# Boot from floppy
qemu-system-x86_64 -fda floppy.img
# Boot from hard disk
qemu-system-x86_64 -hda disk.img
# Specify RAM size (default: 128 MB)
qemu-system-x86_64 -m 512M -hda disk.img
# Enable KVM acceleration (Linux only, same architecture)
qemu-system-x86_64 -enable-kvm -hda disk.img
# Boot from CD-ROM
qemu-system-x86_64 -cdrom os.iso
# No graphical window (serial console only)
qemu-system-x86_64 -nographic -hda disk.img
# Serial output to stdio
qemu-system-x86_64 -serial stdio -hda disk.img
Multiple Devices
# Multiple disks
qemu-system-x86_64 \
-drive file=disk1.img,format=raw \
-drive file=disk2.img,format=raw
# Network card
qemu-system-x86_64 -netdev user,id=net0 -device e1000,netdev=net0
# USB device
qemu-system-x86_64 -usb -device usb-mouse
QEMU Monitor
The QEMU monitor is a powerful interactive console for controlling the virtual machine.
Accessing the Monitor
Press Ctrl-Alt-2 to switch to monitor (Ctrl-Alt-1 returns to VM).
Or start QEMU with monitor on stdio:
qemu-system-x86_64 -monitor stdio -hda disk.img
Or use telnet:
qemu-system-x86_64 -monitor telnet:127.0.0.1:55555,server,nowait -hda disk.img
# In another terminal:
telnet 127.0.0.1 55555
Common Monitor Commands
# Get help
(qemu) help
# Show CPU registers
(qemu) info registers
# Show memory mappings
(qemu) info mem
# Show TLB contents
(qemu) info tlb
# Examine physical memory (10 hex words at address 0x7c00)
(qemu) xp /10x 0x7c00
# Examine virtual memory
(qemu) x /10x 0x7c00
# Display disassembly
(qemu) x /10i 0x7c00
# Show PCI devices
(qemu) info pci
# Show block devices
(qemu) info block
# Take screenshot
(qemu) screendump screen.ppm
# Pause VM
(qemu) stop
# Resume VM
(qemu) cont
# Reset VM
(qemu) system_reset
# Quit QEMU
(qemu) quit
# Save VM state
(qemu) savevm snapshot1
# Load VM state
(qemu) loadvm snapshot1
# Change removable media
(qemu) change floppy0 newdisk.img
Memory Inspection
# Examine memory at bootloader location
(qemu) xp /128x 0x7c00
# Check if boot signature is present
(qemu) xp /2hx 0x7dfe
# Should output: 0x7dfe: 0x55aa
# Examine GDT
(qemu) x /16x gdt_address
# Look at stack
(qemu) xp /32x $esp
Debugging with QEMU and GDB
QEMU can act as a GDB remote debugging target.
Basic GDB Debugging
Terminal 1: Start QEMU with GDB server
qemu-system-x86_64 -s -S -drive file=disk.img,format=raw
# -s : Shorthand for -gdb tcp::1234 (start GDB server on port 1234)
# -S : Freeze CPU at startup (wait for GDB)
Terminal 2: Connect GDB
gdb
(gdb) target remote localhost:1234
(gdb) set architecture i386:x86-64
(gdb) break *0x7c00 # Breakpoint at bootloader
(gdb) continue # Start execution
GDB Commands for System Programming
# Set breakpoint at address
(gdb) break *0x7c00
# Show registers
(gdb) info registers
# Show all registers (including segment registers)
(gdb) info all-registers
# Examine memory (hex)
(gdb) x/32xb 0x7c00
# Examine as instructions
(gdb) x/10i 0x7c00
# Disassemble
(gdb) disassemble 0x7c00,+64
# Step one instruction
(gdb) stepi
(gdb) si
# Step one instruction (step over calls)
(gdb) nexti
(gdb) ni
# Continue execution
(gdb) continue
(gdb) c
# Layout with assembly
(gdb) layout asm
# Layout with registers
(gdb) layout regs
# Watch memory location
(gdb) watch *0x7c00
# Set value in register
(gdb) set $eax = 0x42
# Write to memory
(gdb) set {int}0x7c00 = 0x12345678
Debugging Protected Mode Transition
# Connect GDB
(gdb) target remote :1234
# Set breakpoint before switching to protected mode
(gdb) break *0x7c00
(gdb) continue
# Step through instructions
(gdb) si
# Watch CR0 register (when bit 0 set, protected mode enabled)
(gdb) watch $cr0
# After protected mode enabled, change architecture
(gdb) set architecture i386:x86-64
(gdb) continue
Creating GDB Script
debug.gdb:
# Connect to QEMU
target remote localhost:1234
# Set architecture
set architecture i386:x86-64
# Add symbols if you have them
# symbol-file kernel.elf
# Set breakpoints
break *0x7c00
break *0x1000
# Define custom commands
define hook-stop
info registers
x/10i $pc
end
# Start execution
continue
Run with: gdb -x debug.gdb
QEMU Disk Images
Creating Disk Images
# Create raw disk image (1 GB)
qemu-img create -f raw disk.img 1G
# Create qcow2 image (more efficient, supports snapshots)
qemu-img create -f qcow2 disk.qcow2 10G
# Convert raw to qcow2
qemu-img convert -f raw -O qcow2 disk.img disk.qcow2
# Get info about image
qemu-img info disk.qcow2
Mounting Disk Images (Linux)
# Mount raw image with loopback
sudo losetup /dev/loop0 disk.img
sudo mount /dev/loop0 /mnt
# Or use offset for partitions
sudo mount -o loop,offset=1048576 disk.img /mnt
# Unmount
sudo umount /mnt
sudo losetup -d /dev/loop0
Partitioning Disk Images
# Create partition table
fdisk disk.img
# Or use parted
parted disk.img mklabel msdos
parted disk.img mkpart primary ext4 1MiB 100%
Creating a Complete Development Environment
Makefile for Building and Testing
# Makefile for bootloader and kernel development
ASM = nasm
CC = gcc
LD = ld
BOOT_SRC = boot.asm
KERNEL_SRC = kernel.c
BOOT_BIN = boot.bin
KERNEL_BIN = kernel.bin
DISK_IMG = os.img
all: $(DISK_IMG)
# Assemble bootloader
$(BOOT_BIN): $(BOOT_SRC)
$(ASM) -f bin -o $@ $<
# Compile kernel
kernel.o: $(KERNEL_SRC)
$(CC) -m32 -ffreestanding -fno-pie -c -o $@ $<
# Link kernel
$(KERNEL_BIN): kernel.o
$(LD) -m elf_i386 -T linker.ld -o $@ $<
objcopy -O binary $@ $@
# Create disk image
$(DISK_IMG): $(BOOT_BIN) $(KERNEL_BIN)
dd if=/dev/zero of=$@ bs=512 count=2880
dd if=$(BOOT_BIN) of=$@ bs=512 count=1 conv=notrunc
dd if=$(KERNEL_BIN) of=$@ bs=512 seek=1 conv=notrunc
# Run in QEMU
run: $(DISK_IMG)
qemu-system-x86_64 -drive file=$(DISK_IMG),format=raw
# Debug with GDB
debug: $(DISK_IMG)
qemu-system-x86_64 -s -S -drive file=$(DISK_IMG),format=raw &
gdb -ex "target remote :1234" \
-ex "break *0x7c00" \
-ex "continue"
# Clean build artifacts
clean:
rm -f *.o *.bin $(DISK_IMG)
.PHONY: all run debug clean
Usage:
make # Build disk image
make run # Build and run in QEMU
make debug # Build and debug with GDB
make clean # Clean build files
ARM Emulation
QEMU supports various ARM machines.
Available ARM Machines
# List available ARM machines
qemu-system-arm -machine help
# Common machines:
# - versatilepb: ARM Versatile Platform Baseboard
# - vexpress-a9: ARM Versatile Express Cortex-A9
# - raspi2: Raspberry Pi 2
# - raspi3: Raspberry Pi 3
Running ARM Code
# Compile for ARM
arm-none-eabi-gcc -mcpu=arm926ej-s -c -o boot.o boot.s
arm-none-eabi-ld -T linker.ld -o kernel.elf boot.o
arm-none-eabi-objcopy -O binary kernel.elf kernel.bin
# Run in QEMU
qemu-system-arm -M versatilepb -m 128M -kernel kernel.bin -serial stdio
# Debug with GDB
qemu-system-arm -M versatilepb -kernel kernel.bin -s -S
arm-none-eabi-gdb kernel.elf
(gdb) target remote :1234
Simple ARM Boot Code
/* boot.s - ARM bootloader */
.section .text
.global _start
_start:
/* Set up stack */
ldr sp, =stack_top
/* Call main function */
bl main
/* Halt */
hang:
b hang
.section .bss
.align 4
stack_bottom:
.space 4096
stack_top:
Advanced QEMU Features
Serial Port Communication
In your bootloader/kernel:
// Write to COM1 serial port (x86)
void serial_putchar(char c) {
while (!(inb(0x3F8 + 5) & 0x20)); // Wait for transmit buffer empty
outb(0x3F8, c); // Write character
}
void serial_puts(const char *str) {
while (*str) {
serial_putchar(*str++);
}
}
Run QEMU with serial output:
qemu-system-x86_64 -serial stdio -hda disk.img
Logging
# Enable debug logging
qemu-system-x86_64 -d int,cpu_reset -hda disk.img
# Log to file
qemu-system-x86_64 -d int -D qemu.log -hda disk.img
# Available debug categories (see qemu-system-x86_64 -d help):
# - int: Interrupts
# - cpu_reset: CPU resets
# - guest_errors: Guest errors
# - mmu: MMU operations
# - pcall: x86 only: protected mode far calls
# - unimp: Unimplemented functionality
Snapshots
# Create snapshot
(qemu) savevm snapshot1
# List snapshots
(qemu) info snapshots
# Load snapshot
(qemu) loadvm snapshot1
# Delete snapshot
(qemu) delvm snapshot1
Networking
# User mode networking (no root required)
qemu-system-x86_64 -netdev user,id=net0 -device e1000,netdev=net0
# Tap networking (requires root)
sudo qemu-system-x86_64 -netdev tap,id=net0 -device e1000,netdev=net0
# Dump network traffic
qemu-system-x86_64 -netdev user,id=net0 -device e1000,netdev=net0 \
-object filter-dump,id=f1,netdev=net0,file=network.pcap
Troubleshooting
Common Issues
Problem: Bootloader doesn't run
# Check boot signature
hexdump -C boot.bin | tail
# Should see: ... 55 aa
# Verify size
ls -l boot.bin
# Should be 512 bytes
Problem: Kernel not loaded
# Check disk image
qemu-system-x86_64 -monitor stdio -hda disk.img
(qemu) xp /512x 0x1000
# Should see kernel code
Problem: GDB won't connect
# Ensure QEMU is listening
netstat -an | grep 1234
# Try explicit port in GDB
(gdb) target remote localhost:1234
Problem: Wrong architecture
# Verify image architecture
file boot.bin
# Should show: DOS/MBR boot sector
# Use correct QEMU binary
qemu-system-x86_64 # For x64
qemu-system-arm # For ARM
Testing Checklist
# 1. Test bootloader loads
qemu-system-x86_64 -drive file=disk.img,format=raw
# 2. Test serial output
qemu-system-x86_64 -serial stdio -drive file=disk.img,format=raw
# 3. Inspect bootloader in memory
qemu-system-x86_64 -monitor stdio -drive file=disk.img,format=raw
(qemu) xp /128x 0x7c00
# 4. Debug with GDB
qemu-system-x86_64 -s -S -drive file=disk.img,format=raw &
gdb -ex "target remote :1234"
# 5. Check registers after boot
(qemu) info registers
# 6. Verify kernel loaded
(qemu) xp /256x 0x1000
Key Concepts
- QEMU emulates complete computer systems
- Emulation simulates foreign architectures, slower than virtualization
- QEMU Monitor provides introspection and control
- GDB integration enables source-level debugging
- Serial output simplifies debugging (no video driver needed)
- Disk images can be raw or qcow2 format
- Multiple architectures supported (x86, ARM, RISC-V, etc.)
- Snapshots save and restore VM state
Common Mistakes
- Wrong architecture binary - Using qemu-system-arm for x86 code
- Missing boot signature - Forgetting 0x55AA
- Not using -S flag - VM starts before GDB connects
- Wrong memory inspection - Use
xpfor physical,xfor virtual - Ignoring serial output - Easier than video for early debugging
- Not saving work - Use snapshots during long tests
- Testing on hardware first - Always use QEMU first!
Debugging Tips
- Start with serial output - Easier than video
- Use QEMU monitor - Inspect memory and registers
- Enable debug logging -
-d int,cpu_reset - Use GDB breakpoints - Stop at specific addresses
- Check boot signature - Most common bootloader error
- Verify disk reads - Use monitor to check loaded data
- Test incrementally - Small changes, frequent testing
- Read QEMU docs - Extensive documentation available
Mini Exercises
- Boot a simple bootloader in QEMU
- Use QEMU monitor to examine bootloader memory
- Set up GDB debugging for bootloader
- Create Makefile that builds and runs in QEMU
- Use serial port to print debug messages
- Create a QEMU snapshot and restore it
- Log interrupts to file using
-d int - Run an ARM bootloader in QEMU
- Use GDB to step through protected mode transition
- Create multi-stage bootloader and test in QEMU
Review Questions
- What's the difference between emulation and virtualization?
- What QEMU option enables GDB debugging?
- How do you access the QEMU monitor?
- What command shows CPU registers in QEMU monitor?
- How do you enable serial output in QEMU?
Reference Checklist
By the end of this chapter, you should be able to:
- Install and run QEMU
- Boot disk images in QEMU
- Use QEMU monitor to inspect memory and registers
- Debug bootloaders and kernels with GDB
- Create and manage disk images
- Use serial port for debugging output
- Test code on multiple architectures
- Create automated build and test workflows
- Enable debug logging
- Use snapshots for testing
Next Steps
With QEMU as your development environment, you're ready to build a kernel. The next chapter covers kernel fundamentals: what a kernel does, kernel architecture, interrupt handling, and the basics of kernel development.
Key Takeaway: QEMU is an indispensable tool for system programming. It provides a safe, debuggable environment for developing and testing bootloaders, kernels, and low-level software across multiple architectures.