3. 调试环境搭建:QEMU + BusyBox 构建最小调试内核,配置 KGDB 与 KDB 双模式调试

在深入文件系统 I/O 栈异常定位之前,必须搭建一个可控、可复现、可打断点的内核调试环境。本章将引导你从零构建基于 QEMU 虚拟化平台的最小 Linux 内核调试系统,并同时启用 KGDB(内核调试器,通过串口/GDB 远程调试)与 KDB(内核内置调试器,直接键盘输入)两种模式,为后续章节的实战调试打下坚实基础。

学习目标:完成本章后,你将拥有一个可在宿主机上用 GDB 下断点、在 QEMU 控制台用 KDB 命令查看内核状态的完整调试环境,且整个系统镜像不超过 20MB。

3.1 环境概览与组件选择

最小调试系统由四个核心组件构成,其关系如下图所示:

最小调试内核系统组件关系 宿主机 GDB (x86_64 架构) QEMU 虚拟机 (模拟硬件) 调试内核 (vmlinux + System.map) BusyBox 根文件系统 (initramfs) 调试通道: KGDB 通过 QEMU 的 -serial tcp:... 参数建立 TCP 连接;KDB 通过 QEMU 的 -serial stdio 映射到终端键盘。
组件 版本/来源 作用
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 验证调试环境是否正常工作

完成上述配置后,执行以下验证步骤确认双模式均可用:

  1. 验证 KGDB 连接:启动 QEMU 后,在 GDB 中执行 target remote :1234,应看到内核停止在 kgdb_breakpoint() 函数。执行 bt 应显示完整的启动调用链。
  2. 验证 KDB 切换:在 QEMU 串口终端中执行 echo g > /proc/sysrq-trigger,应进入 [0]kdb> 提示符。输入 ps 应显示当前进程列表。
  3. 验证断点命中:在 GDB 中设置断点 b do_sys_open,然后 c 继续执行。在 QEMU 串口终端中执行 ls,GDB 应命中断点并暂停。
  4. 验证 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