Skip to content

Week 4 — QEMU Deep Dive

Goal

Run your custom kernel in QEMU with full networking. Learn to iterate fast: edit code → build → boot → test in under a minute. By the end of this week, QEMU will be your primary development environment.

Why This Matters

You never test kernel changes on your real system first. QEMU gives you an isolated environment where a kernel panic just means closing a window. For networking work, QEMU also provides virtual network devices that behave like real hardware, so you can test driver and protocol changes without physical NICs.


Install QEMU

sudo apt install -y qemu-system-x86 qemu-utils

Verify:

qemu-system-x86_64 --version

Create a Root Filesystem Disk Image

Your kernel needs a root filesystem to boot into. We'll create a Debian-based disk image using debootstrap:

sudo apt install -y debootstrap

# Create a 4 GB sparse disk image
qemu-img create -f qcow2 ~/qemu-debian.qcow2 4G

# Attach it as a block device
sudo modprobe nbd max_part=8
sudo qemu-nbd --connect=/dev/nbd0 ~/qemu-debian.qcow2

# Partition and format
sudo parted /dev/nbd0 --script mklabel msdos mkpart primary ext4 1MiB 100%
sudo mkfs.ext4 /dev/nbd0p1

# Mount and install Debian
sudo mkdir -p /mnt/qemu-rootfs
sudo mount /dev/nbd0p1 /mnt/qemu-rootfs
sudo debootstrap --variant=minbase trixie /mnt/qemu-rootfs http://deb.debian.org/debian

Configure the rootfs:

# Set up basic networking, fstab, hostname
sudo chroot /mnt/qemu-rootfs bash -c '
  echo "qemu-dev" > /etc/hostname
  echo "127.0.0.1 localhost qemu-dev" > /etc/hosts
  echo "/dev/sda1 / ext4 errors=remount-ro 0 1" > /etc/fstab

  # Set root password
  echo "root:root" | chpasswd

  # Enable serial console for QEMU
  systemctl enable serial-getty@ttyS0.service 2>/dev/null || true

  # Install minimal networking tools
  apt update
  apt install -y iproute2 iputils-ping net-tools kmod procps
'

Clean up:

sudo umount /mnt/qemu-rootfs
sudo qemu-nbd --disconnect /dev/nbd0

Boot Your Custom Kernel

First, install your kernel modules into the rootfs:

cd ~/linux

# Install modules to a temporary directory
make modules_install INSTALL_MOD_PATH=/tmp/kernel-modules

