3. QEMU虚拟机调试:使用QEMU启动内核、配置GDB远程调试、连接QEMU的GDB stub
QEMU 是内核调试中最常用的全系统模拟器。它内置了 GDB stub,允许 GDB 通过 TCP 远程连接到 QEMU 的虚拟 CPU,实现与真实硬件几乎一致的调试体验。本章将完整演示从 QEMU 启动内核到 GDB 远程连接的全流程。
核心概念: QEMU 的 GDB stub 在虚拟机内部实现了一个轻量级 GDB 服务器。当 QEMU 启动时加上
-s 或 -gdb tcp::PORT 参数,它会在指定端口等待 GDB 连接。连接后,GDB 可以控制虚拟 CPU、访问内存、设置断点,就像调试本地进程一样。
3.1 使用 QEMU 启动内核
启动一个可调试的内核需要准备内核镜像(vmlinux)、根文件系统(initrd 或磁盘镜像)以及正确的 QEMU 参数。以下是最小化启动命令模板:
qemu-system-x86_64 \
-kernel /path/to/arch/x86_64/boot/bzImage \
-initrd /path/to/rootfs.cpio.gz \
-append "console=ttyS0 nokaslr" \
-nographic \
-s
| 参数 | 作用 | 调试相关说明 |
|---|---|---|
-kernel |
指定内核二进制文件(bzImage) | 必须与 GDB 加载的 vmlinux 符号文件对应 |
-initrd |
指定初始根文件系统 | 可使用最小 busybox 镜像 |
-append "console=ttyS0 nokaslr" |
向内核传递命令行参数 | nokaslr 禁用内核地址空间布局随机化,保证符号地址固定 |
-nographic |
不使用图形窗口,串口输出到终端 | 便于在终端中同时运行 GDB |
-s |
等价于 -gdb tcp::1234 |
在 TCP 1234 端口开启 GDB stub |
调试要点: 务必添加
nokaslr 参数。如果 KASLR 开启,内核每次启动的基地址都会变化,GDB 加载的符号地址将与实际运行地址不匹配,导致断点失效或崩溃。
3.2 QEMU GDB stub 架构
下图展示了 QEMU、GDB stub 与 GDB 客户端之间的通信架构:
协议说明: GDB 与 QEMU stub 之间使用 GDB Remote Serial Protocol (RSP)。这是一个基于 ASCII 或二进制的包协议,包含寄存器读写、内存读写、断点插入/删除、单步执行等命令。QEMU stub 将这些命令翻译为对虚拟 CPU 和内存的实际操作。
3.3 配置 GDB 远程调试
启动 QEMU 后,在另一个终端中启动 GDB 并加载内核符号文件:
# 启动 GDB,加载未压缩的内核镜像(含符号表)
gdb /path/to/vmlinux
# 在 GDB 内部执行远程连接
(gdb) target remote :1234
Remote debugging using :1234
0xffffffff81000000 in ?? ()
# 验证连接状态
(gdb) info registers
rax 0x0 0
rbx 0x0 0
rcx 0x0 0
rdx 0x0 0
rsi 0x0 0
rdi 0x0 0
rip 0xffffffff81000000 0xffffffff81000000
...
常见错误: 如果 GDB 报
Remote 'g' packet reply is too long,通常是因为 QEMU 模拟的 CPU 架构与 GDB 预期不匹配。解决方法:在 GDB 中执行 set architecture i386:x86-64:intel 手动指定架构。
3.4 连接 QEMU 的 GDB stub
连接成功后,GDB 会停在 QEMU 启动后的第一条指令(通常是 BIOS 或内核入口点)。此时可以执行标准调试操作:
| GDB 命令 | 作用 | QEMU 环境下的行为 |
|---|---|---|
c / continue |
继续执行 | QEMU 虚拟 CPU 开始运行,直到遇到断点或中断 |
si / stepi |
单步执行一条指令 | QEMU 执行一条 x86 指令后返回控制权 |
b start_kernel |
设置函数断点 | QEMU 在 start_kernel 入口处暂停 |
p/x $cr3 |
打印寄存器值 | 读取虚拟 CPU 的 CR3 寄存器(页表基址) |
x/10gx 0xffffffff81000000 |
查看内存内容 | 读取 QEMU 虚拟物理内存 |
3.5 完整调试启动流程
以下是一个从 QEMU 启动到内核入口点断点的完整操作序列:
- 终端 1: 启动 QEMU,等待 GDB 连接
qemu-system-x86_64 \
-kernel bzImage \
-initrd initramfs.cpio.gz \
-append "console=ttyS0 nokaslr" \
-nographic \
-S -s
关键参数
-S: 该参数让 QEMU 在启动时立即冻结 CPU,不执行任何指令,直到 GDB 发送 continue 命令。这确保 GDB 可以在第一条指令之前就获得控制权。
- 终端 2: 启动 GDB 并连接
gdb vmlinux
(gdb) target remote :1234
(gdb) break start_kernel
(gdb) continue
此时 QEMU 开始执行,直到进入 start_kernel 函数后暂停,GDB 返回提示符:
Breakpoint 1, start_kernel () at init/main.c:576
576 {
(gdb) list
571 asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
572 {
573 char *command_line;
574 char *after_dashes;
575
576 set_task_stack_end_magic(&init_task);
577 smp_setup_processor_id();
578 debug_objects_early_init();
579 init_vmlinux_build_id();
580 cgroup_init_early();
581 local_irq_disable();
3.6 断开与重连
调试过程中可以安全地断开 GDB 而不影响 QEMU 运行:
# 断开连接(QEMU 继续运行)
(gdb) disconnect
# 重新连接(QEMU 必须仍在运行)
(gdb) target remote :1234
注意: 如果 QEMU 进程已经退出,GDB 会报
Connection refused。此时需要重新启动 QEMU。如果 QEMU 仍在运行但端口被占用,检查是否有其他进程占用了 1234 端口。
3.7 调试配置检查清单
在开始调试前,逐项确认以下配置:
| 检查项 | 正确配置 | 常见错误 |
|---|---|---|
| 内核编译选项 | CONFIG_DEBUG_INFO=yCONFIG_GDB_SCRIPTS=y |
未开启调试符号,GDB 无法显示源码 |
| KASLR 状态 | 内核参数添加 nokaslr |
KASLR 开启导致符号地址偏移 |
| QEMU 端口 | -s 或 -gdb tcp::1234 |
端口被防火墙或其它进程占用 |
| GDB 符号文件 | 使用 vmlinux(非 bzImage) |
加载 bzImage 导致无符号信息 |
| CPU 冻结 | 首次调试使用 -S |
未加 -S 导致错过早期断点 |
进阶技巧: 如果需要在 QEMU 启动早期(如 BIOS 阶段)调试,可以使用
-s -S 组合,GDB 连接后执行 break *0xfffffff0(BIOS 入口),然后 continue。这可以观察从实模式到保护模式再到长模式的完整切换过程。