4. kprobe 注册与注销 API:register_kprobe()、unregister_kprobe()、register_kretprobe()、unregister_kretprobe()
kprobe 与 kretprobe 的注册与注销是驱动调试的核心操作。内核提供了两组对称的 API,分别用于管理 kprobe(指令级探测点)和 kretprobe(函数入口/返回探测点)。正确理解这些 API 的参数、返回值与生命周期,是编写稳定探测程序的前提。
4.1 API 原型与参数说明
四个核心 API 均定义在 <linux/kprobes.h> 中,其函数原型如下:
#include <linux/kprobes.h>
/* kprobe 注册与注销 */
int register_kprobe(struct kprobe *kp);
void unregister_kprobe(struct kprobe *kp);
/* kretprobe 注册与注销 */
int register_kretprobe(struct kretprobe *rp);
void unregister_kretprobe(struct kretprobe *rp);
| API | 参数 | 返回值 | 说明 |
|---|---|---|---|
register_kprobe() |
struct kprobe *kp |
0 成功;负值 errno | 在指定地址插入断点,触发 pre_handler/post_handler/fault_handler |
unregister_kprobe() |
struct kprobe *kp |
void | 移除断点,恢复原始指令;同步等待所有执行中的 handler 退出 |
register_kretprobe() |
struct kretprobe *rp |
0 成功;负值 errno | 在函数入口劫持返回地址,注册 entry_handler/return_handler |
unregister_kretprobe() |
struct kretprobe *rp |
void | 卸载返回探测,释放 kretprobe_instance 池 |
关键区别:
register_kprobe() 需要用户手动填充 kp->symbol_name 或 kp->addr;而 register_kretprobe() 通常只需设置 rp->kp.symbol_name 和 rp->handler,内核自动处理返回地址劫持。
4.2 数据结构初始化规范
注册前必须正确初始化探测结构体。以下是两种探测器的标准初始化模板:
/* kprobe 初始化示例 */
static struct kprobe kp = {
.symbol_name = "do_sys_openat2",
.pre_handler = handler_pre,
.post_handler = handler_post,
.fault_handler = handler_fault,
};
/* kretprobe 初始化示例 */
static struct kretprobe rp = {
.kp.symbol_name = "vfs_write",
.entry_handler = entry_handler,
.handler = return_handler,
.maxactive = 20, /* 最大并发探测实例数 */
.data_size = sizeof(struct my_ret_data),
};
| 字段 | 所属结构体 | 含义 | 是否必须 |
|---|---|---|---|
symbol_name |
struct kprobe |
被探测函数名(内核自动解析地址) | 是(与 addr 二选一) |
addr |
struct kprobe |
直接指定探测地址(十六进制) | 是(与 symbol_name 二选一) |
offset |
struct kprobe |
在 symbol_name 基础上的偏移字节 | 否(默认 0) |
pre_handler |
struct kprobe |
指令执行前回调 | 否(建议提供) |
post_handler |
struct kprobe |
指令执行后回调 | 否 |
fault_handler |
struct kprobe |
内存访问异常处理 | 否 |
maxactive |
struct kretprobe |
可同时跟踪的嵌套调用数 | 是(建议 20-100) |
data_size |
struct kretprobe |
每次探测携带的私有数据大小 | 否(默认 0) |
避坑指南:
maxactive 设置过小会导致 kretprobe 在递归或高并发场景下丢失探测实例,内核会打印 kretprobe: missed 警告。建议根据函数调用深度设置为 20~100。
4.3 注册与注销的完整生命周期
下图展示了 kprobe 从注册到注销的完整状态流转,以及内核在后台执行的同步操作:
4.4 返回值与错误码速查
注册 API 返回负值 errno 时,常见错误码及原因如下:
| 错误码 | 数值 | 含义 | 典型原因 |
|---|---|---|---|
-EINVAL |
-22 | 参数无效 | symbol_name 为空或 addr 为 NULL |
-ENOENT |
-2 | 符号未找到 | 函数名拼写错误或未导出(非 EXPORT_SYMBOL) |
-EEXIST |
-17 | 探测点已存在 | 同一地址已被其他 kprobe 注册(kprobe 不支持共享) |
-EAGAIN |
-11 | 暂时失败 | 内存分配失败或 kretprobe instance 池不足 |
-EPERM |
-1 | 操作不允许 | 探测点位于内核只读区域或黑名单中 |
调试技巧: 注册失败时,使用
printk(KERN_ERR "register_kprobe failed: %d\n", ret); 打印返回值,然后对照上表快速定位问题。最常见的是 -ENOENT(函数名错误)和 -EEXIST(重复注册)。
4.5 对称注销与同步语义
unregister_kprobe() 和 unregister_kretprobe() 具有以下重要特性:
- 幂等性: 多次调用
unregister_kprobe()是安全的,第二次调用不会产生副作用。 - 同步等待: 注销函数会调用
synchronize_rcu()或synchronize_sched(),确保所有正在执行的 handler 退出后才返回。 - 模块卸载必调: 在模块
exit函数中必须调用注销 API,否则内核会残留断点,导致系统崩溃。 - 注销顺序: 如果同时注册了 kprobe 和 kretprobe,建议按注册的逆序注销。
/* 模块退出时的标准清理模式 */
static void __exit my_kprobe_exit(void)
{
unregister_kretprobe(&rp); /* 先注销 kretprobe */
unregister_kprobe(&kp); /* 再注销 kprobe */
pr_info("kprobe/kretprobe unregistered successfully\n");
}
4.6 kprobe vs kretprobe 注册对比
下表从多个维度对比两种探测器的注册差异,帮助你在实际场景中做出选择:
| 对比维度 | kprobe | kretprobe |
|---|---|---|
| 探测时机 | 指令执行前/后 | 函数入口/返回 |
| 获取参数 | 通过 pt_regs 直接读取寄存器 |
entry_handler 可读参数;return_handler 可读返回值 |
| 获取返回值 | 需在 post_handler 中从寄存器提取 | return_handler 直接通过 ri->ret 获取 |
| 并发支持 | 天然支持多核并发 | 需设置 maxactive 防止丢失 |
| 性能开销 | 每次命中触发两次 trap(pre+post) | 入口一次 trap + 返回一次 trap |
| 适用场景 | 内联函数、指令级调试、任意地址探测 | 函数调用频次统计、执行时间测量、返回值监控 |
实战建议: 本章课程的核心目标——统计函数调用频次与执行时间——推荐使用
kretprobe。entry_handler 记录时间戳,return_handler 计算差值并累加计数,代码简洁且性能优于 kprobe 的 pre+post 组合。
4.7 完整注册/注销代码模板
以下是一个可直接编译的 kretprobe 注册/注销模板,包含完整的错误处理和资源清理:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kprobe Expert");
/* 私有数据结构:记录时间戳 */
struct my_data {
u64 entry_time;
};
/* 入口 handler:记录进入时间 */
static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
struct my_data *data;
data = (struct my_data *)ri->data;
data->entry_time = ktime_get_ns();
return 0;
}
/* 返回 handler:计算耗时 */
static int return_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
struct my_data *data;
u64 delta;
data = (struct my_data *)ri->data;
delta = ktime_get_ns() - data->entry_time;
pr_info("vfs_write duration: %llu ns\n", delta);
return 0;
}
/* 定义 kretprobe 实例 */
static struct kretprobe rp = {
.kp.symbol_name = "vfs_write",
.entry_handler = entry_handler,
.handler = return_handler,
.maxactive = 50,
.data_size = sizeof(struct my_data),
};
static int __init my_probe_init(void)
{
int ret;
ret = register_kretprobe(&rp);
if (ret < 0) {
pr_err("register_kretprobe failed: %d\n", ret);
return ret;
}
pr_info("kretprobe registered on %s\n", rp.kp.symbol_name);
return 0;
}
static void __exit my_probe_exit(void)
{
unregister_kretprobe(&rp);
pr_info("kretprobe unregistered\n");
}
module_init(my_probe_init);
module_exit(my_probe_exit);
重要提醒: 上述模板中的
ktime_get_ns() 返回纳秒级时间戳,在 return_handler 中计算差值时要注意 u64 溢出问题(实际场景中函数执行时间通常远小于 2^64 ns)。另外,maxactive 设置为 50 适用于大多数文件系统调用场景,若探测递归深度大的函数(如 unlink 路径),建议增大到 100。