Testing with QEMU
QEMU allows you to test kernel modules without physical hardware. This is invaluable for development and debugging.
Why QEMU?
- No hardware needed: Test ARM64 modules on x86 host
- Fast iteration: Reboot virtual machine in seconds
- Safe testing: Crashes don’t affect your system
- Debugging: GDB integration for kernel debugging
- CI/CD friendly: Automated testing in pipelines
QEMU Overview
QEMU can emulate various architectures:
| Command | Architecture |
|---|---|
qemu-system-aarch64 |
ARM64 (AArch64) |
qemu-system-arm |
ARM32 |
qemu-system-x86_64 |
x86_64 |
For embedded development, we’ll focus on ARM64.
Quick Start: Minimal Boot
Let’s boot a minimal ARM64 Linux system:
Build a Minimal Kernel
cd /kernel/linux
# Configure for ARM64 with minimal options
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
# Enable useful options for testing
./scripts/config --enable CONFIG_MODULES
./scripts/config --enable CONFIG_MODULE_UNLOAD
./scripts/config --enable CONFIG_PRINTK
./scripts/config --enable CONFIG_DEVTMPFS
./scripts/config --enable CONFIG_DEVTMPFS_MOUNT
# Build the kernel
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) Image
Create a Minimal Root Filesystem
Using BusyBox for a simple initramfs:
# Create directory structure
mkdir -p /tmp/initramfs/{bin,sbin,etc,proc,sys,dev,lib,lib64,tmp}
cd /tmp/initramfs
# Download and build BusyBox (static)
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar xf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- install CONFIG_PREFIX=/tmp/initramfs
Create init script /tmp/initramfs/init:
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
echo "Welcome to Linux Driver Development Testing Environment"
echo "Load your module with: insmod /path/to/your.ko"
echo ""
exec /bin/sh
Make it executable:
chmod +x /tmp/initramfs/init
Create the initramfs:
cd /tmp/initramfs
find . | cpio -H newc -o | gzip > /tmp/initramfs.cpio.gz
Boot with QEMU
qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-m 512M \
-kernel /kernel/linux/arch/arm64/boot/Image \
-initrd /tmp/initramfs.cpio.gz \
-append "console=ttyAMA0" \
-nographic
Press Ctrl+A X to exit QEMU.
Testing Your Module
Copy Module to Initramfs
# Add your module to initramfs
cp /workspace/examples/part1/hello-world/hello.ko /tmp/initramfs/
# Rebuild initramfs
cd /tmp/initramfs
find . | cpio -H newc -o | gzip > /tmp/initramfs.cpio.gz
Boot and Test
qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-m 512M \
-kernel /kernel/linux/arch/arm64/boot/Image \
-initrd /tmp/initramfs.cpio.gz \
-append "console=ttyAMA0" \
-nographic
Inside QEMU:
# Load module
insmod /hello.ko
# [ 1.234567] Hello, World! Module loaded.
# Check it's loaded
lsmod
# Module Size Used by
# hello 16384 0
# Unload module
rmmod hello
# [ 1.345678] Goodbye, World! Module unloaded.
Using a Pre-built Root Filesystem
For more complex testing, use a pre-built Debian/Ubuntu root filesystem.
Download Debian ARM64 Rootfs
# Create disk image
qemu-img create -f qcow2 /tmp/debian-arm64.qcow2 4G
# Download Debian installer (or use debootstrap)
# See: https://www.debian.org/distrib/netinst
Alternative: Use Buildroot
Buildroot creates minimal, customized root filesystems:
git clone https://github.com/buildroot/buildroot
cd buildroot
make qemu_aarch64_virt_defconfig
make -j$(nproc)
# Output: output/images/rootfs.cpio
Sharing Files with QEMU
Using 9P Filesystem (VirtFS)
Share a directory between host and QEMU:
qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-m 512M \
-kernel /kernel/linux/arch/arm64/boot/Image \
-initrd /tmp/initramfs.cpio.gz \
-append "console=ttyAMA0" \
-nographic \
-fsdev local,id=shared,path=/workspace/examples,security_model=mapped \
-device virtio-9p-pci,fsdev=shared,mount_tag=hostshare
Inside QEMU:
mkdir -p /mnt/host
mount -t 9p -o trans=virtio hostshare /mnt/host
# Now access your modules
insmod /mnt/host/part1/hello-world/hello.ko
Debugging with GDB
Start QEMU with GDB Server
qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-m 512M \
-kernel /kernel/linux/arch/arm64/boot/Image \
-initrd /tmp/initramfs.cpio.gz \
-append "console=ttyAMA0 nokaslr" \
-nographic \
-s -S # -s = gdb on :1234, -S = wait for gdb
Connect with GDB
In another terminal:
aarch64-linux-gnu-gdb /kernel/linux/vmlinux
(gdb) target remote :1234
(gdb) continue
The nokaslr boot parameter disables kernel address randomization, making debugging easier.
QEMU Convenience Script
Create /workspace/scripts/run-qemu.sh:
#!/bin/bash
# Run QEMU with kernel and initramfs
KERNEL=${KERNEL:-/kernel/linux/arch/arm64/boot/Image}
INITRD=${INITRD:-/tmp/initramfs.cpio.gz}
MEMORY=${MEMORY:-512M}
SHARED=${SHARED:-/workspace/examples}
qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-m "$MEMORY" \
-kernel "$KERNEL" \
-initrd "$INITRD" \
-append "console=ttyAMA0" \
-nographic \
-fsdev local,id=shared,path="$SHARED",security_model=mapped \
-device virtio-9p-pci,fsdev=shared,mount_tag=hostshare
Troubleshooting
“Kernel panic - not syncing: No init found”
Your initramfs is missing /init or it’s not executable:
chmod +x /tmp/initramfs/init
“Unable to mount root fs”
Check initramfs path and format. Rebuild with:
find . | cpio -H newc -o | gzip > /tmp/initramfs.cpio.gz
Module Version Mismatch
Ensure module was built against the same kernel you’re booting:
# Check kernel version
cat /proc/version
# Check module was built against same kernel
modinfo hello.ko | grep vermagic
Summary
With QEMU, you can:
- Build kernel and modules for ARM64
- Create minimal initramfs with your modules
- Boot and test without hardware
- Debug with GDB if needed
This workflow enables rapid development and testing of kernel drivers.
Next Steps
Congratulations! You’ve completed Part 1. Continue to Part 2: Kernel Fundamentals to learn more about kernel internals.