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 从注册到注销的完整状态流转,以及内核在后台执行的同步操作:

kprobe 注册与注销生命周期 1. 结构体初始化 2. register_kprobe() 3. 活跃探测 内核后台同步操作 • 解析 symbol_name → 转换为内存地址(kallsyms_lookup_name) • 保存原始指令字节 → 写入断点指令(int3/brk) • 注册到全局 kprobe 哈希表 → 激活 CPU 核上的探测点 • 对于 kretprobe:额外分配 kretprobe_instance 池,劫持返回地址 4. unregister_kprobe() 注销后内核清理动作 • 恢复原始指令字节 → 从全局哈希表移除 → 同步等待所有 CPU 核退出 handler • 对于 kretprobe:释放 instance 池 → 清理返回地址劫持

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。