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 注册流程(内部机制)

  1. 参数校验:检查 p 指针有效性,确认 addr 或 symbol_name 已设置。
  2. 地址解析:若使用 symbol_name,通过 kallsyms_lookup_name() 解析符号地址。
  3. 指令备份:读取目标地址处的原始指令,保存到 p->ainsn.insn。
  4. 断点插入:将目标指令首字节替换为 int3(x86 架构)或等效断点指令。
  5. 链表挂载:将 p 插入全局 kprobe 哈希表 kprobe_table。
  6. 内存屏障:执行 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 注销流程(内部机制)

  1. 安全检查:确认 p 当前处于已注册状态。
  2. 指令恢复:将 p->ainsn.insn 中保存的原始指令写回目标地址。
  3. 缓存刷新:调用 flush_icache_range() 使指令缓存失效。
  4. 链表移除:从 kprobe_table 哈希表中删除 p 节点。
  5. 同步等待:通过 synchronize_rcu() 确保所有正在执行的探测回调已完成。
  6. 资源释放:释放 kprobe 内部占用的内存(不释放 p 本身)。

4.3 注册与注销的完整生命周期

kprobe 注册与注销生命周期 1. 结构体初始化 2. register_kprobe() 3. 探测执行 4. unregister_kprobe() register_kprobe() 内部子步骤 ① 参数校验 → ② 地址解析(symbol_name → addr) → ③ 原始指令备份 ④ 插入 int3 断点 → ⑤ 挂入 kprobe_table 哈希表 → ⑥ 内存屏障同步 unregister_kprobe() 内部子步骤 ① 安全检查 → ② 恢复原始指令 → ③ 刷新指令缓存(flush_icache_range) ④ 从 kprobe_table 移除 → ⑤ synchronize_rcu() 等待 → ⑥ 释放内部资源

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]);
}
性能注意: 批量注册时,若中间某个探测点注册失败,必须回滚所有已注册的探测点,否则会导致资源泄漏。上述示例中的回滚逻辑是标准做法。