4. kprobe 注册与注销:register_kprobe()、unregister_kprobe() API 详解与参数含义
kprobe 的生命周期由两个核心 API 控制:注册(将探测点插入内核指令流)与注销(从内核指令流中移除探测点)。理解这两个函数的参数含义、返回值及底层行为,是正确使用 kprobe 的基础。
4.1 register_kprobe():注册探测点
register_kprobe() 是 kprobe 机制的入口函数,负责将预配置的 struct kprobe 结构体注册到内核的探测点管理框架中。
4.1.1 函数原型与参数
#include <linux/kprobes.h>
int register_kprobe(struct kprobe *p);
| 参数 | 类型 | 含义 | 必须设置 |
|---|---|---|---|
p |
struct kprobe * |
指向已初始化的 kprobe 结构体指针 | 是 |
p->addr |
kprobe_opcode_t * |
探测点目标地址(函数入口或指令地址) | 是 |
p->pre_handler |
kprobe_pre_handler_t |
指令执行前回调函数(可选) | 否 |
p->post_handler |
kprobe_post_handler_t |
指令执行后回调函数(可选) | 否 |
p->fault_handler |
kprobe_fault_handler_t |
异常处理回调函数(可选) | 否 |
p->symbol_name |
const char * |
内核符号名称(与 addr 二选一) |
否 |
p->offset |
unsigned long |
符号内偏移量(配合 symbol_name 使用) |
否 |
关键约束:
addr 与 symbol_name 必须且只能设置一个。若使用 symbol_name,内核会自动解析符号地址并填充到 addr 字段。
4.1.2 返回值与错误码
| 返回值 | 含义 | 常见原因 |
|---|---|---|
0 |
注册成功 | 探测点已插入目标指令 |
-EINVAL |
无效参数 | p 为 NULL,或 addr/symbol_name 均未设置 |
-ENOENT |
符号未找到 | symbol_name 指定的内核符号不存在 |
-EACCES |
权限不足 | 目标地址位于只读内存区域(如 __init 段) |
-EEXIST |
探测点已存在 | 同一地址已被其他 kprobe 注册(未启用优化) |
-ENOMEM |
内存不足 | 内核无法分配 kprobe 内部数据结构 |
4.1.3 注册流程(内部机制)
- 参数校验:检查
p指针有效性,确认addr或symbol_name已设置。 - 地址解析:若使用
symbol_name,通过kallsyms_lookup_name()解析符号地址。 - 指令备份:读取目标地址处的原始指令,保存到
p->ainsn.insn。 - 断点插入:将目标指令首字节替换为
int3(x86 架构)或等效断点指令。 - 链表挂载:将
p插入全局 kprobe 哈希表kprobe_table。 - 内存屏障:执行
smp_wmb()确保其他 CPU 看到更新后的指令缓存。
调试提示: 注册失败时,优先检查
/sys/kernel/debug/kprobes/list 查看已注册探测点,确认目标地址是否已被占用。
4.2 unregister_kprobe():注销探测点
unregister_kprobe() 负责从内核中移除已注册的 kprobe,恢复原始指令并释放相关资源。
4.2.1 函数原型与参数
void unregister_kprobe(struct kprobe *p);
| 参数 | 类型 | 含义 |
|---|---|---|
p |
struct kprobe * |
指向之前注册成功的 kprobe 结构体指针 |
重要:
unregister_kprobe() 无返回值。若传入未注册或已注销的 p,行为未定义(可能导致内核崩溃)。
4.2.2 注销流程(内部机制)
- 安全检查:确认
p当前处于已注册状态。 - 指令恢复:将
p->ainsn.insn中保存的原始指令写回目标地址。 - 缓存刷新:调用
flush_icache_range()使指令缓存失效。 - 链表移除:从
kprobe_table哈希表中删除p节点。 - 同步等待:通过
synchronize_rcu()确保所有正在执行的探测回调已完成。 - 资源释放:释放 kprobe 内部占用的内存(不释放
p本身)。
4.3 注册与注销的完整生命周期
4.4 完整代码示例
以下示例展示了一个完整的 kprobe 注册与注销流程,探测 do_sys_open 函数入口:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
static struct kprobe kp;
// 预执行回调
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
printk(KERN_INFO "pre_handler: do_sys_open called, ip=0x%lx\n",
regs->ip);
return 0;
}
// 后执行回调
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
printk(KERN_INFO "post_handler: do_sys_open returned\n");
}
static int __init kprobe_init(void)
{
int ret;
// 1. 初始化 kprobe 结构体
kp.symbol_name = "do_sys_open";
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
// 2. 注册探测点
ret = register_kprobe(&kp);
if (ret < 0) {
printk(KERN_ERR "register_kprobe failed, returned %d\n", ret);
return ret;
}
printk(KERN_INFO "kprobe registered at %p\n", kp.addr);
return 0;
}
static void __exit kprobe_exit(void)
{
// 3. 注销探测点
unregister_kprobe(&kp);
printk(KERN_INFO "kprobe unregistered\n");
}
module_init(kprobe_init);
module_exit(kprobe_exit);
MODULE_LICENSE("GPL");
4.5 常见错误与避坑指南
| 错误场景 | 现象 | 解决方案 |
|---|---|---|
| 重复注册同一地址 | register_kprobe() 返回 -EEXIST |
使用 kprobe_lookup() 检查是否已注册 |
| 符号名拼写错误 | 返回 -ENOENT |
通过 /proc/kallsyms 确认符号名 |
| 在 __init 函数上注册 | 返回 -EACCES |
避免在只读内存区域注册;使用 jprobe 替代 |
| 注销未注册的 kprobe | 内核崩溃(NULL 指针解引用) | 使用标志位跟踪注册状态,确保只注销已注册的探测点 |
| 模块卸载前未注销 | 内核 panic(回调函数地址失效) | 在 __exit 函数中必须调用 unregister_kprobe() |
最佳实践: 始终在模块初始化时检查
register_kprobe() 的返回值,并在模块退出时无条件调用 unregister_kprobe()。建议使用 bool kprobe_registered 标志位跟踪注册状态,避免重复注销。
4.6 多探测点管理
当需要同时监控多个函数时,可以使用 kprobe 数组统一管理:
#define MAX_PROBES 5
static struct kprobe kprobes[MAX_PROBES];
static const char *target_symbols[MAX_PROBES] = {
"do_sys_open",
"vfs_read",
"vfs_write",
"do_mmap",
"do_exit"
};
static int __init batch_kprobe_init(void)
{
int i, ret;
for (i = 0; i < MAX_PROBES; i++) {
kprobes[i].symbol_name = target_symbols[i];
kprobes[i].pre_handler = handler_pre;
ret = register_kprobe(&kprobes[i]);
if (ret) {
pr_err("Failed to register kprobe on %s: %d\n",
target_symbols[i], ret);
// 回滚已注册的探测点
while (--i >= 0)
unregister_kprobe(&kprobes[i]);
return ret;
}
}
return 0;
}
static void __exit batch_kprobe_exit(void)
{
int i;
for (i = 0; i < MAX_PROBES; i++)
unregister_kprobe(&kprobes[i]);
}
性能注意: 批量注册时,若中间某个探测点注册失败,必须回滚所有已注册的探测点,否则会导致资源泄漏。上述示例中的回滚逻辑是标准做法。