1. 调试基础
1.1 内核调试的挑战与重要性
Linux 内核是操作系统的心脏,管理着硬件资源、进程调度、内存分配和文件系统。与用户空间程序不同,内核运行在最高特权级(Ring 0),一旦出现错误,往往导致系统崩溃(Kernel Panic)、数据损坏或安全漏洞。内核调试的难度远高于用户态调试,主要体现在以下几个方面:
- 无独立进程空间: 内核代码运行在共享的全局地址空间,一个野指针可能污染整个系统。
- 中断与并发: 内核需要处理硬件中断、软中断、多核并发,调试器本身可能改变时序,导致“Heisenbug”(一调试就消失的 Bug)。
- 资源受限: 嵌入式设备或早期启动阶段,没有网络、没有显示终端,甚至没有完整的文件系统。
- 实时性要求: 某些驱动(如网卡、存储)对延迟敏感,传统的断点调试可能破坏实时行为。
💡 核心观点: 内核调试不是“能不能”的问题,而是“用什么工具、在什么阶段、以什么视角”的问题。掌握多种调试手段的组合使用,是内核驱动开发者的必备技能。
1.2 常见调试工具概览
Linux 内核社区经过几十年的发展,沉淀了一套从“轻量级日志”到“重量级断点调试”的完整工具链。下表对比了四种最核心的调试工具:
| 工具 | 类型 | 侵入性 | 适用场景 | 学习曲线 |
|---|---|---|---|---|
| printk | 日志打印 | 低(仅增加日志) | 快速定位、状态跟踪、早期启动 | ★☆☆☆☆ |
| ftrace | 动态追踪 | 极低(内核内置) | 函数调用流、延迟分析、事件统计 | ★★★☆☆ |
| kprobes | 动态插桩 | 低(运行时插入) | 任意函数/指令的监控、无需重新编译 | ★★★★☆ |
| kgdb | 断点调试 | 高(暂停内核) | 复杂逻辑分析、死锁排查、内存损坏 | ★★★★★ |
1.2.1 printk — 最朴素的“printf”
printk 是内核版的 printf,可以将信息输出到内核日志缓冲区(通过 dmesg 查看)。它支持日志级别(如 KERN_ERR、KERN_DEBUG),用于控制消息的显示优先级。
// 基本用法
printk(KERN_INFO "my_driver: device initialized, irq=%d\n", irq_num);
printk(KERN_ERR "my_driver: DMA transfer failed, status=0x%x\n", status);
// 查看日志
$ dmesg | tail -20
$ dmesg -w # 实时跟踪
💡 实用技巧: 使用
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt 在文件头部定义,可以让所有 pr_info()/pr_err() 自动带上模块名前缀,便于日志过滤。
1.2.2 ftrace — 内核内置的追踪器
ftrace 是内核自带的动态追踪框架,可以在几乎零开销的情况下记录函数调用、中断上下文切换、调度事件等。它通过 tracefs 文件系统(挂载在 /sys/kernel/tracing)进行控制。
# 启用函数追踪
$ echo function > /sys/kernel/tracing/current_tracer
$ echo my_driver_function > /sys/kernel/tracing/set_ftrace_filter
$ cat /sys/kernel/tracing/trace
# 查看结果(示例输出)
# tracer: function
# TASK-PID CPU# TIMESTAMP FUNCTION
# myapp-1234 [001] 123.456: my_driver_function
# myapp-1234 [001] 123.457: sub_function_a
# myapp-1234 [001] 123.458: sub_function_b
1.2.3 kprobes — 动态插桩
kprobes 允许你在内核函数的任意位置(包括函数入口、出口、甚至指令级)动态插入探测点,而无需重新编译内核或模块。这对于生产环境下的问题定位非常有用。
// 使用 kprobes 监控 do_sys_open 的调用
#include <linux/kprobes.h>
static struct kprobe kp = {
.symbol_name = "do_sys_open",
};
static int handler_pre(struct kprobe *p, struct pt_regs *regs) {
pr_info("do_sys_open called, filename at %lx\n", regs->di);
return 0;
}
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags) {
// 后处理
}
int init_module(void) {
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
register_kprobe(&kp);
return 0;
}
1.2.4 kgdb — 内核级 GDB
kgdb 是 GDB 的内核变体,允许你像调试用户态程序一样设置断点、单步执行、查看变量和堆栈。它通常需要两台机器(或 QEMU 模拟)通过串口或网络连接:一台运行目标内核,另一台运行 GDB 客户端。
# 目标机(内核启动参数)
kgdboc=ttyS0,115200 kgdbwait
# 宿主机(GDB 客户端)
$ gdb vmlinux
(gdb) target remote /dev/ttyS0
(gdb) break my_driver_function
(gdb) continue
⚠️ 注意事项: kgdb 会暂停整个内核的执行,不适合生产环境。它主要用于开发阶段的复杂逻辑调试。在 SMP(多核)系统上,kgdb 默认只暂停当前 CPU,其他 CPU 可能仍在运行,需要配合
kgdb_breakpoint() 或硬件断点使用。
1.3 调试环境搭建:QEMU + 内核编译
为了安全、高效地学习内核调试,我们使用 QEMU 模拟器搭建虚拟目标机。这种方式可以随时重置、设置断点,不会影响物理机。
1.3.1 环境准备清单
| 组件 | 版本/说明 | 获取方式 |
|---|---|---|
| Linux 内核源码 | 推荐 5.10 LTS 或 6.1 LTS | git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git |
| QEMU | ≥ 6.0,支持 x86_64 和 ARM | sudo apt install qemu-system-x86 |
| 交叉编译工具链 | gcc, make, flex, bison, libncurses-dev | sudo apt install build-essential |
| BusyBox | 用于制作最小根文件系统 | wget https://busybox.net/downloads/busybox-1.36.0.tar.bz2 |
1.3.2 编译内核并启动 QEMU
以下步骤演示了如何编译一个支持调试符号的内核,并通过 QEMU 启动:
# 1. 配置内核(启用调试选项)
$ cd linux-6.1
$ make defconfig
$ make menuconfig
-> Kernel hacking --->
[*] Kernel debugging
[*] Compile the kernel with debug info
[*] KGDB: kernel debugger
[*] KGDB: use kgdb over the serial console
# 2. 编译内核(使用 -j 加速)
$ make -j$(nproc)
# 3. 制作最小根文件系统(使用 BusyBox)
$ cd ../busybox-1.36.0
$ make defconfig
$ make -j$(nproc)
$ make install
$ cd _install
$ mkdir proc sys dev etc
$ cat > init << "EOF"
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mdev -s
echo "Welcome to QEMU Kernel Debug Lab"
/bin/sh
EOF
$ chmod +x init
$ find . | cpio -o --format=newc > ../rootfs.cpio
# 4. 启动 QEMU(无图形界面,串口输出)
$ cd ../linux-6.1
$ qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd ../busybox-1.36.0/rootfs.cpio \
-append "console=ttyS0 nokaslr" \
-serial stdio \
-smp 2 \
-m 512M
1.3.3 验证调试环境
启动 QEMU 后,在宿主机上打开另一个终端,使用 GDB 连接目标内核:
# 宿主机终端 2
$ gdb vmlinux
(gdb) target remote localhost:1234 # QEMU 默认监听 TCP 1234 端口
(gdb) break start_kernel
(gdb) continue
# 此时 QEMU 终端会停在 start_kernel 入口,你可以单步、查看变量
(gdb) print init_task
(gdb) info registers
(gdb) step
💡 环境验证清单:
- QEMU 终端输出 "Welcome to QEMU Kernel Debug Lab" 表示根文件系统正常。
- GDB 能成功连接并停在断点处,表示 KGDB 通信正常。
- 执行
print命令能显示内核数据结构,表示调试符号加载成功。
1.4 本章小结
本章我们建立了内核调试的全局认知:
- 挑战: 共享地址空间、并发、资源受限、实时性要求。
- 工具矩阵: printk(快速日志)、ftrace(低开销追踪)、kprobes(动态插桩)、kgdb(断点调试)。
- 环境: 基于 QEMU + 内核编译 + KGDB 的沙盒调试环境,为后续章节的实战演练打下基础。