# Copy them into the disk image
sudo qemu-nbd --connect=/dev/nbd0 ~/qemu-debian.qcow2
sudo mount /dev/nbd0p1 /mnt/qemu-rootfs
sudo cp -r /tmp/kernel-modules/lib/modules/* /mnt/qemu-rootfs/lib/modules/
sudo umount /mnt/qemu-rootfs
sudo qemu-nbd --disconnect /dev/nbd0

Now boot:

qemu-system-x86_64 \
  -kernel ~/linux/arch/x86/boot/bzImage \
  -drive file=~/qemu-debian.qcow2,format=qcow2 \
  -append "root=/dev/sda1 rw console=ttyS0" \
  -nographic \
  -m 2G \
  -smp 4 \
  -enable-kvm

Flags explained:

  • -kernel — boot this kernel directly, bypassing GRUB
  • -drive — attach the disk image
  • -append — kernel command line (console=ttyS0 redirects output to serial)
  • -nographic — no graphical window, everything on your terminal (essential for SSH)
  • -m 2G — 2 GB of RAM for the guest
  • -smp 4 — 4 virtual CPUs
  • -enable-kvm — use hardware virtualization (much faster)

Expect: You'll see kernel boot messages scroll by, then a login prompt. Login with root / root.

To exit QEMU: Press Ctrl+A then X.

Create a Boot Script

You'll run this hundreds of times. Make a script:

cat > ~/boot-kernel.sh << 'EOF'
#!/bin/bash
KERNEL=~/linux/arch/x86/boot/bzImage
ROOTFS=~/qemu-debian.qcow2
APPEND="root=/dev/sda1 rw console=ttyS0 nokaslr"

qemu-system-x86_64 \
  -kernel "$KERNEL" \
  -drive file="$ROOTFS",format=qcow2 \
  -append "$APPEND" \
  -nographic \
  -m 2G \
  -smp 4 \
  -enable-kvm \
  -netdev user,id=net0,hostfwd=tcp::2222-:22 \
  -device virtio-net-pci,netdev=net0 \
  "$@"
EOF
chmod +x ~/boot-kernel.sh

New flags:

  • nokaslr — disables kernel address space layout randomization (needed for GDB)
  • -netdev user,id=net0,hostfwd=tcp::2222-:22 — user-mode networking, forwards port 2222 on host to port 22 on guest (SSH)
  • -device virtio-net-pci,netdev=net0 — a virtio network card

QEMU Networking Modes

QEMU has several networking backends. Understand the tradeoffs:

User mode (SLIRP) — simplest, no root needed

-netdev user,id=n0 -device virtio-net-pci,netdev=n0

The guest gets IP 10.0.2.15, gateway 10.0.2.2, DNS 10.0.2.3. NAT-based — the guest can reach the internet but the host can't initiate connections to the guest (except via port forwarding). Good enough for most development.

TAP — full network integration

# Create a TAP device (requires root)
sudo ip tuntap add dev tap0 mode tap user $(whoami)
sudo ip addr add 192.168.100.1/24 dev tap0
sudo ip link set tap0 up

# Use it in QEMU
-netdev tap,id=n0,ifname=tap0,script=no,downscript=no \
-device virtio-net-pci,netdev=n0

The guest gets a real interface on a shared network. You can configure routing, bridging, packet capture. Use this when you need to test real networking scenarios.

Socket — connect multiple QEMU VMs

# VM 1 - listen
-netdev socket,id=n0,listen=:1234 -device virtio-net-pci,netdev=n0

# VM 2 - connect
-netdev socket,id=n0,connect=127.0.0.1:1234 -device virtio-net-pci,netdev=n0

Creates a virtual cable between two VMs. Useful for testing multi-host protocols.

QEMU Snapshots

Save and restore VM state:

# In the QEMU monitor (Ctrl+A, C to enter monitor mode):
savevm my-snapshot      # Save state
loadvm my-snapshot      # Restore state
info snapshots          # List snapshots

From the command line:

# Create a snapshot without running the VM
qemu-img snapshot -c clean-boot ~/qemu-debian.qcow2

# List snapshots
qemu-img snapshot -l ~/qemu-debian.qcow2

# Revert to snapshot
qemu-img snapshot -a clean-boot ~/qemu-debian.qcow2

Use snapshots aggressively. Save a clean-boot snapshot, then you can always revert after a kernel panic or filesystem corruption.

Sharing Files Between Host and Guest

Use 9p/virtio-fs to share a directory:

qemu-system-x86_64 \
  ... \
  -virtfs local,path=/home/you/shared,mount_tag=host_share,security_model=mapped-xattr,id=host0

In the guest:

mkdir -p /mnt/host
mount -t 9p -o trans=virtio host_share /mnt/host

This is great for copying test scripts or kernel modules into the guest without rebuilding the disk image.

The Development Loop

Your daily workflow becomes:

1. Edit code in ~/linux/ (host, neovim)
2. make -j$(nproc)                        # ~seconds for incremental build
3. make modules_install INSTALL_MOD_PATH=/tmp/km
4. Copy modules to rootfs (or use 9p share)
5. ~/boot-kernel.sh                       # Boot in QEMU
6. Test your changes
7. Ctrl+A, X to exit
8. Repeat

For module-only changes, you can skip rebuilding bzImage and just reload the module in a running guest.

Exercises

  1. Boot the defconfig kernel in QEMU. Verify it reaches a login prompt.
  2. Inside the guest, run uname -r and confirm it matches your built kernel.
  3. Set up user-mode networking with port forwarding. Install openssh-server in the guest and SSH in from the host via ssh -p 2222 root@localhost.
  4. Set up a TAP interface and ping between host and guest.
  5. Create a clean-boot snapshot. Break something (delete /bin/bash in the guest). Restore the snapshot.
  6. Set up 9p sharing. Write a test script on the host and run it in the guest.

What's Next

Next week we add debugging to this setup — GDB attached to the kernel, printk tracing, and ftrace. This is how you'll diagnose your networking changes when they don't work.