3. 调试环境搭建:QEMU + BusyBox 构建最小调试内核,配置 KGDB 与 KDB 双模式调试
在深入文件系统 I/O 栈异常定位之前,必须搭建一个可控、可复现、可打断点的内核调试环境。本章将引导你从零构建基于 QEMU 虚拟化平台的最小 Linux 内核调试系统,并同时启用 KGDB(内核调试器,通过串口/GDB 远程调试)与 KDB(内核内置调试器,直接键盘输入)两种模式,为后续章节的实战调试打下坚实基础。
学习目标:完成本章后,你将拥有一个可在宿主机上用 GDB 下断点、在 QEMU 控制台用 KDB 命令查看内核状态的完整调试环境,且整个系统镜像不超过 20MB。
3.1 环境概览与组件选择
最小调试系统由四个核心组件构成,其关系如下图所示:
| 组件 | 版本/来源 | 作用 |
|---|---|---|
| Linux 内核源码 | v6.x (LTS 版本) | 编译带调试符号的内核镜像 |
| BusyBox | 1.36.x | 提供精简的 init 与 shell 工具集 |
| QEMU | ≥ 7.2 | 模拟 x86_64 硬件,提供串口与网络 |
| GDB | ≥ 13.x | 宿主机端远程调试客户端 |
3.2 编译带调试符号的内核
内核必须开启调试相关配置,才能支持 KGDB 和 KDB。以下是关键内核配置项:
# 进入内核源码目录
make menuconfig
# 必须开启的调试选项(路径与值)
# Kernel hacking --->
# [*] KGDB: kernel debugger
# [*] KGDB: use kgdb over serial console
# [*] KGDB: internal debugger (KDB)
# [*] KGDB: KDB keyboard input
# [*] Compile the kernel with debug info
# [*] Provide GDB scripts for kernel debugging
# [*] Magic SysRq key
# [*] Enable magic SysRq key by default
# 也可以直接写入 .config
echo "CONFIG_DEBUG_INFO=y" >> .config
echo "CONFIG_KGDB=y" >> .config
echo "CONFIG_KGDB_SERIAL_CONSOLE=y" >> .config
echo "CONFIG_KGDB_KDB=y" >> .config
echo "CONFIG_KDB_KEYBOARD=y" >> .config
echo "CONFIG_MAGIC_SYSRQ=y" >> .config
echo "CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE=0x1" >> .config
避坑提示:如果未开启
CONFIG_KGDB_SERIAL_CONSOLE,KGDB 将无法通过串口与 GDB 通信;如果未开启 CONFIG_KDB_KEYBOARD,KDB 将无法在 QEMU 图形界面中通过键盘触发。这两个选项是双模式调试的基础。
配置完成后,编译内核并保留调试符号:
# 编译内核(使用 -jN 加速)
make -j$(nproc) bzImage
# 编译模块(可选,用于调试文件系统模块)
make -j$(nproc) modules
# 生成 vmlinux(带完整符号表的内核镜像)
# vmlinux 位于源码根目录,用于 GDB 加载符号
ls -lh vmlinux
# 通常约 500MB-1GB(含调试信息)
# 生成 System.map(符号地址映射)
cp System.map /path/to/debug/env/
3.3 使用 BusyBox 构建最小根文件系统
BusyBox 将数百个常用 Linux 命令合并为一个可执行文件,极大减小了根文件系统体积。构建步骤如下:
# 1. 下载并解压 BusyBox
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
# 2. 配置为静态编译(避免依赖动态库)
make menuconfig
# Settings --->
# [*] Build static binary (no shared libs)
# 3. 编译并安装到临时目录
make -j$(nproc)
make install CONFIG_PREFIX=/path/to/initramfs
# 4. 创建必要的设备节点和启动脚本
cd /path/to/initramfs
mkdir -p proc sys dev etc/init.d
# 创建 init 脚本
cat > init << 'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
echo "Welcome to Minimal Debug Kernel"
exec /bin/sh
EOF
chmod +x init
# 5. 打包为 initramfs 镜像
find . | cpio -H newc -o | gzip > /path/to/initramfs.gz
| 步骤 | 关键命令 | 说明 |
|---|---|---|
| 1 | make menuconfig |
选择静态编译,避免在 initramfs 中携带 libc |
| 2 | make install |
将编译好的 BusyBox 和符号链接安装到指定目录 |
| 3 | cpio + gzip |
将目录打包为内核可识别的 initramfs 格式 |
3.4 配置 QEMU 启动参数与双串口映射
QEMU 需要两个串口:一个用于 KGDB(TCP 通道,供 GDB 连接),另一个用于 KDB(stdio 通道,供键盘输入)。启动脚本如下:
#!/bin/bash
# 启动 QEMU 调试内核
KERNEL="/path/to/linux-6.x/arch/x86_64/boot/bzImage"
INITRAMFS="/path/to/initramfs.gz"
qemu-system-x86_64 \
-kernel ${KERNEL} \
-initrd ${INITRAMFS} \
-append "console=ttyS0,115200 nokaslr kgdboc=ttyS1,115200 kgdbwait" \
-serial tcp::1234,server,nowait \
-serial stdio \
-m 512M \
-smp 2 \
-nographic
参数解析:
kgdboc=ttyS1,115200:将 KGDB 绑定到第二个串口(ttyS1),波特率 115200。kgdbwait:内核启动后立即进入 KGDB 等待状态,直到 GDB 连接。nokaslr:禁用内核地址空间布局随机化,确保断点地址稳定。-serial tcp::1234,server,nowait:第一个串口映射为 TCP 1234 端口,供 GDB 连接。-serial stdio:第二个串口映射到当前终端,供 KDB 键盘输入。
3.5 双模式调试操作流程
启动 QEMU 后,内核会因 kgdbwait 停在启动早期。此时可以分别使用 KGDB 和 KDB 进行调试。
3.5.1 KGDB 模式(远程 GDB 调试)
在宿主机另一个终端中启动 GDB:
# 启动 GDB 并加载内核符号
gdb /path/to/linux-6.x/vmlinux
# 在 GDB 中连接 QEMU 的 KGDB 端口
(gdb) target remote :1234
# 此时 GDB 会显示内核停止的位置,例如:
# Remote debugging using :1234
# 0xffffffff81000000 in ?? ()
# 设置断点,例如在 VFS 的 sys_open 函数
(gdb) b do_sys_open
(gdb) c
# 在 QEMU 串口终端中执行一条命令(如 ls),GDB 会命中断点
# 之后可以使用 bt、info registers、list 等标准 GDB 命令
3.5.2 KDB 模式(内置键盘调试器)
当系统正在运行且未通过 KGDB 连接时,可以通过键盘组合键进入 KDB:
# 在 QEMU 的 stdio 串口终端中,按下:
# Ctrl + A, 然后按 c 切换到 QEMU 控制台
# 再按 Ctrl + Alt + 1 切换回串口终端
# 在串口终端中按下 SysRq 键触发 KDB:
# 对于 QEMU stdio,发送 SysRq 的方式是:
# 先按 Ctrl + A, 然后按 x 退出 QEMU(不推荐)
# 正确方式:在 QEMU 启动时添加 -serial mon:stdio 参数
# 然后按 Ctrl + A, 再按 c 进入 QEMU monitor
# 在 monitor 中执行:sendkey sysrq
# 更简单的方法:在内核启动参数中添加:
# kgdboc=ttyS1,115200 kgdbcon
# 然后在串口终端中直接输入:
echo g > /proc/sysrq-trigger
# 此时终端会进入 KDB 提示符:
# [0]kdb>
# 常用 KDB 命令:
# bt - 打印当前 CPU 的堆栈回溯
# btpid <pid> - 打印指定进程的堆栈
# lsmod - 列出已加载的内核模块
# ps - 显示进程列表
# dmesg - 查看内核日志
# go - 继续执行
# help - 显示帮助信息
| 特性 | KGDB | KDB |
|---|---|---|
| 调试接口 | GDB 远程协议(TCP 串口) | 内核内置命令行(键盘/串口) |
| 断点能力 | 支持硬件/软件断点、条件断点、watchpoint | 仅支持简单的硬件断点(b 命令) |
| 源码级调试 | 支持(加载 vmlinux 后可见源码) | 不支持(仅显示汇编/符号地址) |
| 适用场景 | 复杂逻辑分析、单步跟踪、变量查看 | 紧急现场分析、死锁/崩溃快速诊断 |
| 启动方式 | GDB 主动连接(target remote) | SysRq 触发或 kgdbwait 后切换 |
3.6 验证调试环境是否正常工作
完成上述配置后,执行以下验证步骤确认双模式均可用:
- 验证 KGDB 连接:启动 QEMU 后,在 GDB 中执行
target remote :1234,应看到内核停止在kgdb_breakpoint()函数。执行bt应显示完整的启动调用链。 - 验证 KDB 切换:在 QEMU 串口终端中执行
echo g > /proc/sysrq-trigger,应进入[0]kdb>提示符。输入ps应显示当前进程列表。 - 验证断点命中:在 GDB 中设置断点
b do_sys_open,然后c继续执行。在 QEMU 串口终端中执行ls,GDB 应命中断点并暂停。 - 验证 KDB 回溯:在 KDB 提示符下输入
bt,应显示当前 CPU 的内核堆栈,包含函数名和偏移地址。
调试技巧:如果 KGDB 连接超时,检查 QEMU 是否已启动且
kgdbwait 参数生效。可以在 QEMU 启动后立即执行 netstat -an | grep 1234 确认 TCP 端口是否处于 LISTEN 状态。如果 KDB 无法通过 SysRq 触发,确认内核配置中 CONFIG_MAGIC_SYSRQ 已启用且 /proc/sysrq-trigger 文件存在。
3.7 常见问题与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
GDB 连接后显示 Remote 'g' packet reply is too long |
QEMU 与 GDB 的架构不匹配或内核未正确停止 | 在 GDB 中执行 set architecture i386:x86-64 后重新连接 |
| KDB 提示符出现但键盘输入无响应 | 串口映射错误,KDB 未绑定到正确的输入源 | 检查 QEMU 启动参数中 -serial 的顺序,确保第二个串口是 stdio |
| 内核启动时崩溃,无法进入 shell | initramfs 中缺少关键设备节点或 init 脚本错误 | 确认 initramfs 中包含 /dev/console、/proc、/sys 挂载点 |
| KGDB 断点命中后无法单步 | 内核开启了 KASLR 导致地址漂移 | 在启动参数中添加 nokaslr |