4、QEMU快速上手:安装、基本命令行用法、启动一个虚拟目标(如ARM、RISC-V)、与GDB联调

QEMU 是当前硬件仿真与驱动开发领域使用最广泛的开源模拟器之一。它支持多种指令集架构(如 x86、ARM、RISC-V、MIPS、PowerPC 等),既可以进行纯系统级仿真(System Mode),也可以进行用户态仿真(User Mode)。在驱动开发、BSP 移植、固件调试和嵌入式系统学习场景中,QEMU 提供了无需真实硬件即可运行目标代码的能力,配合 GDB 的远程调试接口,可以极大提升开发效率。本章将围绕 QEMU 的安装、命令行参数、启动 ARM/RISC-V 虚拟目标以及与 GDB 的联调方法展开,帮助学员建立一套可复用的 QEMU 驱动模拟工作流。

QEMU 驱动模拟与调试链路 主机开发环境 (Linux/Windows/macOS) QEMU 安装 交叉编译工具链 GDB 调试器 终端/脚本工具 QEMU 虚拟目标 (Machine) ARM (vexpress-a9) Cortex-A9 / GIC / PL011 -M vexpress-a9 RISC-V (virt) SiFive U54 / CLINT / PLIC -M virt -cpu rv64 外设模型 UART / GPIO / Timer Flash / SD / Ethernet 调试与运行通道 GDB 远程调试 -s -S 启动后等待连接 target remote :1234 break / continue / info reg 串口控制台 -nographic 重定向到终端 -serial mon:stdio UART 输出 / 输入交互

上图展示了 QEMU 驱动模拟与调试的整体链路。主机环境通过 QEMU 启动虚拟目标,虚拟目标内部包含 CPU 核、内存、中断控制器和各类外设模型。调试时,GDB 通过 QEMU 开放的远程调试端口(默认 1234)连接,实现断点、单步、寄存器查看等操作。串口控制台则通过 -nographic 或 -serial 参数映射到主机终端,用于观察驱动输出和交互输入。理解这条链路是后续所有操作的基础。

4.1 QEMU 的安装与版本选择

核心原理

QEMU 的安装方式取决于主机操作系统和使用场景。对于驱动开发人员,推荐使用发行版自带的 QEMU 包或从源码编译。发行版包安装简单,但版本可能较旧;源码编译可以获取最新特性,但需要额外处理依赖。QEMU 的版本直接影响所支持的机器类型(Machine Type)和 CPU 模型,例如 ARM 的 vexpress-a9、RISC-V 的 virt 机器在不同版本中行为可能略有差异。

安装操作流程

以下是在 Ubuntu 22.04 LTS 上安装 QEMU 的典型步骤:

# 更新软件源
sudo apt update

# 安装 QEMU 全功能包(包含系统模式与用户模式)
sudo apt install qemu-system-arm qemu-system-riscv64 qemu-user

# 验证安装
qemu-system-arm --version
qemu-system-riscv64 --version

# 如果需要图形界面支持(如 GTK 显示)
sudo apt install qemu-system-gui

对于 macOS,可以使用 Homebrew:

brew install qemu

对于 Windows,建议使用 MSYS2 或 WSL2 环境,因为 QEMU 在 Linux 环境下对串口、网络和调试的支持更完善。

版本选择建议

场景推荐版本理由
嵌入式驱动开发(ARM/RISC-V)QEMU 7.0+支持更多外设模型,修复了旧版中断控制器 bug
生产环境长期维护发行版自带版本稳定性优先,经过充分测试
需要最新 CPU 特性源码编译 8.0+可自定义编译选项,启用 TCG 优化
经验提醒:在驱动开发中,不要盲目追求最新版本。某些外设模型(如 PL011 UART、PL190 VIC)在旧版本中反而更稳定。建议在项目开始时固定 QEMU 版本,避免因升级导致行为变化。

4.2 基本命令行用法与启动 ARM 虚拟目标

核心原理

QEMU 的命令行参数遵循 qemu-system-<arch> [options] 的格式。关键参数包括 -M(指定机器类型)、-cpu(指定 CPU 模型)、-m(内存大小)、-kernel(加载内核镜像)、-nographic(无图形界面)、-s(开启 GDB 服务)、-S(启动时暂停 CPU)。理解这些参数是启动虚拟目标的前提。

启动 ARM 虚拟目标(vexpress-a9)

以 ARM Cortex-A9 开发板为例,使用 vexpress-a9 机器类型。该机器模拟了 RealView Express 开发板,包含 PL011 UART、PL181 SD 控制器、PL041 音频等外设。启动命令如下:

# 准备内核镜像(以 Linux 内核 zImage 为例)
# 假设已经编译好 zImage 和 dtb 文件

qemu-system-arm \
  -M vexpress-a9 \
  -cpu cortex-a9 \
  -m 512M \
  -kernel zImage \
  -dtb vexpress-v2p-ca9.dtb \
  -nographic \
  -append "console=ttyAMA0,115200 root=/dev/mmcblk0 rw" \
  -sd rootfs.ext4

参数说明:

  • -M vexpress-a9:指定机器类型为 vexpress-a9。
  • -cpu cortex-a9:指定 CPU 为 Cortex-A9。
  • -m 512M:分配 512MB 内存。
  • -kernel zImage:加载内核镜像。
  • -dtb:加载设备树文件。
  • -nographic:将串口重定向到当前终端,不启动图形窗口。
  • -append:传递内核启动参数。
  • -sd rootfs.ext4:挂载 SD 卡镜像作为根文件系统。

