调试是驱动开发最花时间 的环节。7.2.5 的调试手段比 4.0 丰富得多: 从最原始的 printk,到 dynamic debug、ftrace、perf、eBPF、KASAN、 lockdep、pstore,再到 QEMU + gdbstub 的单步源码级调试。
本章的主线:按"从粗到细"的顺序,逐层掌握调试武器。
粗 printk / dev_dbg / dynamic debug
│ /proc / /sys / debugfs
│ Oops 分析 / decode_stacktrace.sh
│ BUG_ON / WARN_ON / lockdep
│ ftrace / perf / eBPF / KASAN
细 QEMU gdbstub + vmlinux 源码级单步调试
21.1 GDB 调试器的用法
21.1.1 GDB 的基本用法
GDB 是用户空间程序的调试器,也可以(配合 gdbstub)调试内核。
编译带调试信息的程序:
bash
gcc -g -O0 -o myprog myprog.c
常用命令:
| 命令 | 简写 | 作用 |
|---|---|---|
break |
b |
设断点(函数名/行号/file:line) |
run |
r |
运行 |
continue |
c |
继续 |
next |
n |
单步(跳过函数) |
step |
s |
单步(进入函数) |
finish |
运行到当前函数返回 | |
print |
p |
打印变量/表达式 |
display |
每次停下自动打印 | |
backtrace |
bt |
调用栈 |
info locals / info args |
局部变量/参数 | |
list |
l |
看源码 |
watch |
观察点(值变化时中断) | |
x/16xb addr |
查看内存 | |
info registers |
寄存器 | |
set var x = 5 |
改变量 | |
thread apply all bt |
所有线程栈 |
调试带参数/核心转储的程序:
bash
gdb --args ./myprog arg1 arg2
gdb ./myprog core
gdb -p <pid> # attach 到运行中的进程
调试内核模块时的技巧 :把 .ko 的调试信息加到 GDB:
bash
(gdb) add-symbol-file mydrv.ko 0xffffffffc0000000
(地址从 /sys/module/mydrv/sections/.text 读,需 CONFIG_KALLSYMS。)
21.1.2 DDD 图形界面调试工具
DDD(Data Display Debugger) 是 GDB 的图形前端,用鼠标点断点、看数据结构:
bash
sudo apt install ddd
ddd ./myprog
| DDD 特色 | 说明 |
|---|---|
| 源码窗口 + 断点标记 | 点击行号设断点 |
| 数据显示窗口 | 把变量拖进去,图形化看链表/树 |
| 调用栈窗口 | 点击栈帧跳转 |
| 汇编窗口 | 源码/汇编对照 |
| 寄存器窗口 | 实时寄存器值 |
7.2.5 时代的替代品:
| 工具 | 说明 |
|---|---|
| VS Code + C/C++ 扩展 | 最流行的图形调试器 |
gdb TUI (gdb -tui) |
终端里的伪图形界面 |
| gdb-dashboard | gdb 的 Python 增强 |
| gef / pwndbg | 面向安全的 gdb 增强 |
21.2 Linux 内核调试
内核调试的三大支柱:
| 支柱 | 手段 |
|---|---|
| 日志 | printk、dynamic debug、earlycon、pstore |
| 追踪 | ftrace、perf、eBPF、tracepoints |
| 检查器 | KASAN、KCSAN、KMSAN、UBSAN、lockdep、kmemleak |
关键内核配置(调试用,生产环境慎用):
bash
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_DWARF5=y
CONFIG_GDB_SCRIPTS=y
CONFIG_FRAME_POINTER=y
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
CONFIG_DEBUG_FS=y
CONFIG_DYNAMIC_DEBUG=y
CONFIG_FTRACE=y
CONFIG_FUNCTION_TRACER=y
CONFIG_STACKTRACE=y
CONFIG_KASAN=y # 内存错误
CONFIG_KCSAN=y # 数据竞争
CONFIG_UBSAN=y # 未定义行为
CONFIG_PROVE_LOCKING=y # lockdep
CONFIG_LOCKDEP=y
CONFIG_DEBUG_ATOMIC_SLEEP=y # 原子上下文睡眠
CONFIG_DEBUG_MUTEXES=y
CONFIG_DEBUG_SPINLOCK=y
CONFIG_KMEMLEAK=y # 内存泄漏
CONFIG_PSTORE=y # 崩溃日志持久化
内核源码自带 kernel/configs/debug.config 和 kernel/configs/x86_debug.config, 可以合并进你的配置:
bash
make -C "$KSRC" O="$KBUILD" x86_64_defconfig
scripts/kconfig/merge_config.sh -O "$KBUILD" \
"$KBUILD/.config" "$KSRC/kernel/configs/debug.config"
make -C "$KSRC" O="$KBUILD" olddefconfig
21.3 内核打印信息------printk()
printk 是驱动调试的第一武器。7.2.5 的 printk 比 4.0 强大很多。
21.3.1 日志级别
bash
printk(KERN_EMERG "system is unusable\n"); /* 0 */
printk(KERN_ALERT "action must be taken\n"); /* 1 */
printk(KERN_CRIT "critical condition\n"); /* 2 */
printk(KERN_ERR "error condition\n"); /* 3 */
printk(KERN_WARNING "warning\n"); /* 4 */
printk(KERN_NOTICE "normal but significant\n");/* 5 */
printk(KERN_INFO "informational\n"); /* 6 */
printk(KERN_DEBUG "debug-level\n"); /* 7 */
现代写法(推荐):
cpp
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/printk.h>
pr_err("failed to open: %d\n", ret);
pr_warn("retrying\n");
pr_info("device ready\n");
pr_debug("register = 0x%08x\n", val); /* 默认不打印,见 dynamic debug */
/* 设备相关(自动带设备名前缀) */
dev_err(dev, "probe failed: %d\n", ret);
dev_warn(dev, "unexpected value\n");
dev_info(dev, "registered as %s\n", name);
dev_dbg(dev, "state = %d\n", state);
7.2.5 的重要变化:
| 4.0 | 7.2.5 |
|---|---|
多行 printk 可能被其它 CPU 插入 |
printk 有 per-cpu 缓冲,输出原子性更好 |
printk_ratelimit() |
printk_ratelimited() 系列宏 |
| 无时间戳格式选项 | printk.time=、printk.devkmsg= |
| 无结构化日志 | printk 支持 %p 扩展、print_hex_dump() |
| 无序号 | CONFIG_PRINTK_CALLER 打印调用者 |
实用打印函数:
| 函数 | 作用 |
|---|---|
pr_info() / dev_info() |
信息 |
pr_err() / dev_err() |
错误 |
dev_err_probe(dev, err, fmt, ...) |
7.2.5 推荐 :只在非 -EPROBE_DEFER 时打印 |
print_hex_dump(level, prefix, prefix_type, rowsize, groupsize, buf, len, ascii) |
十六进制转储 |
print_hex_dump_debug() |
只在 dynamic debug 打开时输出 |
WARN_ON() / WARN_ON_ONCE() |
警告(见 21.7) |
dump_stack() |
打印调用栈 |
21.3.2 动态调试(dynamic debug)
这是 7.2.5 相比 4.0 最重要的调试增强 :不用重编译就能打开 pr_debug()/dev_dbg()。
bash
CONFIG_DYNAMIC_DEBUG=y
CONFIG_DEBUG_FS=y
bash
# 打开某个模块的所有 pr_debug
echo 'module mydrv +p' > /sys/kernel/debug/dynamic_debug/control
# 打开某个文件的所有调试输出
echo 'file mydrv.c +p' > /sys/kernel/debug/dynamic_debug/control
# 打开匹配某行的
echo 'file mydrv.c line 42 +p' > /sys/kernel/debug/dynamic_debug/control
# 查看所有被修改的
cat /sys/kernel/debug/dynamic_debug/control | grep '=p'
# 也可以用命令行
# 内核参数:dyndbg="module mydrv +p"
好处 :pr_debug() 在未打开时零开销 (编译进一个 _DPRINTK_FLAGS_NONE 分支)。
21.3.3 控制台与日志
bash
# 控制台日志级别(只打印 < 该级别的消息)
echo 8 > /proc/sys/kernel/printk
cat /proc/sys/kernel/printk
# 用户空间读内核日志
dmesg -w # 实时跟踪
dmesg -T # 人类可读时间
journalctl -k # systemd 系统
# 串口控制台参数
console=ttyAMA0,115200 loglevel=7
pstore:崩溃后日志持久化(重启不丢):
bash
CONFIG_PSTORE=y
CONFIG_PSTORE_CONSOLE=y
CONFIG_PSTORE_RAM=y
bash
ls /sys/fs/pstore/
cat /sys/fs/pstore/dmesg-ramoops-0
21.4 DEBUG_LL 和 EARLY_PRINTK
(详细见第 20.5 节,这里讲调试用法。)
问题 :内核在串口驱动加载之前崩溃了,怎么办?
| 机制 | 用途 | 命令/配置 |
|---|---|---|
earlycon |
早期控制台 | earlycon=pl011,mmio32,0x9000000 |
keep_bootcon |
保留早期控制台 | 内核参数 |
DEBUG_LL(ARM32) |
汇编级输出 | CONFIG_DEBUG_LL + DEBUG_UART_PHYS |
EARLY_PRINTK(x86) |
早期打印 | CONFIG_EARLY_PRINTK + earlyprintk |
boot_delay= |
启动延迟 | 方便看清每行 |
QEMU 实例:
bash
qemu-system-aarch64 -machine virt -nographic \
-kernel Image \
-append "console=ttyAMA0 earlycon=pl011,mmio32,0x9000000 keep_bootcon"
观察 :如果串口驱动有问题,earlycon 仍然能输出, 从而定位"卡在哪一步"。
21.5 使用 "/proc"
/proc 是内核暴露给用户空间的运行时信息接口。
| 文件 | 内容 |
|---|---|
/proc/interrupts |
每个 CPU 的中断计数 |
/proc/iomem |
物理内存/IO 映射 |
/proc/ioports |
IO 端口 |
/proc/meminfo |
内存统计 |
/proc/slabinfo |
slab 分配器 |
/proc/vmallocinfo |
vmalloc 区域 |
/proc/modules |
已加载模块 |
/proc/kallsyms |
所有内核符号(需 CONFIG_KALLSYMS) |
/proc/devices |
已注册的字符/块设备主号 |
/proc/cmdline |
内核命令行 |
/proc/version |
内核版本 |
/proc/config.gz |
内核配置(需 CONFIG_IKCONFIG_PROC) |
/proc/device-tree/ |
设备树 |
/proc/self/maps |
进程内存映射 |
/proc/<pid>/stack |
内核栈(需权限) |
/proc/timer_list |
定时器列表 |
驱动可以创建自己的 /proc 条目(新代码更推荐 debugfs):
cpp
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
static int my_show(struct seq_file *m, void *v)
{
struct my_dev *d = m->private;
seq_printf(m, "state=%d count=%u\n", d->state, d->count);
return 0;
}
static int my_open(struct inode *inode, struct file *file)
{
return single_open(file, my_show, PDE_DATA(inode));
}
/* 7.2.5 用 struct proc_ops(不再是 file_operations) */
static const struct proc_ops my_proc_ops = {
.proc_open = my_open,
.proc_read = seq_read,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
proc_create("mydrv", 0444, NULL, &my_proc_ops);
7.2.5 变化 :struct file_operations → struct proc_ops (成员带 proc_ 前缀)。
sysfs 与 debugfs(更现代的选择):
bash
ls /sys/kernel/debug/
cat /sys/kernel/debug/clk/clk_summary
cat /sys/kernel/debug/regmap/
cat /sys/kernel/debug/gpio
cat /sys/kernel/debug/dmaengine/
21.6 Oops
Oops 是内核遇到非法操作时的"崩溃报告" ,不是 panic(除非 panic_on_oops=1)。
典型 Oops 内容:
bash
BUG: unable to handle page fault for address: 0000000000000010
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
PGD 0 P4D 0
Oops: 0000 [#1] SMP
CPU: 0 PID: 1234 Comm: insmod Tainted: G OE 7.2.5
RIP: 0010:my_probe+0x2a/0x100 [mydrv]
Code: 48 8b 47 10 ...
RSP: 0018:ffffc900003cfd10 EFLAGS: 00010246
RAX: 0000000000000000 RBX: ffff888100a4c000 RCX: ...
Call Trace:
<TASK>
my_probe+0x2a/0x100 [mydrv]
platform_probe+0x4a/0x90
really_probe+0x1c0/0x430
__driver_probe_device+0x11a/0x1f0
driver_probe_device+0x1e/0x90
__device_attach_driver+0x9e/0x120
...
</TASK>
分析步骤:
- 看 RIP (出错的指令地址)和 Call Trace(调用链)。
- 用
scripts/decode_stacktrace.sh把地址翻译成函数+行号:
bash
./scripts/decode_stacktrace.sh vmlinux < oops.txt
- 用
scripts/faddr2line把my_probe+0x2a/0x100翻译成源码行:
bash
./scripts/faddr2line vmlinux my_probe+0x2a/0x100
- 看
Code:行的字节,配合objdump -d定位指令:
bash
objdump -dS --start-address=0x... --stop-address=0x... vmlinux
- 常见原因:空指针 (
0000000000000010=NULL + 0x10)、 野指针 、use-after-free 、栈溢出。
打开更多信息:
bash
CONFIG_KALLSYMS=y
CONFIG_DEBUG_INFO=y
CONFIG_FRAME_POINTER=y
echo 1 > /proc/sys/kernel/panic_on_oops # 崩溃即重启(配合 pstore)
21.7 BUG_ON() 和 WARN_ON()
| 宏 | 行为 | 使用场景 |
|---|---|---|
BUG() |
立即崩溃(oops/panic) | 不可能发生且继续会破坏数据 |
BUG_ON(cond) |
条件为真时崩溃 | 同上 |
WARN_ON(cond) |
打印警告 + 栈,继续运行 | 可疑但可继续 |
WARN_ON_ONCE(cond) |
只警告一次 | 高频路径 |
WARN(cond, fmt, ...) |
带格式化信息 | |
panic_on_warn |
WARN 变成 panic |
测试用 |
cpp
if (WARN_ON(!priv->regmap))
return -EINVAL;
/* 7.2.5 推荐:用 WARN 而非 BUG,除非真的无法继续 */
WARN_ON_ONCE(d->state != EXPECTED);
7.2.5 的观点 :尽量少用 BUG_ON() 。现代内核倾向于用 WARN_ON() + 错误返回,因为 BUG_ON() 会让整台机器不可用(对生产系统是灾难)。 CONFIG_BUG=n 时 BUG_ON() 甚至会被优化掉。
dump_stack():只想打印调用栈、不崩溃:
bash
dump_stack();
lockdep(锁依赖检查):检测死锁和锁顺序错误:
bash
CONFIG_PROVE_LOCKING=y
CONFIG_LOCKDEP=y
CONFIG_DEBUG_LOCK_ALLOC=y
bash
echo 1 > /proc/sys/kernel/lock_stat
cat /proc/lockdep_stats
cat /proc/lockdep
DEBUG_ATOMIC_SLEEP:检测"在原子上下文睡眠":
CONFIG_DEBUG_ATOMIC_SLEEP=y
KASAN:检测越界/use-after-free:
bash
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y
bash
BUG: KASAN: slab-out-of-bounds in my_read+0x45/0x100 [mydrv]
Read of size 4 at addr ffff888100a4c010 by task cat/1234
KCSAN :检测数据竞争。KMSAN:检测未初始化内存。
21.8 strace
strace 跟踪用户空间程序的系统调用,是"驱动接口是否正常"的照妖镜。
bash
strace ./myprog
strace -f ./myprog # 跟踪子进程
strace -e openat,read,write,ioctl ./myprog
strace -o log.txt ./myprog
strace -T ./myprog # 每个调用耗时
strace -p <pid> # attach
调试驱动的典型用法:
bash
strace -e ioctl ./my_i2c_test
# ioctl(3, I2C_RDWR, 0x7ffd...) = 0
# 如果返回 -1 EPERM/EINVAL,看是哪个 ioctl 失败
相关工具:
| 工具 | 作用 |
|---|---|
strace |
系统调用 |
ltrace |
库函数调用 |
perf trace |
系统调用(带时间戳,开销更低) |
bpftrace |
用 eBPF 追踪任意函数 |
7.2.5 的现代替代 :perf trace 和 bpftrace 开销更低、能力更强:
bash
perf trace -e 'syscalls:sys_enter_ioctl' ./myprog
bpftrace -e 'tracepoint:syscalls:sys_enter_ioctl { printf("%s %x\n", comm, args->cmd); }'
21.9 KGDB
KGDB 让 GDB 能像调试用户程序一样调试内核(断点、单步、看变量)。
21.9.1 配置
bash
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_KDB=y
CONFIG_DEBUG_INFO=y
CONFIG_FRAME_POINTER=y
CONFIG_GDB_SCRIPTS=y
CONFIG_MAGIC_SYSRQ=y
21.9.2 用 QEMU 的 gdbstub(推荐,比串口 KGDB 简单)
这是无开发板调试内核的最佳方式:
bash
# 宿主机:启动 QEMU,开放 gdbstub 端口 1234,并在启动时暂停
qemu-system-x86_64 \
-kernel "$KBUILD/arch/x86/boot/bzImage" \
-append "console=ttyS0 nokaslr" \
-s -S \
-nographic
# 另一个终端:用 GDB 连接
gdb "$KBUILD/vmlinux"
(gdb) target remote :1234
(gdb) hbreak start_kernel
(gdb) continue
(gdb) break my_probe # 驱动断点
(gdb) continue
(gdb) bt
(gdb) p *dev
(gdb) list
-s = -gdb tcp::1234,-S = 启动时暂停。
关键点:
nokaslr让内核地址固定,断点才稳定。- 硬件断点用
hbreak(内核里软件断点常常无效)。 - 加载内核自带的 GDB 脚本(
scripts/gdb/)增强功能:
bash
(gdb) source "$KSRC/vmlinux-gdb.py"
(gdb) lx-version
(gdb) lx-dmesg
(gdb) lx-lsmod
(gdb) lx-ps
(gdb) lx-symbols # 加载所有模块符号
(gdb) lx-device-list
scripts/gdb/linux/ 下有一整套命令(lx-dmesg、lx-lsmod、lx-ps、 lx-device-list、lx-tasks 等),驱动调试非常有用。
21.9.3 串口 KGDB(真实硬件)
bash
# 内核参数
kgdboc=ttyS0,115200 kgdbwait
# 宿主
gdb vmlinux
(gdb) target remote /dev/ttyUSB0
21.9.4 KDB(无 GDB 的内核调试器)
CONFIG_KGDB_KDB 提供 KDB,在控制台按 SysRq-G 进入:
bash
kdb> help
kdb> bt
kdb> md 0xffffffff81000000
kdb> go
Magic SysRq 常用键 (echo h > /proc/sysrq-trigger 看帮助):
| 键 | 作用 |
|---|---|
b |
立即重启 |
c |
触发 crash(测试 kdump) |
d |
显示所有锁 |
e |
给所有进程发 SIGTERM |
g |
进入 KDB |
h |
帮助 |
l |
所有 CPU 的栈 |
m |
内存信息 |
p |
寄存器 |
s |
同步磁盘 |
t |
所有任务栈 |
w |
阻塞任务 |
21.10 使用仿真器调试内核
(QEMU 是"仿真器"的代表,见 21.9.2。)
| 仿真器 | 说明 |
|---|---|
| QEMU | 全系统仿真,-s -S + gdbstub,首选 |
| QEMU user mode | 只仿真用户程序(qemu-arm ./prog) |
| Renode | 面向嵌入式的外设级仿真 |
| Spike | RISC-V 官方 ISS |
| Simics(商业) | 全系统仿真 |
QEMU 调试进阶:
bash
# 记录/回放(逆向调试)
qemu-system-x86_64 -icount shift=auto,rr=record,rrfile=/tmp/replay.bin ...
qemu-system-x86_64 -icount shift=auto,rr=replay,rrfile=/tmp/replay.bin ...
# 单步执行到某地址
(gdb) hbreak *0xffffffff81234567
(gdb) stepi
(gdb) info registers
观察外设寄存器 :QEMU 的 -d 选项可以打印各种事件:
qemu-system-x86_64 -d int,cpu_reset -D /tmp/qemu.log ...
21.11 应用程序调试
驱动工程师也常要调试用户空间的测试程序:
| 工具 | 作用 |
|---|---|
gdb / gdb -tui |
源码级调试 |
valgrind |
内存错误/泄漏 |
AddressSanitizer(-fsanitize=address) |
编译期插桩的内存检查 |
UndefinedBehaviorSanitizer(-fsanitize=undefined) |
未定义行为 |
ThreadSanitizer(-fsanitize=thread) |
数据竞争 |
perf |
性能分析 |
gprof / callgrind |
函数级剖析 |
strace / ltrace |
调用跟踪 |
bash
# ASan 示例
gcc -fsanitize=address -g -o myprog myprog.c
./myprog # 越界/UAF 会打印详细报告
# valgrind
valgrind --leak-check=full ./myprog
# perf 剖析
perf record -g ./myprog && perf report
驱动测试程序常见问题 :open() 权限、ioctl 命令号编码错误、 未处理 EINTR、缓冲区大小与内核不一致。
21.12 Linux 性能监控与调优工具
| 工具 | 层面 | 作用 |
|---|---|---|
top / htop |
进程 | CPU/内存占用 |
perf |
全栈 | 采样剖析、tracepoint、PMU |
ftrace |
内核 | 函数/调度/中断追踪 |
bpftrace / bcc |
全栈 | eBPF 动态追踪 |
ftrace + trace-cmd |
内核 | 事件记录与可视化 |
LTTng |
全栈 | 低开销追踪 |
vmstat / iostat |
系统 | IO/内存 |
powertop |
电源 | 唤醒源/耗电 |
perf lock |
内核 | 锁竞争 |
perf sched |
内核 | 调度延迟 |
slabtop |
内核 | slab 使用 |
pmap / smem |
进程 | 内存映射 |
21.12.1 ftrace
bash
cd /sys/kernel/debug/tracing
# 函数追踪
echo function > current_tracer
echo my_probe > set_ftrace_filter
echo 1 > tracing_on
cat trace
echo 0 > tracing_on
# 函数图(看调用链)
echo function_graph > current_tracer
cat trace
# 追踪事件(中断、调度、GPIO、regmap 等)
echo 1 > events/irq/enable
echo 1 > events/sched/sched_switch/enable
# tracepoint 列表
ls events/
7.2.5 的 tracepoint 覆盖极广 :gpio、regmap、i2c、spi、dma、 irq、sched、power、usb......驱动调试必备。
21.12.2 perf
bash
# 采样剖析
perf top
perf record -g -a sleep 10 && perf report
# 追踪系统调用
perf trace ./myprog
# 硬件计数器
perf stat -e cycles,instructions,cache-misses ./myprog
# 锁竞争
perf lock record && perf lock report
# 调度延迟
perf sched record && perf sched latency
21.12.3 eBPF / bpftrace
bash
# 追踪 ioctl
bpftrace -e 'tracepoint:syscalls:sys_enter_ioctl { printf("%s cmd=%x\n", comm, args->cmd); }'
# 追踪内核函数耗时
bpftrace -e 'kprobe:my_hw_xfer { @start[tid] = nsecs; }
kretprobe:my_hw_xfer /@start[tid]/ {
@ns = hist(nsecs - @start[tid]); delete(@start[tid]); }'
7.2.5 的 eBPF 能力 :kprobe、kretprobe、tracepoint、perf event、fentry/fexit、 BTF、CO-RE,几乎可以追踪任何内核行为,是 4.0 时代完全不具备的。
21.12.4 内核自带脚本
bash
./scripts/checkpatch.pl --no-tree -f mydrv.c # 代码风格检查
./scripts/decode_stacktrace.sh vmlinux < oops.txt
./scripts/faddr2line vmlinux my_probe+0x2a/0x100
./scripts/checkstack.pl # 栈使用量
./scripts/get_maintainer.pl mydrv.c # 找维护者
./scripts/dtc/dt-extract-compatibles # 提取 compatible
21.13 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
用 printk 不带级别 |
编译告警/默认级别 | 用 pr_*/dev_* |
dev_dbg 没输出 |
以为代码没执行 | 用 dynamic debug 打开 |
| Oops 地址看不懂 | 无符号 | 开 CONFIG_KALLSYMS + DEBUG_INFO |
用 BUG_ON() 做参数检查 |
整机崩溃 | 用 WARN_ON() + 返回错误 |
| gdb 断点打不上 | 软件断点无效 | 用 hbreak,加 nokaslr |
| KGDB 连不上 | 串口/端口配置错 | 检查 kgdboc= 和 QEMU -s |
strace 看不到 ioctl 参数 |
命令号难读 | 对照 _IO* 宏解码 |
ftrace 无输出 |
未开 tracing_on 或 filter |
检查 set_ftrace_filter |
perf 权限不足 |
perf_event_paranoid |
echo 1 > /proc/sys/kernel/perf_event_paranoid |
proc_create 用 file_operations |
编译错误 | 用 struct proc_ops |
| KASAN 报告被淹没 | 日志太乱 | 只保留第一条报告 |
| lockdep 误报 | 假死锁 | 检查是否真的持锁顺序错误 |
21.14 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| GDB 用户程序 | 学习断点/单步 | b/n/p/bt |
| DDD 图形调试 | 学习图形界面 | 数据窗口看链表 |
| printk 级别 | 学习日志 | dmesg 输出 |
| dynamic debug | 学习零开销调试 | dyndbg/control |
| earlycon | 学习早期日志 | 启动最开头输出 |
/proc/interrupts |
学习运行时信息 | 中断计数 |
| 制造 Oops | 学习崩溃分析 | decode_stacktrace.sh |
| WARN_ON | 学习非致命检查 | 栈+继续运行 |
| KASAN | 学习内存错误 | 越界报告 |
| lockdep | 学习死锁检测 | 锁顺序报告 |
| strace | 学习系统调用 | ioctl 跟踪 |
| QEMU + GDB | 内核源码级调试 | hbreak+lx-dmesg |
| ftrace | 学习函数追踪 | function_graph |
| perf | 学习性能剖析 | perf report |
| bpftrace | 学习动态追踪 | 自定义探针 |
21.15 总结
- 调试按"从粗到细":printk → /proc/debugfs → Oops 分析 → 检查器 → 追踪 → GDB。
- printk 仍是第一武器 ;7.2.5 用
pr_*/dev_*宏,dev_err_probe()是 probe 的推荐错误打印。 - dynamic debug 让
pr_debug()/dev_dbg()零开销且可运行时打开。 - earlycon 是无重编译的早期调试;ARM32 还有汇编级
DEBUG_LL。 - Oops 分析 用
scripts/decode_stacktrace.sh和scripts/faddr2line。 WARN_ON()优先于BUG_ON();用 KASAN/KCSAN/lockdep 主动发现 bug。- strace 看用户态系统调用;ftrace/perf/bpftrace 看内核行为。
- QEMU + gdbstub(
-s -S)是无开发板调试内核的最佳方式 , 配合scripts/gdb/的lx-*命令可以像调试用户程序一样调试内核。 - KGDB/KDB 在真实硬件上用串口调试内核;SysRq 提供运行时"急救"手段。
- 原书的 GDB/DDD、printk、DEBUG_LL/EARLY_PRINTK、/proc、Oops、BUG_ON/WARN_ON、 strace、KGDB、仿真器调试、应用调试、性能工具在本章全部覆盖, 并补充了 7.2.5 的 dynamic debug、ftrace、perf、eBPF、KASAN、lockdep。