内核调试技术:从printk到KGDB的完整武器库
驱动开发最耗时的不是写代码,是调bug。内核态没有gdb直接挂,没有printf随便打,一个空指针oops就能让你对着Call Trace发呆半小时。本文基于Linux 6.8内核,把printk、动态调试、ftrace、kprobes、KGDB、oops分析、内存调试这些工具串一遍,每个都给操作步骤和代码示例,最后一张对比表帮你按场景选工具。
一、printk调试
printk是内核调试的瑞士军刀,上手零门槛,但用好了也有讲究。
日志级别
内核定义7个日志级别,数值越小优先级越高。
| 级别 | 值 | | 用途 |
|:-----|:---|:---|:-----|
| KERN_EMERG | pr_emerg | 0 | 系统不可用,panic前最后一句话 |
| KERN_ALERT | pr_alert | 1 | 必须立即处理 |
| KERN_CRIT | pr_crit | 2 | 严重条件 |
| KERN_ERR | pr_err | 3 | 错误条件 |
| KERN_WARNING | pr_warn | 4 | 警告 |
| KERN_NOTICE | pr_notice | 5 | 正常但值得注意 |
| KERN_INFO | pr_info | 6 | 信息 |
| KERN_DEBUG | pr_debug | 7 | 调试信息 |
控制台只显示优先级数值低于console_loglevel的消息。默认console_loglevel。(KERN_WARNING),所以pr_info和pr_debug默认不会打印到控制台,但dmesg能看到。
便利。
/* 推荐用便利宏,比printk(KERN_XXX ...)简*/
pr_emerg("system on fire!\n");
pr_alert("immediate action required\n");
pr_crit("critical condition\n");
pr_err("something went wrong: %d\n", ret);
pr_warn("suspicious value: %u\n", val);
pr_notice("event occurred\n");
pr_info("initialized successfully\n");
pr_debug("buf=%p len=%zu\n", buf, len);
pr_debug比较特殊——没开CONFIG_DYNAMIC_DEBUG时它等价于printk(KERN_DEBUG ...),开了之后走动态调试子系统,可以运行时开关。
pr_fmt格式化前缀
一堆驱动的printk输出混在一起,分不清谁打的。pr_fmt给所有printk加统一前缀。
/* 在文件最开头定义,注意在include之前 */
#define pr_fmt(fmt) "%s: " fmt, __func__
#include <linux/module.h>
#include <linux/init.h>
static int __init my_init(void)
{
pr_info("module loaded\n");
/* 输出:my_init: module loaded */
return 0;
}
__func__自动填当前函数名。也可以用模块名。
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
/* 输出:my_module: module loaded */
dmesg查看
# 查看全部内核日志
dmesg
# 实时跟踪(类似tail -f。dmesg -w
# 按级别过滤:只看err和warn
dmesg -l err,warn
# 按级别过滤:只看debug级别
dmesg -l debug
# 按设施过。dmesg -f daemon,kern
# 时间戳转人类可读
dmesg -T
# 清空日志缓冲。sudo dmesg -c
loglevel内核参数
# 查看当前控制台日志级别cat /proc/sys/kernel/printk
# 输出四个数:当前日志级别 默认日志级别 最低日志级别启动时日志级别# 例如。 4 1 7
# 临时修改(重启失效)
echo 8 > /proc/sys/kernel/printk
# 8表示所有级别的printk都输出到控制。
# 启动参数永久修改
# 在GRUB的linux行添加:
loglevel=7
# 调试时常用:让所有printk都打到控制台
loglevel=8
开发阶段建议把loglevel设到8,所有printk直接在控制台可见,省得切终端看dmesg。
二、动态调试(Dynamic Debug。
printk的问题是:调试时加一堆pr_debug,发版时得删掉或注释掉,下次出bug又得加回来。动态调试解决了这个问题——pr_debug/dev_dbg留在代码里不动,运行时决定开不开销
基本操作
# 开启动态调试需要内核配置# CONFIG_DYNAMIC_DEBUG=y
# 开启某个文件的所有pr_debug
echo 'file drivers/i2c/i2c-core.c +p' > /sys/kernel/debug/dynamic_debug/control
# 开启某个模块的所有pr_debug
echo 'module my_driver +p' > /sys/kernel/debug/dynamic_debug/control
# 开启某个函数的pr_debug
echo 'func my_probe +p' > /sys/kernel/debug/dynamic_debug/control
# 开启某一。echo 'file my_drv.c line 42 +p' > /sys/kernel/debug/dynamic_debug/control
# 按级别过。echo 'module my_driver level=7 +p' > /sys/kernel/debug/dynamic_debug/control
# 关闭:把+p改成-p
echo 'file drivers/i2c/i2c-core.c -p' > /sys/kernel/debug/dynamic_debug/control
过滤条件组合
# 同时匹配多个条件(AND关系。echo 'file my_drv.c func my_probe +p' > /sys/kernel/debug/dynamic_debug/control
# 用OR关系:写多条命令
echo 'file my_drv.c +p' > /sys/kernel/debug/dynamic_debug/control
echo 'file my_helper.c +p' > /sys/kernel/debug/dynamic_debug/control
# 用通配置echo 'file drivers/net/ethernet/intel/* +p' > /sys/kernel/debug/dynamic_debug/control
查看已启用的调试语句
# 查看所有已启用的pr_debug
cat /sys/kernel/debug/dynamic_debug/control | grep "=p"
# 查看某个文件的调试状态cat /sys/kernel/debug/dynamic_debug/control | grep my_drv.c
# 输出格式。# my_drv.c:42 [my_driver]my_probe =p "probe called for device %s\n"
# 文件:行号 [模块]函数。状态格式字符号```
### 启动时开销
```bash
# 内核启动参数,在GRUB中添。dyndbg="file drivers/i2c/* +p"
# 模块加载时开销modprobe my_driver dyndbg="+p"
动态调试的开销极低——关闭时pr_debug只是一个条件跳转,几乎为零。所以放心在代码里撒pr_debug,不用的时候关掉就行。
三、ftrace跟踪
ftrace是Linux内核内置的跟踪框架,不用装任何工具,挂载debugfs就能用。它能跟踪函数调用、记录调用图、统计执行时间,是性能分析和调用链梳理的利器。
function tracer:跟踪函数调试
# 确认debugfs已挂。mount -t debugfs none /sys/kernel/debug
# 查看可用的tracer
cat /sys/kernel/debug/tracing/available_tracers
# 输出:function function_graph blk mmiotrace wakeup_rt wakeup irqsoff ...
# 选择function tracer
echo function > /sys/kernel/debug/tracing/current_tracer
# 可选:只跟踪特定函数echo my_probe > /sys/kernel/debug/tracing/set_ftrace_filter
# 可选:排除特定函数
echo schedule > /sys/kernel/debug/tracing/set_ftrace_notrace
# 开启跟。echo 1 > /sys/kernel/debug/tracing/tracing_on
# 查看跟踪结果
cat /sys/kernel/debug/tracing/trace
# 关闭跟踪
echo 0 > /sys/kernel/debug/tracing/tracing_on
# 清空缓冲。echo > /sys/kernel/debug/tracing/trace
trace输出示例。
# tracer: function
#
# TASK-PID CPU# TIMESTAMP FUNCTION
# | | | | |
<...>-1234 [000] 1234.567890: my_probe <-pci_device_probe
<...>-1234 [000] 1234.567891: my_init_hw <-my_probe
<...>-1234 [000] 1234.567892: my_configure <-my_init_hw
function_graph tracer:调用图
function_graph比function更直观——它展示函数的调用层级和返回关系。
# 切换到function_graph tracer
echo function_graph > /sys/kernel/debug/tracing/current_tracer
# 限制跟踪范围(强烈建议,否则输出爆炸。echo my_probe > /sys/kernel/debug/tracing/set_graph_function
# 开启跟。echo 1 > /sys/kernel/debug/tracing/tracing_on
# 触发操作后查看cat /sys/kernel/debug/tracing/trace
输出示例。
0) | my_probe() {
0) | my_init_hw() {
0) 0.500 us | my_configure();
0) 1.200 us | }
0) 2.100 us | }
大括号表示调用层级,时间表示函数执行耗时。一眼就能看出哪个函数慢。
tracepoint:静态跟踪点
tracepoint是内核预定义的跟踪点,比function tracer开销更低,语义更明确。
# 查看可用的tracepoint
ls /sys/kernel/debug/tracing/events/
# 常用目录。# irq/ - 中断事件
# sched/ - 调度事件
# net/ - 网络事件
# block/ - 块设备事。# syscalls/ - 系统调用
# 启用某个tracepoint
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
# 启用整个子系。echo 1 > /sys/kernel/debug/tracing/events/sched/enable
# 查看结果
cat /sys/kernel/debug/tracing/trace
# 关闭
echo 0 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
trace-cmd工具
直接操作ftrace的sysfs接口比较繁琐,trace-cmd封装了常用操作:
# 安装
sudo apt install trace-cmd
# 跟踪函数调用(等价于function tracer。sudo trace-cmd record -p function -l my_probe
# 跟踪调用。sudo trace-cmd record -p function_graph -l my_probe
# 跟踪tracepoint
sudo trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit
# 查看结果
sudo trace-cmd report
# 录制5。sudo trace-cmd record -p function -l my_probe sleep 5
ftrace操作流程
四、kprobes探针
ftrace只能跟踪函数入口和出口,kprobes能在函数的任意位置插入断点,还能读取和修改寄存器、局部变量。kprobes分两种:kprobe(函数入口,任意指令处触发)和kretprobe(函数返回时触发)。
基本原理
kprobes把目标指令替换成断点指令(x86上是int3),CPU执行到这里触发异常,kprobes的handler被调用。handler执行完后恢复原始指令继续执行。
register_kprobe/unregister_kprobe
/* kprobe示例:在do_sys_open入口打印信息(Linux 6.8*/
#include <linux/module.h>
#include <linux/kprobes.h>
static int kp_handler(struct kprobe *p, struct pt_regs *regs)
{
pr_info("do_sys_open hit: filename_ptr=0x%lx\n",
regs_get_kernel_argument(regs, 0));
return 0;
}
static struct kprobe kp = {
.symbol_name = "do_sys_open",
.pre_handler = kp_handler,
};
static int __init kp_init(void)
{
int ret;
ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("register_kprobe failed: %d\n", ret);
return ret;
}
pr_info("kprobe registered at %pS\n", kp.addr);
return 0;
}
static void __exit kp_exit(void)
{
unregister_kprobe(&kp);
pr_info("kprobe unregistered\n");
}
module_init(kp_init);
module_exit(kp_exit);
MODULE_LICENSE("GPL");
regs_get_kernel_argument(regs, n)获取第n个参数。x86_64调用约定:rdi、rsi、rdx、rcx、r8、r9依次是第1~6个参数。
kretprobe:函数返回时触发
/* kretprobe示例:监控kmalloc返回值(Linux 6.8*/
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/slab.h>
static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
/* 返回值在x86_64的rax寄存器中 */
unsigned long retval = regs_return_value(regs);
pr_info("kmalloc returned: %pS (size info in entry handler)\n",
(void *)retval);
return 0;
}
static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
/* 获取kmalloc的第一个参数:size */
unsigned long size = regs_get_kernel_argument(regs, 0);
pr_info("kmalloc called: size=%lu\n", size);
return 0;
}
static struct kretprobe krp = {
.handler = ret_handler,
.entry_handler = entry_handler,
.maxactive = 20, /* 最大并发实例数 */
};
static int __init krp_init(void)
{
int ret;
krp.kp.symbol_name = "kmalloc";
ret = register_kretprobe(&krp);
if (ret < 0) {
pr_err("register_kretprobe failed: %d\n", ret);
return ret;
}
pr_info("kretprobe registered for kmalloc\n");
return 0;
}
static void __exit krp_exit(void)
{
unregister_kretprobe(&krp);
pr_info("kretprobe unregistered, missed: %lu\n", krp.nmissed);
}
module_init(krp_init);
module_exit(krp_exit);
MODULE_LICENSE("GPL");
maxactive是关键参数——kmalloc可能被多个CPU并发调用,maxactive决定同时能跟踪多少个并发实例。设小了会miss,nmissed记录了遗漏次数。
完整示例:监控kmalloc调用
把kprobe和kretprobe组合起来,统计kmalloc的调用次数和分配大小。
/* kmalloc监控模块(Linux 6.8验证:*/
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/spinlock.h>
static atomic_t alloc_count = ATOMIC_INIT(0);
static atomic_t total_size = ATOMIC_INIT(0);
static int kmalloc_entry(struct kretprobe_instance *ri, struct pt_regs *regs)
{
unsigned long size = regs_get_kernel_argument(regs, 0);
atomic_inc(&alloc_count);
atomic_add(size, &total_size);
return 0;
}
static int kmalloc_return(struct kretprobe_instance *ri, struct pt_regs *regs)
{
unsigned long ptr = regs_return_value(regs);
if (!ptr)
pr_warn("kmalloc returned NULL!\n");
return 0;
}
static struct kretprobe kmalloc_krp = {
.entry_handler = kmalloc_entry,
.handler = kmalloc_return,
.maxactive = 64,
.kp.symbol_name = "kmalloc",
};
static int __init monitor_init(void)
{
int ret = register_kretprobe(&kmalloc_krp);
if (ret) {
pr_err("register_kretprobe failed: %d\n", ret);
return ret;
}
pr_info("kmalloc monitor started\n");
return 0;
}
static void __exit monitor_exit(void)
{
unregister_kretprobe(&kmalloc_krp);
pr_info("kmalloc stats: calls=%d total_size=%d missed=%lu\n",
atomic_read(&alloc_count),
atomic_read(&total_size),
kmalloc_krp.nmissed);
}
module_init(monitor_init);
module_exit(monitor_exit);
MODULE_LICENSE("GPL");
注意:生产环境别对kmalloc这种高频函数插kprobe,开销很大。调试时用用就行。
五、KGDB远程调试
printk和ftrace。看日。的思路,KGDB是真正的源码级调试——断点、单步、查看变量,和用户态gdb一样。
内核配置
# 必须开启的配置。CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y # 串口调试
# 可。CONFIG_KGDB_KDB=y # KDB文本模式调试。CONFIG_DEBUG_INFO=y # 调试符号。g。CONFIG_FRAME_POINTER=y # 帧指针,保证backtrace准确
编译内核时确保CONFIG_DEBUG_INFO=y,否则gdb看不到源码和变量。
启动参数
# 在GRUB的linux行添加:
kgdboc=ttyS0,115200 # 指定串口和波特率
# 或者用ttyUSB(USB转串口)
kgdboc=ttyUSB0,115200
# 如果要KDB模式(不需要另一台机器)
kgdboc=ttyS0,115200 kgdbwait
# kgdbwait让内核启动时等待gdb连接
gdb连接
目标机上运行内核,开发机上用gdb连接vmlinux。
# 开发机。gdb vmlinux
# 连接目标机(通过串口线)
(gdb) target remote /dev/ttyS0
# 或者通过网络(需要kgdboe配置。(gdb) target remote 192.168.1.100:5555
连接成功后目标机内核被冻住,开发机的gdb获得完全控制权限
常用gdb命令
# 断点
(gdb) break my_probe # 函数断点
(gdb) break my_drv.c:42 # 行号断点
(gdb) break *0xffffffffc0123456 # 地址断点
(gdb) info breakpoints # 查看断点
(gdb) delete 1 # 删除断点1
# 执行控制
(gdb) continue # 继续运行
(gdb) step # 单步进入函数
(gdb) next # 单步跳过函数
(gdb) finish # 运行到当前函数返。
# 查看数据
(gdb) print my_struct->field # 打印结构体字。(gdb) print *ptr@10 # 打印数组。0个元。(gdb) print /x regs->rax # 十六进制打印
(gdb) x/10xw 0xffff888000123400 # 查看内存
# 调用。(gdb) backtrace # 完整调用。(gdb) backtrace 5 # 只看。。(gdb) frame 3 # 切到。。(gdb) info locals # 当前帧的局部变更
# 硬件断点(用于调试MMIO区域。(gdb) hbreak *0xffff888000123400 # 硬件断点
KGDB调试流程
触发KGDB的两种方式:
1. 代码中主动触*:kgdb_breakpoint(),放在驱动的probe或ioctl。2. 运行时手动触*:echo g > /proc/sysrq-trigger,内核立即冻住等gdb连接
六、内核oops分析
oops是内核遇到非法操作(空指针、非法地址访问等)时打印的错误报告。学会读oops,定位bug的效率翻倍。
oops信息解读
一个典型的空指针解引用oops。
BUG: unable to handle page fault for address: 0000000000000018
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
PGD 0 P4D 0
Oops: 0000 [#1] PREEMPT SMP NOPTI
CPU: 0 PID: 1234 Comm: my_app Tainted: G O
RIP: 0010:my_process_data+0x28/0x60 [my_driver]
Code: 48 89 c7 48 8b 40 18 83 f8 01 74 0a 48 8b 47 08 5d c3 <48> 8b 47 18 5d c3
RSP: 0018:ffffc90000123bf0 EFLAGS: 00010246
RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
RDX: 0000000000000000 RSI: ffff888012345678 RDI: 0000000000000000
Call Trace:
<TASK>
? show_regs+0x6d/0x80
? my_ioctl+0x45/0x80 [my_driver]
? do_vfs_ioctl+0xa5/0x680
? __x64_sys_ioctl+0x6e/0xb0
? do_syscall_64+0x3d/0x90
? entry_SYSCALL_64_after_hwframe+0x6e/0x76
</TASK>
关键信息逐行解读。
| 字段 | | 含义 |
|:-----|:---|:-----|
| unable to handle page fault for address | 0x0000000000000018 | 访问的非法地址,偏。x18说明是结构体指针为NULL后访问了1个字 |
| RIP | my_process_data+0x28/0x60 | 出错函数和偏移,函数大小0x60,出错在偏移0x28|
| RDI | 0x0000000000000000 | x86_64第一个参数,是NULL |
| RAX | 0x0000000000000000 | 返回值或中间结果,也是NULL |
| Call Trace | my_ioctl→do_vfs_ioctl。.. | 调用链,从下往上读 |
0x18偏移 + RDI为NULL:大概率是ptr->field,ptr是NULL,field在结构体偏移0x18处。
addr2line:地址转源码行。
# 用oops中的RIP地址定位源码
# 注意:需要用模块。ko文件(或vmlinux对于内建代码。addr2line -e my_driver.ko 0x28
# 输出示例。# /home/user/my_driver.c:42
# 如果地址是内核函数的
addr2line -e vmlinux ffffffff81234567
# 显示函数。addr2line -e my_driver.ko -f 0x28
# 输出。# my_process_data
# /home/user/my_driver.c:42
objdump反汇。
# 反汇编出错的函数
objdump -d -S my_driver.ko | grep -A 20 "my_process_data>"
# -S选项混合源码和汇编(需要编译时。g。# 输出示例。# 0000000000000000 <my_process_data>:
# 0: 55 push %rbp
# ...
# 28: 48 8b 47 18 mov 0x18(%rdi),%rax <-- 这里崩溃
# ...
0x18(%rdi)就是ptr->field,rdi是NULL。x18偏移正好和oops中的地址吻合。
decodecode脚本
内核源码自带scripts/decodecode,自动把oops中的Code行反汇编。
# 把oops中的Code行保存到文件
echo "Code: 48 89 c7 48 8b 40 18 83 f8 01 74 0a 48 8b 47 08 5d c3 <48> 8b 47 18 5d c3" > oops_code.txt
# 用decodecode反汇。scripts/decodecode < oops_code.txt
# 输出会标注出错的指令。>包裹的那条)
# All code:
# 0: 48 89 c7 mov %rax,%rdi
# 3: 48 8b 40 18 mov 0x18(%rax),%rax
# ...
# 1c: 48 8b 47 08 mov 0x8(%rdi),%rax
# 20: 5d pop %rbp
# 21: c3 ret
# Code starting with the faulting instruction:
# 0: 48 8b 47 18 mov 0x18(%rdi),%rax
# 4: 5d pop %rbp
# 5: c3 ret
oops分析流程
七、内存调试
内存问题是最难调的bug类型之一——泄漏、越界、use-after-free,症状和原因往往隔了十万八千里。Linux内核提供了三个层次的内存调试工具。
kmemleak:内核内存泄漏检。
kmemleak的工作原理类似垃圾回收器——定期扫描内核内存,找没有被任何指针引用的已分配块,这些就是疑似泄漏。
# 内核配置
# CONFIG_DEBUG_KMEMLEAK=y
# 手动触发扫描
echo scan > /sys/kernel/debug/kmemleak
# 查看泄漏报告
cat /sys/kernel/debug/kmemleak
# 输出示例。# unreferenced object 0xffff888012345600 (size 64):
# comm "my_app", pid 1234, jiffies 4294912345
# hex dump (first 32 bytes):
# 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
# 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
# backtrace:
# [<ffffffff81234567>] kmalloc_trace+0x27/0x90
# [<ffffffffc0123456>] my_probe+0x45/0x80
# [<ffffffff81234568>] pci_device_probe+0x78/0x120
# 清空当前报告(确认不是误报后。echo clear > /sys/kernel/debug/kmemleak
# 只看某个地址的详。echo 0xffff888012345600 > /sys/kernel/debug/kmemleak
backtrace直接告诉你哪行代码分配的内存没释放。my_probe+0x45就是泄漏点。
slub_debug:slab分配器调试
slub_debug在分配的slab对象周围加红区(redzone),检测越界读写;加毒值(poison),检测use-after-free。
# 内核启动参数,全局开销slub_debug=FZP
# 参数含义。# F - 对已释放对象填充毒值(检测use-after-free。# Z - 红区检查(检测越界写。# P - 慢速路径(更严格的检查)
# U - 用户追踪(记录分支释放调用栈)
# T - 追踪(记录所有分支释放。
# 只对特定slab开销slub_debug=FZP,kmalloc-64
# 运行时查看slab信息
cat /sys/kernel/slab/kmalloc-64/trace
cat /sys/kernel/slab/kmalloc-64/alloc_calls
cat /sys/kernel/slab/kmalloc-64/free_calls
# 手动触发检。echo 1 > /sys/kernel/slab/kmalloc-64/validate
slub_debug检测到问题会直接打oops,信息很明确。
BUG kmalloc-64 (Tainted: G B ): Redzone overwritten
KASAN:内核地址消毒。
KASAN(Kernel Address SANitizer)是最强的内存错误检测工具,能检测越界读写、use-after-free、栈溢出,开销比slub_debug大但检测能力也强得多。
# 内核配置
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y # 通用模式,兼容性好
# 或。CONFIG_KASAN_SW_TAGS=y # 基于软件标签(arm64专用,开销更小。
# 编译安装新内核后启动
# KASAN会自动在每次内存访问时检```
KASAN检测到错误会打印详细报告:
BUG: KASAN: slab-use-after-free in my_process_data+0x35/0x60
Read of size 4 at addr ffff888012345680 by task my_app/1234
CPU: 0 PID: 1234 Comm: my_app Tainted: G B
Call trace:
dump_stack+0x7c/0xa0
print_report+0x178/0x490
kasan_report+0xb0/0x110
my_process_data+0x35/0x60
Allocated by task 1234:
kasan_save_stack+0x24/0x50
kmem_cache_alloc+0x158/0x340
my_probe+0x45/0x80
Freed by task 1234:
kasan_save_stack+0x24/0x50
kasan_save_free_info+0x28/0x50
kmem_cache_free+0x124/0x2a0
my_remove+0x30/0x60
报告直接告诉你:谁分配的、谁释放的、谁在释放后又访问了。三条调用栈一出,bug基本锁死。
### 内存调试工具对比
| 工具 | 检测能力 | 开销 | 适用阶段 |
|:-----|:---------|:-----|:---------|
| kmemleak | 内存泄漏 | 低(扫描时短暂停顿) | 长时间运行后检测 |
| slub_debug | 越界、use-after-free | 中(5%~20%) | 开发调试 |
| KASAN | 越界、use-after-free、栈溢出 | 高(2x内存+20%速度) | 开发调试、CI |
## 八、调试工具对。
| 工具 | 适用场景 | 运行时开销 | 侵入性 | 需要重编译 | 学习成本 |
|:-----|:---------|:----------|:------|:----------|:---------|
| printk | 快速定位、简单跟踪 | 极低 | 低(加打印语句) | | |
| 动态调试 | pr_debug运行时开销 | 极低(关闭时) | | 否(需CONFIG_DYNAMIC_DEBUG| |
| ftrace | 函数调用链、性能分析 | 低~| | | |
| kprobes | 任意函数探针、变量读 | 中~| | | |
| KGDB | 源码级调试、断点单 | 无(断住时) | | 是(需CONFIG_KGDB| |
| oops分析 | 崩溃定位 | | | | |
| kmemleak | 内存泄漏检 | | | 是(需CONFIG_DEBUG_KMEMLEAK| |
| slub_debug | 越界/use-after-free | | | 是(启动参数 | |
| KASAN | 全面内存错误检 | | | 是(需CONFIG_KASAN| |
选工具的原则*开销从小到大试,侵入性从低到高用**。先printk,不行上ftrace,还不行再kprobes,最后才上KGDB。内存问题直接KASAN,别折腾。
## 九、常见问题
### Q1:ftrace无输出?
最常见的原因是`tracing_on`没开销
```bash
# 检查tracing_on状态cat /sys/kernel/debug/tracing/tracing_on
# 输出0说明没开
# 开销echo 1 > /sys/kernel/debug/tracing/tracing_on
# 其他可能原因。# 1. current_tracer设成了nop
cat /sys/kernel/debug/tracing/current_tracer
# 输出应该是function或function_graph,不是nop
# 2. 过滤器太严,没有匹配的函数cat /sys/kernel/debug/tracing/set_ftrace_filter
# 清空过滤器(跟踪所有函数)
echo > /sys/kernel/debug/tracing/set_ftrace_filter
# 3. 缓冲区满。cat /sys/kernel/debug/tracing/trace_pipe
# 用trace_pipe读取不会丢失数据
Q2:KGDB连接不上。
按这个清单排查:
# 1. 确认串口线接。# 开发机:ttyS0或ttyUSB0
# 目标机:kgdboc指定的串。
# 2. 确认波特率一。# kgdboc=ttyS0,115200 中的115200必须和gdb端一。stty -F /dev/ttyS0 115200
# 3. 确认内核配置
grep CONFIG_KGDB /boot/config-$(uname -r)
# CONFIG_KGDB=y
# CONFIG_KGDB_SERIAL_CONSOLE=y
# 4. 确认kgdboc参数生效
cat /proc/cmdline | grep kgdboc
# 5. 确认目标机已进入KGDB模式
# 目标机上执行。echo g > /proc/sysrq-trigger
# 或者代码中调用 kgdb_breakpoint()
# 6. 串口被其他程序占。fuser /dev/ttyS0
# 如果有输出,先kill掉占用的进程
Q3:kmemleak误报。
kmemleak基于指针扫描,有些合法的内存管理方式会被误判。
# 常见误报场景:# 1. 内存池:预分配一批对象存在链表里,kmemleak看不到链表外的引。# 2. 通过物理地址访问:ioremap映射的内存,kmemleak扫不。# 3. 指针被编码存储:比如低几位用作标志位,kmemleak认不。
# 处理方法。# 1. 确认是误报后,清空报。echo clear > /sys/kernel/debug/kmemleak
# 2. 对已知误报的地址,添加白名单
echo scan > /sys/kernel/debug/kmemleak # 重新扫描
# 然后只关注新增的泄漏
# 3. 代码中主动告诉kmemleak某块内存不是泄漏
kmemleak_not_leak(ptr);
kmemleak_ignore(ptr);
十、总结
调试场景速查表
| 调试场景 | 推荐工具 | 操作速查 |
|---|---|---|
| 快速加打印看变更 | printk/pr_err | pr_info("val=%d\n", val) |
| 运行时开关调试输 | 动态调试 | echo 'file xxx.c +p' > dynamic_debug/control |
| 梳理函数调用途 | ftrace function | echo function > current_tracer |
| 分析函数执行耗时 | ftrace function_graph | echo function_graph > current_tracer |
| 在任意函数插探针 | kprobes | register_kprobe(&kp) |
| 源码级断点调试 | KGDB | target remote /dev/ttyS0 |
| 崩溃后定位代码行 | oops + addr2line | addr2line -e xxx.ko 0x28 |
| 内存泄漏 | kmemleak | echo scan > kmemleak |
| 越界/use-after-free | KASAN | CONFIG_KASAN=y 编译内核 |
调试工具选择流程
调试内核没有银弹,但工具链是完整的。printk解决80%的问题,ftrace和kprobes解决15%,剩。%的疑难杂症交给KGDB和KASAN。关键是根据问题类型选对工具,别拿printk去调内存泄漏,也别上KGDB去查一个printk就能看出来的逻辑错误。
本文首发于linuxros.cn,转载请注明出处。