启动 RISC-V 虚拟目标(virt)

RISC-V 的 virt 机器是 QEMU 提供的通用虚拟平台,支持 64 位 RISC-V 处理器。启动命令如下:

qemu-system-riscv64 \
  -M virt \
  -cpu rv64 \
  -m 256M \
  -kernel Image \
  -nographic \
  -append "console=ttyS0,115200 root=/dev/vda rw" \
  -drive file=rootfs.img,format=raw,id=hd0 \
  -device virtio-blk-device,drive=hd0

这里使用了 virtio-blk 设备作为磁盘,这是 RISC-V virt 机器常用的存储方案。

常见误区

风险提示:很多初学者在启动 ARM 目标时忘记加载设备树(dtb),导致内核无法识别外设。另外,-nographic 模式下,QEMU 的 monitor 控制台默认绑定到串口 0,如果内核串口参数配置错误,将看不到任何输出。建议先使用 -serial mon:stdio 分离 monitor 和串口输出,便于排查。

4.3 与 GDB 联调:断点、单步与寄存器查看

核心原理

QEMU 内置了 GDB 远程调试服务器。通过 -s 参数在端口 1234 上监听 GDB 连接,-S 参数让 CPU 在启动时暂停,等待调试器连接。GDB 通过 target remote :1234 连接后,可以像调试本地程序一样设置断点、单步执行、查看寄存器和内存。对于驱动开发,这允许我们在驱动初始化代码处设置断点,逐步跟踪硬件寄存器操作。

联调操作流程

以下是一个完整的 GDB 联调示例,以 ARM vexpress-a9 为例:

# 终端 1:启动 QEMU 并等待 GDB
qemu-system-arm \
  -M vexpress-a9 \
  -cpu cortex-a9 \
  -m 512M \
  -kernel zImage \
  -dtb vexpress-v2p-ca9.dtb \
  -nographic \
  -s -S \
  -append "console=ttyAMA0,115200"
# 终端 2:启动 GDB 并连接
arm-none-eabi-gdb vmlinux   # 使用带符号的内核镜像
(gdb) target remote :1234
(gdb) break start_kernel    # 在内核入口设置断点
(gdb) continue
(gdb) info registers        # 查看寄存器
(gdb) x/10x 0x10000000      # 查看内存内容
(gdb) stepi                 # 单步执行一条指令

案例拆解:调试一个 UART 驱动初始化

假设我们需要调试 PL011 UART 驱动的初始化过程。PL011 的基地址在 vexpress-a9 上为 0x10009000。我们可以在驱动初始化函数中设置断点,观察寄存器写入顺序。

步骤GDB 命令预期结果
1. 设置断点break pl011_init断点命中在 pl011_init 函数入口
2. 查看当前 PCinfo registers pcPC 指向 pl011_init 首条指令
3. 单步执行stepi逐条执行汇编指令
4. 查看 UART 寄存器x/4x 0x10009000观察 UARTDR、UARTFR 等寄存器值
5. 检查中断状态p/x *(unsigned int*)0x10009018查看 UARTIMSC 中断屏蔽寄存器

通过这种方式,我们可以精确判断驱动是否按照预期顺序配置了波特率、数据位、停止位等参数。如果发现寄存器值异常,可以回溯到代码逻辑,检查是否存在位操作错误或地址偏移错误。

实战复盘:在一次驱动调试中,我们发现 UART 输出乱码。通过 GDB 查看 UARTIBRD(整数波特率寄存器)和 UARTFBRD(小数波特率寄存器),发现波特率计算错误。原因是驱动代码中使用了错误的时钟频率(假设为 24MHz,实际为 14.7MHz)。通过 GDB 在计算函数处设置断点,检查传入的时钟参数,最终定位到时钟初始化代码中的宏定义错误。修复后,串口输出恢复正常。

常见误区与总结

  • 误区一:忘记使用 -S 参数,导致 CPU 在 GDB 连接前就开始执行,错过早期初始化代码。
  • 误区二:使用 -s 时端口被占用,可以改用 -gdb tcp::1235 指定其他端口。
  • 误区三:GDB 连接后无法设置硬件断点。QEMU 的 TCG 模式支持软件断点,但硬件断点数量有限(通常 4 个),如果断点过多,可以改用 hbreak 命令。
  • 误区四:在 -nographic 模式下,GDB 的 continue 后串口输出会与 GDB 输出混在一起。建议使用 set pagination off 关闭分页,并使用 set logging file 保存调试日志。

总结本章内容,QEMU 的快速上手需要掌握三个核心能力:一是正确安装并选择合适版本;二是熟练使用命令行参数启动 ARM/RISC-V 虚拟目标;三是通过 GDB 远程调试实现驱动代码的断点跟踪和寄存器分析。这三项能力构成了驱动模拟开发的基础工作流。在实际项目中,建议将 QEMU 启动命令封装为脚本,将 GDB 调试命令写入初始化文件(如 .gdbinit),以提高开发效率。同时,要养成查看 QEMU 日志(-d 参数)的习惯,例如 -d int,cpu_reset 可以查看中断和 CPU 复位信息,这对于排查驱动中的中断处理问题非常有帮助。