欢迎光临
我们一直在努力

Linux 驱动故障排查:利用 kprobe 与 kretprobe 动态注入探测点分析内核变量

/* 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 字节码运行) 最高 (内核安全验证器保证不宕机) 生产级长周期观测、指标统计
  • 绝对禁止在 NMI(不可屏蔽中断)与硬件中断核心临界区插桩:在包含 raw_spin_lock_irqsave 保护的极高频代码中大量插入 kprobe,会导致中断响应延迟超标甚至触发内核 Soft Lockup。
  • maxactive 必须根据并发度合理设置:使用 kretprobe 时,maxactive 决定了内核预分配的返回实例池大小。如果多核高并发调用该函数,maxactive 过小会导致大量探测事件被丢弃(nmissed 计数上升)。
  • 善用 dump_stack() 快速定位非法调用链:在捕获到野指针或非预期返回值时,单次调用 dump_stack() 可以直接呈现完整的调用上下文堆栈,排查效率提升 10 倍以上。
  • 赞(0)
    未经允许不得转载:171主机测评 » Linux 驱动故障排查:利用 kprobe 与 kretprobe 动态注入探测点分析内核变量
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址