/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
Linux 驱动故障排查:利用 kprobe 与 kretprobe 动态注入探测点分析内核变量

在设备驱动和嵌入式系统排障中,最令人头疼的问题往往发生在生产运行态:网络设备偶发丢包、DMA 缓冲区物理地址映射异常、或者某硬件寄存器读写时断时续。
如果每次为了排查问题都需要在内核代码中插入 printk 并重新编译烧录固件,不仅开发效率极其低下,而且往往因为改变了代码时序导致竞态 Bug 无法复现(Heisenbug 现象)。
Linux 内核提供的 Kprobes(Kernel Probes) 机制提供了一种接近“外科手术式”的非侵入式动态跟踪能力。它允许开发者在内核几乎任何指令地址动态挂载探测点,实时提取寄存器状态、函数入参与局部变量,配合 kretprobe 还能精准捕获函数的返回值与执行延迟。
一、Kprobes 与 kretprobe 的底层工作机制
Kprobes 实现动态无缝插桩的核心在于利用了 CPU 的断点异常与指令模拟执行:
1. 注册探测点 (kprobe_register)
内核将目标地址的原指令首字节备份,并替换为断点指令 (如 x86 的 INT3, ARM64 的 BRK)
│
▼
2. 目标代码执行到该断点
CPU 触发断点异常中断 ──> 陷落入 Kprobes 异常处理程序 (do_int3 / do_debug_exception)
│
▼
3. 执行预设前置钩子 (pre_handler)
获取当前的 pt_regs 寄存器快照,读取入参、内存指针与调用栈
│
▼
4. 单步执行原指令 (Single-step Out-of-Line)
在备份的独立内存缓冲区中单步执行原指令,恢复 CPU 寄存器上下文
│
▼
5. 执行后置钩子 (post_handler) ──> 恢复正常执行流
对于捕获函数返回值的 kretprobe,内核采用了“跳板(Trampoline)”技术:在函数入口处将返回地址(Return Address)临时修改为 kretprobe_trampoline,当函数执行 ret 时,控制流首先跳入 kretprobe 收集返回值,随后再跳转回原本的调用者。
二、内核模块实战:动态监控驱动 DMA 映射与参数异常
假设我们在排查某外设驱动中 DMA 缓冲区映射偶发失败的故障,需要抓取 dma_map_page_attrs 或自定义驱动函数 my_device_xmit 的参数和返回值。
以下是一个完整的独立 Kprobe 内核模块源码:
/* kprobe_dma_tracer.c – 动态跟踪驱动参数与返回值 */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/dma-mapping.h>
static char symbol_name[KSYM_NAME_LEN] = "dma_map_page_attrs";
module_param_string(symbol, symbol_name, sizeof(symbol_name), 0644);
/* 1. kprobe 入口前置钩子: 打印入参 */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
/* x86_64 体系下入参分别位于: di (arg1), si (arg2), dx (arg3), cx (arg4) */
#ifdef CONFIG_X86_64
struct device *dev = (struct device *)regs->di;
unsigned long offset = (unsigned long)regs->dx;
size_t size = (size_t)regs->cx;
pr_info("[KProbe] %s: dev=%p, offset=0x%lx, size=%zu\\n",
p->symbol_name, dev, offset, size);
#elif defined(CONFIG_ARM64)
/* ARM64 体系下入参位于: regs[0], regs[1], regs[2], regs[3] */
pr_info("[KProbe ARM64] %s called, arg0=0x%llx, size=0x%llx\\n",
p->symbol_name, regs->regs[0], regs->regs[2]);
#endif
return 0;
}
/* 2. kretprobe 返回钩子: 捕获返回值与错误码 */
static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
#ifdef CONFIG_X86_64
unsigned long retval = regs->ax;
#elif defined(CONFIG_ARM64)
unsigned long retval = regs->regs[0];
#endif
// 判断 DMA 映射是否返回了错误地址 (dma_mapping_error)
if (retval == 0 || retval == ~0UL) {
pr_err("🚨 [KRetProbe Alert] %s 返回非法 DMA 映射地址: 0x%lx!\\n", symbol_name, retval);
dump_stack(); // 打印完整内核回溯栈
}
return 0;
}
static struct kprobe kp = {
.pre_handler = handler_pre,
};
static struct kretprobe rp = {
.handler = ret_handler,
.maxactive = 32, // 最大并发探测深度
};
static int __init kprobe_tracer_init(void)
{
int ret;
kp.symbol_name = symbol_name;
rp.kp.symbol_name = symbol_name;
ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("register_kprobe failed, returned %d\\n", ret);
return ret;
}
ret = register_kretprobe(&rp);
if (ret < 0) {
pr_err("register_kretprobe failed, returned %d\\n", ret);
unregister_kprobe(&kp);
return ret;
}
pr_info("Kprobe tracer loaded successfully for symbol: %s\\n", symbol_name);
return 0;
}
static void __exit kprobe_tracer_exit(void)
{
unregister_kretprobe(&rp);
unregister_kprobe(&kp);
pr_info("Kprobe tracer unloaded.\\n");
}
module_init(kprobe_tracer_init);
module_exit(kprobe_tracer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhong Yiren");
三、基于 Ftrace 的免编译命令行实时插桩
如果生产环境没有编译工具链,Linux 内核通过 ftrace 子系统直接开放了 kprobe 接口:
# 1. 动态挂载一个名为 my_dma_probe 的探测点,捕获 r8169 网卡发送函数的参数
echo 'p:my_dma_probe rtl8169_start_xmit skb=%di dev=%si' >> /sys/kernel/debug/tracing/kprobe_events
# 2. 挂载返回探测点,捕获返回值
echo 'r:my_dma_ret rtl8169_start_xmit ret=$retval' >> /sys/kernel/debug/tracing/kprobe_events
# 3. 启用跟踪器
echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable
# 4. 实时查看追踪日志
cat /sys/kernel/debug/tracing/trace_pipe
输出的实时结果如下:
<idle>-0 [002] d..1 1845.120: my_dma_probe: (rtl8169_start_xmit+0x0/0x6b0) skb=0xffff888104230000 dev=0xffff888102142000
<idle>-0 [002] d..1 1845.122: my_dma_ret: (rtl8169_start_xmit+0x6b0/0x6b0 <- rtl_hard_start_xmit) ret=0x0
排查完毕后的一键清理:
echo 0 > /sys/kernel/debug/tracing/events/kprobes/enable
echo > /sys/kernel/debug/tracing/kprobe_events
四、性能开销与安全防护军规
| Kprobe (断点模拟) | 0.8 ~ 1.5 μs (引发 CPU 异常陷入) | 极高 (指令单步安全隔离) | 定位偶发低频异常、参数校验 |
| Ftrace (基于 -pg mcount) | 0.05 ~ 0.1 μs (纯函数跳转) | 极高 (编译器支持) | 全局性能分析、函数耗时统计 |
| eBPF (kprobe-bpf) | 0.2 ~ 0.5 μs (JIT 字节码运行) | 最高 (内核安全验证器保证不宕机) | 生产级长周期观测、指标统计 |






