3. kprobe 的注册与注销
kprobe 的生命周期由两个核心 API 管理:register_kprobe() 用于将探测点动态插入内核指令流,unregister_kprobe() 用于安全移除探测点并恢复原始指令。理解这两个 API 的调用契约、参数约束和错误处理,是编写可靠 kprobe 模块的基础。此外,/sys/kernel/debug/kprobes/ 接口提供了用户态管理能力,方便调试和监控。
核心要点: register_kprobe() 在内核代码的任意地址插入断点指令(如 x86 的 int3),当 CPU 执行到该地址时触发 trap,进入 kprobe 处理流程。unregister_kprobe() 则负责恢复原始指令并清理相关数据结构。
3.1 register_kprobe() API 详解
函数原型定义在 <linux/kprobes.h> 中:
int register_kprobe(struct kprobe *kp);
参数 kp 是一个预先填充好的 struct kprobe 结构体指针。调用前必须至少设置以下字段:
| 字段 | 类型 | 说明 | 是否必须 |
|---|---|---|---|
symbol_name |
const char * |
被探测的函数名,如 "do_sys_open" |
与 addr 二选一 |
addr |
kprobe_opcode_t * |
被探测的指令地址,通常由 kallsyms_lookup_name() 获取 |
与 symbol_name 二选一 |
offset |
unsigned long |
从 symbol_name 或 addr 开始的偏移量(字节) |
可选,默认 0 |
pre_handler |
kprobe_pre_handler_t |
在探测点指令执行前调用 | 可选 |
post_handler |
kprobe_post_handler_t |
在探测点指令执行后调用 | 可选 |
fault_handler |
kprobe_fault_handler_t |
在处理探测点时发生异常时调用 | 可选 |
返回值:成功返回 0;失败返回负的错误码,常见错误码包括:
- -EINVAL:参数无效,如
symbol_name和addr都未设置,或探测点位于不允许探测的区域(如 __init 段)。 - -ENOENT:指定的
symbol_name在内核符号表中未找到。 - -EEXIST:该地址已被其他 kprobe 注册(kprobe 不支持同一地址重复注册)。
- -EAGAIN:临时资源不足,可稍后重试。
- -EPERM:探测点位于受保护区域(如 ftrace 保留区域)。
最佳实践: 始终检查 register_kprobe() 的返回值。在模块初始化函数中注册失败时,应回滚所有已成功的注册,避免留下孤立的探测点。
3.2 unregister_kprobe() API 详解
函数原型:
void unregister_kprobe(struct kprobe *kp);
该函数执行以下操作:
- 从内核的 kprobe 哈希表中移除该探测点。
- 恢复被替换的原始指令(将断点指令替换回原操作码)。
- 释放关联的 kprobe 资源。
- 如果该探测点正在被其他 CPU 执行,则等待执行完成后再返回(通过 synchronize_sched() 或类似机制)。
注意: unregister_kprobe() 必须在模块卸载前调用。如果模块卸载后仍有未注销的 kprobe,内核在访问已释放的模块代码时将触发 panic。建议在模块的 __exit 函数中按注册顺序的逆序注销所有 kprobe。
3.3 完整注册与注销示例
以下代码展示了在 do_sys_open 函数入口处注册 kprobe 的完整流程:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
static struct kprobe kp;
// pre_handler:在探测点指令执行前调用
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; // 返回 0 继续正常执行,返回 1 跳过原指令
}
// post_handler:在探测点指令执行后调用
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
printk(KERN_INFO "post_handler: do_sys_open returned\n");
}
// fault_handler:处理探测过程中的异常
static int handler_fault(struct kprobe *p, struct pt_regs *regs,
int trapnr)
{
printk(KERN_INFO "fault_handler: trapnr=%d\n", trapnr);
return 0; // 返回 0 表示未处理,内核继续默认异常处理
}
static int __init kprobe_init(void)
{
int ret;
kp.symbol_name = "do_sys_open";
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
kp.fault_handler = handler_fault;
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 0x%px\n", kp.addr);
return 0;
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
printk(KERN_INFO "kprobe unregistered\n");
}
module_init(kprobe_init);
module_exit(kprobe_exit);
MODULE_LICENSE("GPL");
3.4 通过 /sys/kernel/debug/kprobes/ 管理 kprobe
内核提供了 debugfs 接口用于查看和管理 kprobe 状态。需要挂载 debugfs:
mount -t debugfs none /sys/kernel/debug
挂载后,/sys/kernel/debug/kprobes/ 目录下包含以下文件:
| 文件/目录 | 类型 | 说明 |
|---|---|---|
list |
文件 | 列出所有已注册的 kprobe,包括地址、符号名、偏移量、处理函数地址等 |
enabled |
文件 | 写入 0 禁用所有 kprobe,写入 1 重新启用(不卸载探测点) |
blacklist |
文件 | 显示不允许注册 kprobe 的函数列表(如 __init 函数、kprobe 自身代码) |
stats |
文件 | 显示 kprobe 的统计信息,如命中次数、miss 次数、处理耗时等 |
查看当前所有 kprobe 的示例输出:
# cat /sys/kernel/debug/kprobes/list
ffffffff810a3b40 k do_sys_open+0x0 [DISABLED]
ffffffff810b2000 k __x64_sys_read+0x0 [DISABLED]
ffffffff810c1050 j do_sys_open+0x10 [DISABLED]
每行格式:地址 类型(k/j/r) 符号名+偏移 [状态]。类型 k 表示 kprobe,j 表示 jprobe,r 表示 kretprobe。
调试技巧: 当 kprobe 模块加载后未产生预期输出时,首先检查
/sys/kernel/debug/kprobes/list 确认探测点是否成功注册。如果列表为空,说明 register_kprobe() 失败,应检查 dmesg 中的错误信息。
3.5 kprobe 注册与注销流程架构
下图展示了从用户态模块加载到 kprobe 注册、触发、注销的完整流程:
3.6 常见错误与排查
- 注册失败 -ENOENT:确认函数名拼写正确,使用
grep <function> /proc/kallsyms验证符号是否存在。 - 注册失败 -EINVAL:检查是否同时设置了
symbol_name和addr(应只设置一个),或探测点位于黑名单中。 - 注册失败 -EEXIST:该地址已被其他 kprobe 占用。使用
cat /sys/kernel/debug/kprobes/list查看冲突的探测点。 - 模块卸载时 panic:忘记调用
unregister_kprobe(),或注销顺序错误导致依赖关系混乱。 - 探测点未触发:检查
/sys/kernel/debug/kprobes/enabled是否为 1;确认被探测的函数确实被调用(如通过 ftrace 或 printk 验证)。
调试命令速查:
# cat /sys/kernel/debug/kprobes/list — 查看所有注册的 kprobe# cat /sys/kernel/debug/kprobes/blacklist — 查看不可探测的函数# echo 0 > /sys/kernel/debug/kprobes/enabled — 临时禁用所有 kprobe# dmesg | tail -20 — 查看 kprobe 注册/注销的日志输出