4. kprobe 注册与注销 API:register_kprobe / unregister_kprobe 与 struct kprobe 结构体详解
kprobe 机制的核心操作围绕 注册 与 注销 两个 API 展开。开发者通过填充 struct kprobe 结构体描述探针行为,然后调用 register_kprobe() 将其插入内核监控点;当不再需要时,调用 unregister_kprobe() 安全移除。本节将深入剖析这两个 API 的语义、参数、返回值,并逐字段解析 struct kprobe 结构体,为后续 USB 驱动加载与中断响应的实战调试打下坚实基础。
4.1 struct kprobe 结构体:探针的“身份证”
struct kprobe 是 kprobe 框架中最核心的数据结构,它定义了探针的插入位置、触发时的回调函数以及运行时状态。该结构体定义在 <linux/kprobes.h> 中,主要字段如下表所示:
| 字段名 | 类型 | 说明 | 使用要点 |
|---|---|---|---|
addr |
kprobe_opcode_t * |
被探测指令的内存地址 | 必须指向内核代码段的有效指令;可通过 kallsyms_lookup_name() 获取函数名地址 |
symbol_name |
const char * |
被探测函数的名称(字符串) | 与 addr 二选一;内核会自动解析为地址 |
offset |
unsigned long |
在 symbol_name 函数内的偏移量(字节) |
用于探测函数内部特定指令;通常为 0 表示函数入口 |
pre_handler |
kprobe_pre_handler_t |
在被探测指令执行前调用的回调函数 | 最常用的回调;可修改寄存器、记录参数、阻止原指令执行 |
post_handler |
kprobe_post_handler_t |
在被探测指令执行后调用的回调函数 | 用于检查指令执行结果、返回值或副作用 |
fault_handler |
kprobe_fault_handler_t |
当探针触发时发生异常(如缺页)时调用 | 用于错误恢复或记录异常上下文 |
break_handler |
kprobe_break_handler_t |
当探针命中 kprobe 内部断点指令时调用 | 通常用于 kprobe 内部调试,普通用户很少使用 |
flags |
unsigned long |
控制探针行为的标志位 | 如 KPROBE_FLAG_DISABLED 注册后默认不激活 |
list |
struct list_head |
用于将探针链接到全局 kprobe 链表 | 由内核维护,用户无需手动操作 |
ainsn |
struct arch_specific_insn |
架构相关的指令替换信息(如 x86 的 int3 断点) | 由 register_kprobe() 内部填充 |
最佳实践: 在 USB 驱动调试场景中,通常只需设置
symbol_name、pre_handler 和 post_handler。例如探测 usb_submit_urb 函数时,symbol_name = "usb_submit_urb",offset = 0。
4.2 register_kprobe():将探针注入内核执行流
register_kprobe() 是 kprobe 机制的“安装”接口,其原型如下:
int register_kprobe(struct kprobe *p);
参数: p 指向一个已填充好关键字段的 struct kprobe 实例。
返回值:
- 0:注册成功,探针已激活。
- -EINVAL:参数无效,如
addr和symbol_name均为 NULL,或pre_handler为 NULL。 - -ENOENT:指定的
symbol_name在内核符号表中未找到。 - -EEXIST:该地址上已存在相同类型的探针(kprobe/jprobe/kretprobe 冲突)。
- -EAGAIN:临时资源不足,可稍后重试。
- -ENOMEM:内存分配失败。
注册流程(内部机制):
- 校验
p的合法性(地址、回调函数非空)。 - 通过
symbol_name+offset或直接addr确定目标指令位置。 - 在目标地址写入一条断点指令(x86 上为
int3,ARM64 上为brk)。 - 保存原始指令到
p->ainsn中,用于单步执行。 - 将
p插入全局 kprobe 哈希表,使探针对所有 CPU 可见。 - 执行内存屏障,确保其他 CPU 看到更新后的指令。
关键点: 注册成功后,目标地址的指令被临时替换为断点。当任何 CPU 执行到该地址时,会触发
int3 异常,kprobe 框架接管控制流,依次调用 pre_handler、单步执行原始指令、post_handler。
4.3 unregister_kprobe():安全移除探针
unregister_kprobe() 是 kprobe 的“卸载”接口,其原型如下:
void unregister_kprobe(struct kprobe *p);
参数: p 必须指向之前通过 register_kprobe() 成功注册的同一个 struct kprobe 实例。
返回值: 无(void)。
注销流程(内部机制):
- 从全局 kprobe 哈希表中移除
p。 - 将目标地址的断点指令恢复为原始指令。
- 执行
synchronize_rcu(),等待所有正在执行探针回调的 CPU 完成当前处理。 - 释放架构相关的资源(如单步执行缓冲区)。
重要注意事项:
- 注销后,
p指向的内存仍由调用者管理,内核不会自动释放。 - 如果在探针回调函数内部调用
unregister_kprobe()注销自身,会导致死锁或 use-after-free。应使用kprobe_flush_task()或延迟工作队列处理。 - 模块卸载前必须注销所有由该模块注册的 kprobe,否则内核会 panic。
4.4 注册与注销的完整生命周期(SVG 流程图)
下图展示了从填充结构体到注册、触发、最终注销的完整生命周期:
4.5 实战代码模板:USB 驱动探测场景
以下代码展示了如何在 USB 驱动加载场景中注册和注销一个 kprobe,用于监控 usb_submit_urb 函数的调用:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/usb.h>
static struct kprobe usb_kp;
/* pre_handler:在 usb_submit_urb 执行前调用 */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
printk(KERN_INFO "kprobe: usb_submit_urb called, IP=0x%lx\n", regs->ip);
/* 此处可读取参数:regs->di (第一个参数, struct urb *) */
return 0; /* 返回 0 继续执行原始指令 */
}
/* post_handler:在 usb_submit_urb 执行后调用 */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
printk(KERN_INFO "kprobe: usb_submit_urb returned, ret=0x%lx\n", regs->ax);
}
static int __init kprobe_usb_init(void)
{
int ret;
usb_kp.symbol_name = "usb_submit_urb";
usb_kp.offset = 0;
usb_kp.pre_handler = handler_pre;
usb_kp.post_handler = handler_post;
ret = register_kprobe(&usb_kp);
if (ret < 0) {
printk(KERN_ERR "register_kprobe failed, returned %d\n", ret);
return ret;
}
printk(KERN_INFO "kprobe registered at %p\n", usb_kp.addr);
return 0;
}
static void __exit kprobe_usb_exit(void)
{
unregister_kprobe(&usb_kp);
printk(KERN_INFO "kprobe unregistered\n");
}
module_init(kprobe_usb_init);
module_exit(kprobe_usb_exit);
MODULE_LICENSE("GPL");
调试技巧: 在 USB 设备插入瞬间,
usb_submit_urb 会被频繁调用。通过上述 kprobe,可以捕获每次 URB 提交时的寄存器状态,进而分析驱动加载流程中的中断响应延迟。
4.6 常见错误与排查方法
| 错误码 | 常见原因 | 排查方法 |
|---|---|---|
-EINVAL |
pre_handler 为 NULL,或 addr/symbol_name 均未设置 |
检查结构体初始化,确保至少指定一个定位方式 |
-ENOENT |
符号名拼写错误,或函数被内联/未导出 | 使用 cat /proc/kallsyms | grep usb_submit_urb 确认符号存在 |
-EEXIST |
同一地址上已有其他探针(kprobe/jprobe/kretprobe) | 检查是否有其他模块或 ftrace 在同一位置注册 |
| 内核 panic | 模块卸载前未调用 unregister_kprobe() |
在 __exit 函数中确保注销所有探针 |
4.7 本章小结
- struct kprobe 是探针的完整描述,包含定位信息(
symbol_name/addr)、回调函数(pre_handler/post_handler)和运行时状态。 - register_kprobe() 将探针注入内核,通过替换指令为断点实现拦截;返回 0 表示成功。
- unregister_kprobe() 安全移除探针,恢复原始指令,并等待所有 CPU 同步。
- 在 USB 驱动调试中,合理利用
pre_handler捕获函数参数,post_handler检查返回值,是分析驱动加载与中断响应的有效手段。