Emulation and QEMU

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

  1. Install QEMU - Get hands-on experience immediately
  2. Start simple - Boot a basic bootloader before complex kernels
  3. Use debugging features - Learn QEMU monitor and GDB integration
  4. 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

  1. Full System Emulation: Emulates complete computer (CPU, RAM, devices)
  2. User Mode Emulation: Runs foreign architecture binaries on host OS
  3. 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

  1. Wrong architecture binary - Using qemu-system-arm for x86 code
  2. Missing boot signature - Forgetting 0x55AA
  3. Not using -S flag - VM starts before GDB connects
  4. Wrong memory inspection - Use xp for physical, x for virtual
  5. Ignoring serial output - Easier than video for early debugging
  6. Not saving work - Use snapshots during long tests
  7. 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

  1. Boot a simple bootloader in QEMU
  2. Use QEMU monitor to examine bootloader memory
  3. Set up GDB debugging for bootloader
  4. Create Makefile that builds and runs in QEMU
  5. Use serial port to print debug messages
  6. Create a QEMU snapshot and restore it
  7. Log interrupts to file using -d int
  8. Run an ARM bootloader in QEMU
  9. Use GDB to step through protected mode transition
  10. Create multi-stage bootloader and test in QEMU

Review Questions

  1. What's the difference between emulation and virtualization?
  2. What QEMU option enables GDB debugging?
  3. How do you access the QEMU monitor?
  4. What command shows CPU registers in QEMU monitor?
  5. 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.