1. 调试基础

1.1 内核调试的挑战与重要性

Linux 内核是操作系统的心脏,管理着硬件资源、进程调度、内存分配和文件系统。与用户空间程序不同,内核运行在最高特权级(Ring 0),一旦出现错误,往往导致系统崩溃(Kernel Panic)、数据损坏或安全漏洞。内核调试的难度远高于用户态调试,主要体现在以下几个方面:

  • 无独立进程空间: 内核代码运行在共享的全局地址空间,一个野指针可能污染整个系统。
  • 中断与并发: 内核需要处理硬件中断、软中断、多核并发,调试器本身可能改变时序,导致“Heisenbug”(一调试就消失的 Bug)。
  • 资源受限: 嵌入式设备或早期启动阶段,没有网络、没有显示终端,甚至没有完整的文件系统。
  • 实时性要求: 某些驱动(如网卡、存储)对延迟敏感,传统的断点调试可能破坏实时行为。
💡 核心观点: 内核调试不是“能不能”的问题,而是“用什么工具、在什么阶段、以什么视角”的问题。掌握多种调试手段的组合使用,是内核驱动开发者的必备技能。
内核调试核心挑战 内核调试 无独立进程空间 全局地址空间共享 中断与并发 时序敏感、Heisenbug 资源受限 无网络/无终端/无FS 实时性要求 断点破坏实时行为 ⚠️ 内核 Bug 的后果:系统崩溃(Kernel Panic)、数据损坏、安全漏洞 调试策略:静态分析 + 动态追踪 + 远程调试 组合使用

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
QEMU + KGDB 调试架构 宿主机 (Host) GDB 客户端 (gdb vmlinux) 串口终端 (ttyS0) 源码浏览 / 断点管理 QEMU 模拟层 虚拟 CPU (x86_64/ARM) 虚拟串口 (chardev) 内存 / 中断控制器 目标内核 (Guest Kernel) KGDB 模块 (kgdboc=ttyS0) 串口数据流 KGDB 协议 🔑 关键参数:nokaslr(禁用内核地址随机化)、kgdboc(指定串口)、kgdbwait(等待调试器连接)

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
💡 环境验证清单:
  1. QEMU 终端输出 "Welcome to QEMU Kernel Debug Lab" 表示根文件系统正常。
  2. GDB 能成功连接并停在断点处,表示 KGDB 通信正常。
  3. 执行 print 命令能显示内核数据结构,表示调试符号加载成功。

1.4 本章小结

本章我们建立了内核调试的全局认知:

  • 挑战: 共享地址空间、并发、资源受限、实时性要求。
  • 工具矩阵: printk(快速日志)、ftrace(低开销追踪)、kprobes(动态插桩)、kgdb(断点调试)。
  • 环境: 基于 QEMU + 内核编译 + KGDB 的沙盒调试环境,为后续章节的实战演练打下基础。