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);

该函数执行以下操作:

  1. 从内核的 kprobe 哈希表中移除该探测点。
  2. 恢复被替换的原始指令(将断点指令替换回原操作码)。
  3. 释放关联的 kprobe 资源。
  4. 如果该探测点正在被其他 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 注册、触发、注销的完整流程:

kprobe 注册与注销流程架构 模块加载 填充 struct kprobe register_kprobe() 成功? 插入断点指令 返回错误码 CPU 执行到探测点 pre_handler → 单步 → post_handler 模块卸载 unregister_kprobe() 恢复原始指令 说明: 虚线表示异步触发路径——kprobe 注册后,任何 CPU 执行到探测地址时都会触发处理流程,与模块的当前执行上下文无关。

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 注册/注销的日志输出