3. kprobe API 详解:register_kprobe、unregister_kprobe、handler 回调函数原型

kprobe 驱动的核心操作围绕三个关键 API 展开:注册探针、注销探针以及回调函数的实现。理解这些 API 的原型、参数含义和生命周期管理,是编写稳定、高效 kprobe 模块的基础。本节将逐一拆解这些接口,并通过结构图和代码示例帮助您建立直观认知。

kprobe API 核心流程与生命周期 定义 kprobe 结构体 struct kprobe register_kprobe() 插入 kprobe 框架 内核运行触发 回调函数执行 unregister _kprobe() 回调函数(handler)分类 pre_handler 指令执行前触发 post_handler 指令执行后触发 fault_handler 异常处理回调 注册成功返回 0;失败返回负错误码(如 -EINVAL、-ENOMEM)

3.1 struct kprobe 结构体

所有 kprobe 操作都围绕 struct kprobe 展开。该结构体是注册探针的“身份证”,包含了目标地址、回调函数指针以及内部链表管理字段。

字段名 类型 说明
addr kprobe_opcode_t * 要探测的内核函数地址(必填)
symbol_name const char * 内核函数名称(与 addr 二选一)
offset unsigned int 函数入口偏移量(通常为 0)
pre_handler kprobe_pre_handler_t 指令执行前回调(可选)
post_handler kprobe_post_handler_t 指令执行后回调(可选)
fault_handler kprobe_fault_handler_t 异常处理回调(可选)
最佳实践:推荐使用 symbol_name 字段指定函数名,内核会自动解析地址。这比手动计算 addr 更安全、可移植性更强。

3.2 register_kprobe() — 注册探针

函数原型:

int register_kprobe(struct kprobe *kp);

该函数将 kp 指向的 kprobe 结构体注册到内核 kprobe 框架中。内核会在指定地址插入断点指令(如 x86 的 int3),当 CPU 执行到该地址时,自动触发注册的回调函数。

  • 返回值:成功返回 0;失败返回负错误码。
  • 常见错误码:
    • -EINVAL:参数无效(如 addr 为 NULL)
    • -ENOMEM:内存分配失败
    • -EBUSY:该地址已被其他 kprobe 占用
    • -EINVAL:指定的符号名未找到
注意:register_kprobe() 可以在进程上下文或模块初始化函数中调用,但不能在中断上下文、软中断或持有自旋锁时调用。内核会尝试分配内存并修改代码段,这些操作可能引起睡眠。

3.3 unregister_kprobe() — 注销探针

函数原型:

void unregister_kprobe(struct kprobe *kp);

该函数从内核 kprobe 框架中移除指定探针,恢复原始指令。注销后,内核不再触发该探针的回调函数。

  • 调用时机:通常在模块卸载函数(module_exit)中调用。
  • 安全性:确保在调用时没有其他 CPU 正在执行该探针的回调函数。内核内部通过 synchronize_sched() 或 synchronize_rcu() 保证安全。
  • 重复注销:对未注册的 kprobe 调用 unregister_kprobe() 是安全的,内核会忽略该操作。
性能提示:频繁注册/注销 kprobe 会带来较大的性能开销(涉及内存分配、代码段修改、TLB 刷新)。建议在模块初始化时一次性注册所有探针,在模块卸载时统一注销。

3.4 handler 回调函数原型

kprobe 支持三种回调函数,分别在不同阶段被调用。理解它们的原型和调用时机,是编写正确 handler 的关键。

3.4.1 pre_handler

原型:

int (*pre_handler)(struct kprobe *p, struct pt_regs *regs);
  • 调用时机:在目标指令执行之前,断点触发后立即调用。
  • 参数:p 指向触发该回调的 kprobe 结构体;regs 保存了 CPU 寄存器状态,可用于读取函数参数。
  • 返回值:返回 0 表示正常执行目标指令;返回非 0 表示跳过目标指令(单步模拟后直接返回)。

3.4.2 post_handler

原型:

void (*post_handler)(struct kprobe *p, struct pt_regs *regs, unsigned long flags);
  • 调用时机:在目标指令单步执行完成后调用。
  • 参数:flags 目前保留,通常为 0。
  • 用途:获取函数返回值(通过 regs 中的返回值寄存器)。

3.4.3 fault_handler

原型:

int (*fault_handler)(struct kprobe *p, struct pt_regs *regs, int trapnr);
  • 调用时机:在 pre_handler 或 post_handler 执行过程中发生异常时调用。
  • 参数:trapnr 表示异常号(如 x86 的 #PF 页错误)。
  • 返回值:返回 1 表示异常已处理,kprobe 框架继续执行;返回 0 表示未处理,内核按默认异常处理流程执行。
回调函数调用时序图 时间 断点触发 pre_handler 单步执行 post_handler fault_handler 异常发生 pre_handler 返回非 0 时,跳过单步执行和 post_handler,直接恢复执行 fault_handler 返回 1 表示异常已处理,框架继续执行

3.5 完整代码示例:注册与回调

以下示例展示了如何定义一个 kprobe、注册探针并实现三个回调函数:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>

/* 定义 kprobe 结构体 */
static struct kprobe kp;

/* pre_handler:指令执行前调用 */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
    printk(KERN_INFO "pre_handler: p->addr = 0x%p\n", p->addr);
    /* 读取函数参数示例:x86_64 上第一个参数在 rdi 寄存器 */
    printk(KERN_INFO "pre_handler: arg1 = %lx\n", regs->di);
    return 0;  /* 返回 0 继续执行目标指令 */
}

/* post_handler:指令执行后调用 */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
                         unsigned long flags)
{
    /* x86_64 上返回值在 rax 寄存器 */
    printk(KERN_INFO "post_handler: return value = %lx\n", regs->ax);
}

/* 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 1;  /* 返回 1 表示异常已处理 */
}

static int __init kprobe_init(void)
{
    int ret;

    kp.symbol_name = "do_sys_open";  /* 探测 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%p\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");
调试建议:在开发阶段,可以在 pre_handler 中打印 p->addr 确认探针是否正确注册到目标函数。使用 kallsyms_lookup_name() 可以动态解析符号地址,但 kprobe 的 symbol_name 字段已经内置了该功能。

3.6 API 使用注意事项

场景 注意事项
注册时指定 addr 和 symbol_name 两者不能同时非空,内核优先使用 addr。推荐只使用 symbol_name。
回调函数执行上下文 在中断上下文中执行,不能调用可能睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock)。
寄存器访问 pt_regs 结构体内容与架构相关,x86_64 和 ARM64 的寄存器名称不同,编写跨架构代码时需使用 regs_get_kernel_stack_nth() 等辅助宏。
探针数量限制 内核默认最大 kprobe 数量为 1024(可配置),超出返回 -ENOSPC。
模块卸载顺序 必须在 module_exit 中先调用 unregister_kprobe(),再释放其他资源,避免回调函数访问已释放的内存。
关键约束:回调函数中禁止调用 printk 以外的任何可能引起调度的函数。如果需要记录大量数据,建议使用 per-CPU 缓冲区暂存,在进程上下文中统一处理。