22.1 为什么 QEMU 能替代开发板
| 书上需要的能力 | 真实开发板 | QEMU 对应 |
|---|---|---|
| 启动一个 ARM/x86 内核 | 烧写 SD 卡/UART | -kernel Image -append ... |
| 串口控制台 | UART 转 USB | -nographic + console=ttyAMA0 |
| 字符设备/平台设备 | 板载外设 | platform_device + simple-bus |
| I²C 总线与芯片 | 硬件 I²C + EEPROM | i2c-stub 、virtio-i2c |
| SPI 总线与芯片 | 硬件 SPI + Flash | spi-gpio + gpio-sim 、virtio-spi |
| GPIO | 板载 GPIO | gpio-sim 、gpio-virtuser、virtio-gpio |
| 中断控制器 | GIC/PLIC | QEMU virt 内置 GIC/PLIC |
| 网络 | 板载网卡 | virtio-net |
| 块设备 | eMMC/NAND | virtio-blk、-drive |
| USB 主机 | USB 控制器 | -device qemu-xhci |
| USB 设备(Gadget) | USB 从控制器 | dummy_hcd + configfs |
| DMA | 板载 DMA | dmatest、阅读 dw-dma |
| 时钟/电源管理 | PMIC | QEMU virt 的 cpufreq/cpuidle 有限支持 |
| 内核源码级调试 | JTAG | -s -S + gdbstub |
结论 :除了少数"真机才有"的细节(真实时序、电压、EMC), 书上的每一个知识点都能在 QEMU 上复现。
22.2 环境准备
22.2.1 宿主依赖
bash
sudo apt update
sudo apt install -y \
build-essential git bc bison flex libssl-dev libelf-dev libncurses-dev \
pahole cpio rsync kmod udev \
qemu-system-x86 qemu-system-arm qemu-utils \
gdb gdb-multiarch device-tree-compiler \
busybox-static e2fsprogs dosfstools \
i2c-tools spi-tools gpiod libgpiod-dev \
python3 perl tar xz-utils
版本建议 :QEMU ≥ 8.0(virtio-gpio/virtio-spi 需要较新版本)。
bash
qemu-system-x86_64 --version
qemu-system-x86_64 -device help | grep -E 'virtio|i2c|spi|gpio|usb'
qemu-system-aarch64 -machine virt -device help | grep -E 'virtio|spi|gpio'
22.2.2 环境变量(所有实验共用)
bash
export KSRC=~works/study/linux-7.2.5
export KBUILD=~works/study/build/linux-7.2.5-x86_64
export KBUILD_ARM=~works/study/build/linux-7.2.5-arm64
export LAB=~works/study/qemu-lab
mkdir -p "$LAB"
注意 :构建产物一律放到源码树之外 (
O=),保持源码树干净, 也便于同时保留 x86_64 和 arm64 两套配置。
22.3 编译内核
22.3.1 x86_64(主力实验平台)
bash
mkdir -p "$KBUILD"
# 1. 基础配置
make -C "$KSRC" O="$KBUILD" x86_64_defconfig
# 2. 合并 KVM guest 配置(串口/virtio 等)
"$KSRC/scripts/kconfig/merge_config.sh" -O "$KBUILD" \
"$KBUILD/.config" "$KSRC/kernel/configs/kvm_guest.config"
# 3. 合并调试配置(可选,但强烈建议学习期打开)
"$KSRC/scripts/kconfig/merge_config.sh" -O "$KBUILD" \
"$KBUILD/.config" "$KSRC/kernel/configs/debug.config"
# 4. 应用并交互调整
make -C "$KSRC" O="$KBUILD" olddefconfig
make -C "$KSRC" O="$KBUILD" menuconfig
# 5. 编译
make -C "$KSRC" O="$KBUILD" -j"$(nproc)"
产物:
| 文件 | 说明 |
|---|---|
$KBUILD/arch/x86/boot/bzImage |
可引导内核 |
$KBUILD/vmlinux |
带符号的内核(GDB 用) |
$KBUILD/System.map |
符号表 |
$KBUILD/.config |
最终配置 |
22.3.2 arm64(总线/设备树实验平台)
bash
mkdir -p "$KBUILD_ARM"
make -C "$KSRC" O="$KBUILD_ARM" ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- virt.config
make -C "$KSRC" O="$KBUILD_ARM" ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
make -C "$KSRC" O="$KBUILD_ARM" ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j"$(nproc)" Image dtbs
内核自带
arch/arm64/configs/virt.config,专为 QEMUvirt机器准备, 是最省事的起点。
产物 :$KBUILD_ARM/arch/arm64/boot/Image、.../dts/.../*.dtb。
22.3.3 必开的实验配置项
bash
# 基础
CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y
CONFIG_TMPFS=y
CONFIG_PROC_FS=y
CONFIG_SYSFS=y
CONFIG_DEBUG_FS=y
CONFIG_CONFIGFS_FS=y
# 调试
CONFIG_DYNAMIC_DEBUG=y
CONFIG_FTRACE=y
CONFIG_KALLSYMS_ALL=y
CONFIG_DEBUG_INFO=y
CONFIG_GDB_SCRIPTS=y
CONFIG_MAGIC_SYSRQ=y
# 文件系统共享
CONFIG_9P_FS=y
CONFIG_NET_9P=y
CONFIG_NET_9P_VIRTIO=y
# 虚拟设备
CONFIG_VIRTIO_PCI=y
CONFIG_VIRTIO_BLK=y
CONFIG_VIRTIO_NET=y
CONFIG_VIRTIO_CONSOLE=y
CONFIG_VIRTIO_INPUT=y
# 总线实验
CONFIG_I2C=y
CONFIG_I2C_CHARDEV=y
CONFIG_I2C_STUB=m
CONFIG_EEPROM_AT24=m
CONFIG_REGMAP_I2C=y
CONFIG_SPI=y
CONFIG_SPI_SPIDEV=y
CONFIG_SPI_GPIO=y
CONFIG_GPIO_SIM=m
CONFIG_GPIO_VIRTUSER=m
# USB 实验
CONFIG_USB_GADGET=y
CONFIG_USB_DUMMY_HCD=y
CONFIG_USB_CONFIGFS=y
CONFIG_USB_CONFIGFS_ACM=y
CONFIG_USB_CONFIGFS_MASS_STORAGE=y
CONFIG_USB_CONFIGFS_F_FS=y
CONFIG_USB_MON=y
22.4 制作 initramfs(最简 rootfs)
initramfs 是最简单、最可靠的 QEMU rootfs:一个 cpio 归档,内核启动后解到 tmpfs。
bash
mkdir -p "$LAB/initramfs"/{bin,sbin,etc,proc,sys,dev,tmp,lib,root}
cd "$LAB/initramfs"
# 用静态 busybox 提供所有基本命令
cp /bin/busybox bin/
for cmd in sh ls cat mount umount echo dmesg modprobe insmod rmmod \
mkdir mknod sleep ps ifconfig ip mdev find grep head tail \
printf dd hexdump vi; do
ln -sf busybox "bin/$cmd"
done
cat > init <<'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev 2>/dev/null
mount -t tmpfs none /tmp
mount -t debugfs none /sys/kernel/debug 2>/dev/null
mount -t configfs none /sys/kernel/config 2>/dev/null
echo "=== initramfs booted: $(uname -r) ==="
exec /bin/sh
EOF
chmod +x init
find . -print0 | cpio --null -ov --format=newc | gzip -9 > "$LAB/initramfs.cpio.gz"
验证:
bash
qemu-system-x86_64 -kernel "$KBUILD/arch/x86/boot/bzImage" \
-initrd "$LAB/initramfs.cpio.gz" \
-append "console=ttyS0 rdinit=/init" \
-nographic -no-reboot
22.5 启动脚本(x86_64)
bash
cat > "$LAB/run-x86.sh" <<'EOF'
#!/bin/bash
set -e
KSRC=~works/study/linux-7.2.5
KBUILD=~works/study/build/linux-7.2.5-x86_64
LAB=~works/study/qemu-lab
DISK=$LAB/disk.img
[ -f "$DISK" ] || truncate -s 256M "$DISK"
qemu-system-x86_64 \
-m 1G -smp 2 \
-kernel "$KBUILD/arch/x86/boot/bzImage" \
-initrd "$LAB/initramfs.cpio.gz" \
-append "console=ttyS0 root=/dev/ram rdinit=/init loglevel=7" \
-drive file="$DISK",format=raw,if=virtio \
-netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
-device qemu-xhci,id=xhci \
-device usb-kbd,bus=xhci.0 \
-nographic -no-reboot "$@"
EOF
chmod +x "$LAB/run-x86.sh"
常用附加参数(按需加到脚本或命令行):
| 参数 | 用途 |
|---|---|
-s -S |
开放 gdbstub 并在启动时暂停(第 21 章) |
-smp 4 |
4 核(SMP/热插拔实验) |
-device virtio-gpio-pci |
虚拟 GPIO(若 QEMU 支持) |
-device virtio-i2c-pci |
虚拟 I²C |
-device usb-storage,... |
USB 存储 |
-virtfs local,path=$SHARE,mount_tag=host0,security_model=none |
9p 共享目录 |
-dtb my.dtb |
自定义设备树(arm64) |
22.6 启动脚本(arm64 / 设备树)
bash
cat > "$LAB/run-arm64.sh" <<'EOF'
#!/bin/bash
set -e
KSRC=~works/study/linux-7.2.5
KBUILD=~works/study/build/linux-7.2.5-arm64
LAB=~works/study/qemu-lab
qemu-system-aarch64 \
-machine virt,dumpdtb="$LAB/virt.dtb" \
-cpu cortex-a57 -smp 2 -m 1G \
-kernel "$KBUILD/arch/arm64/boot/Image" \
-initrd "$LAB/initramfs.cpio.gz" \
-append "console=ttyAMA0 rdinit=/init loglevel=7" \
-netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
-nographic -no-reboot "$@"
EOF
chmod +x "$LAB/run-arm64.sh"
# 反编译 QEMU 生成的设备树(第 18 章实验)
dtc -I dtb -O dts -o "$LAB/virt.dts" "$LAB/virt.dtb"
22.7 知识点 → QEMU 手段总表
| 章 | 知识点 | QEMU 手段 | 关键命令/文件 |
|---|---|---|---|
| 1 | Hello World 模块 | 任意 guest | insmod hello.ko、dmesg |
| 1 | LED 驱动 | gpio-sim / gpio-leds |
/sys/class/leds/ |
| 2 | 硬件基础 | QEMU 参数 | -m、-smp、info mtree |
| 3 | 内核编译 | O= 构建 |
make -C $KSRC O=$KBUILD |
| 4 | 内核模块 | 外部模块 | make -f $KDIR/Makefile M=$PWD |
| 5 | 设备文件/udev | devtmpfs | /dev/、mdev |
| 6 | 字符设备 | 任意 guest | cdev、/dev/globalmem |
| 7 | 并发控制 | -smp 4 |
mutex/spinlock |
| 8 | 阻塞/非阻塞 IO | 任意 guest | poll/epoll |
| 9 | 异步通知/AIO | 任意 guest | SIGIO、io_uring |
| 10 | 中断与时钟 | QEMU 定时器 | /proc/interrupts |
| 11 | 内存与 IO | ioremap、DMA |
/proc/iomem |
| 12 | 驱动架构 | platform/simple-bus |
/sys/bus/platform/ |
| 13 | 块设备 | virtio-blk |
-drive if=virtio |
| 14 | 网络设备 | virtio-net |
ip link、ping |
| 15 | I²C | i2c-stub 、virtio-i2c |
i2cdetect、/dev/i2c-0 |
| 16 | USB | qemu-xhci + dummy_hcd |
lsusb、configfs |
| 17 | SPI | spi-gpio+gpio-sim |
/dev/spidev0.0 |
| 18 | 设备树 | -machine virt,dumpdtb= |
/proc/device-tree/ |
| 19 | 电源管理 | QEMU cpufreq/cpuidle | /sys/.../cpufreq/ |
| 20 | 芯片级移植 | QEMU virt |
clk_summary、gpioinfo |
| 21 | 调试 | -s -S + gdbstub |
hbreak、lx-dmesg |
22.8 完整实验:从零到 I²C 读写
bash
# ==== 宿主机 ====
export KSRC=~works/study/linux-7.2.5
export KBUILD=~works/study/build/linux-7.2.5-x86_64
export LAB=~works/study/qemu-lab
# 1. 编译内核(见 22.3.1)
# 2. 制作 initramfs(见 22.4)
# 3. 启动(见 22.5)
"$LAB/run-x86.sh"
# ==== guest 内 ====
modprobe i2c-stub chip_addr=0x50
modprobe i2c-dev
i2cdetect -l
i2cdetect -y 0
i2cset -y 0 0x50 0x00 0x11
i2cget -y 0 0x50 0x00
dmesg | tail
22.9 故障排查
| 症状 | 原因 | 解决 |
|---|---|---|
| 无任何输出 | 控制台名错 | x86 用 ttyS0,arm64 用 ttyAMA0 |
VFS: Unable to mount root |
无 rootfs | 加 -initrd 和 rdinit=/init |
Kernel panic - not syncing: No init found |
init 路径错 | 检查 rdinit= 与 initramfs 里的 init |
模块 insmod 报 Invalid module format |
内核与模块版本/配置不一致 | 用同一 KBUILD 编译模块 |
modprobe 找不到模块 |
无 /lib/modules |
用 insmod 绝对路径,或建 /lib/modules/$(uname -r)/ |
i2cdetect 报无设备 |
i2c-stub 未加载/总线号错 | lsmod、i2cdetect -l |
gpio-sim 无芯片 |
configfs 未挂/未 live |
mount -t configfs,echo 1 > live |
| GDB 断点无效 | KASLR | 加 nokaslr,用 hbreak |
| GDB 符号不对 | 用了错误的 vmlinux | 必须用同一 KBUILD 的 vmlinux |
dummy_hcd 无 /sys/class/udc |
配置未开 | CONFIG_USB_DUMMY_HCD=y |
QEMU 无 virtio-i2c |
QEMU 版本旧 | 降级到 i2c-stub 方案 |
编译缺 pahole |
BTF 需要 | apt install pahole |
bison/flex 缺失 |
配置解析需要 | apt install bison flex |
| 磁盘空间不足 | 构建产物大 | 只保留一套配置,或 make clean |
调试启动问题的三板斧:
- 加
earlycon(第 20.5 节)看最早输出。 - 加
loglevel=7 ignore_loglevel看全部日志。 - 用
-s -S+ GDB 单步(第 21.9 节)。
22.10 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| 编译 x86_64 内核 | 掌握构建 | bzImage/vmlinux |
| 编译 arm64 内核 | 掌握交叉编译 | Image/dtb |
| 制作 initramfs | 掌握 rootfs | guest 进入 shell |
| 启动脚本 | 掌握 QEMU 参数 | uname -r 正确 |
| 9p 共享目录 | 掌握文件交换 | /mnt/host 可见 |
| I²C 全流程 | 综合实验 | i2cget 读到数据 |
| gpio-sim | 综合实验 | gpioinfo 有芯片 |
| dummy_hcd | 综合实验 | /sys/class/udc/ |
| GDB 调试内核 | 综合实验 | 断点命中 |
22.11 总结
- QEMU 可以替代开发板完成本书全部知识点,只有极少数真机细节例外。
- 统一用
O=外部构建,保持源码树干净,并可同时保留多套配置。 - initramfs 是最简可靠的 rootfs:一个 cpio 归档 + 静态 busybox。
- x86_64 用
x86_64_defconfig+kvm_guest.config;arm64 用virt.config。 - i2c-stub 、gpio-sim 、spi-gpio 、dummy_hcd 是四条"凭空造硬件"的关键路径。
-s -S+ gdbstub 让内核调试达到源码级单步的水平。- 排错三板斧:
earlycon、loglevel=7、GDB 单步。 - 本章的启动脚本和配置片段可直接复制使用,是所有后续实验的基础设施。