3. kprobe 回调函数:pre_handler、post_handler、fault_handler 的作用与执行时机

kprobe 的核心机制是在目标指令处插入断点(或跳转指令),并在断点触发时依次调用用户注册的回调函数。Linux 内核为每个 kprobe 探针提供了三个可选的回调钩子:pre_handler、post_handler 和 fault_handler。理解这三个回调的执行时机、调用上下文以及返回值语义,是编写安全、高效 kprobe 模块的基础。

kprobe 回调函数执行流程 原始指令 pre_handler (断点触发时) 返回 0 → 单步执行 单步执行原始指令 post_handler (单步完成后) 异常 fault_handler

3.1 pre_handler:断点触发时的前置回调

执行时机:当 CPU 执行到被 kprobe 替换的断点指令(如 int3)时,内核立即调用 pre_handler。此时原始指令尚未执行。

函数原型:

int (*pre_handler)(struct kprobe *p, struct pt_regs *regs);

参数说明:

  • p:指向触发该回调的 kprobe 结构体。
  • regs:断点触发时刻的 CPU 寄存器快照,包含指令指针(IP)、栈指针(SP)以及通用寄存器值。

返回值语义:

返回值 含义 后续行为
0 正常处理 kprobe 继续执行单步流程,然后调用 post_handler
非 0 禁止单步执行 跳过单步和 post_handler,直接恢复原始指令流
调试技巧:在 pre_handler 中可以通过 regs->ip 获取当前指令地址,通过 regs->sp 获取栈指针。修改 regs 中的值会影响后续指令执行,但需谨慎操作。

3.2 post_handler:单步执行完成后的后置回调

执行时机:在 kprobe 单步执行完原始指令(即被断点替换的那条指令)之后,立即调用 post_handler。此时原始指令的效果已经生效(如寄存器值已更新、内存已写入)。

函数原型:

void (*post_handler)(struct kprobe *p, struct pt_regs *regs, unsigned long flags);

参数说明:

  • p:指向触发该回调的 kprobe 结构体。
  • regs:单步执行完成后的寄存器快照,反映了原始指令的执行结果。
  • flags:保留标志位,当前版本中通常为 0。
典型用途:post_handler 常用于收集函数执行后的结果数据,例如记录函数返回值、统计执行时间(在 pre_handler 中记录时间戳,在 post_handler 中计算差值)。

3.3 fault_handler:异常处理回调

执行时机:当 pre_handler 或 post_handler 内部发生不可恢复的异常(如访问无效内存地址、除零错误)时,内核会调用 fault_handler。注意:单步执行原始指令时发生的异常不会触发 fault_handler,而是由内核正常处理。

函数原型:

int (*fault_handler)(struct kprobe *p, struct pt_regs *regs, int trapnr);

参数说明:

  • p:指向触发异常的 kprobe 结构体。
  • regs:异常发生时的寄存器快照。
  • trapnr:异常号,例如 14 表示缺页异常(page fault)。

返回值语义:

返回值 含义
1 异常已处理,kprobe 继续正常执行流程
0 异常未处理,内核将触发一般性异常处理(可能导致内核 panic)
关键约束:fault_handler 中不能调用可能睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock),因为此时处于中断上下文。建议仅做简单的状态记录或寄存器修正。

3.4 三种回调的对比总结

回调类型 调用时机 可修改 regs 返回值 典型场景
pre_handler 断点触发时,原始指令执行前 是 0 继续 / 非0 跳过 记录函数参数、时间戳、条件过滤
post_handler 单步执行原始指令后 是 void 记录返回值、计算执行耗时
fault_handler pre/post 内部发生异常时 是 1 已处理 / 0 未处理 修复异常、记录错误、防止内核崩溃

3.5 回调执行顺序与中断上下文约束

三个回调的执行顺序严格固定:

  1. pre_handler → 若返回 0,则执行单步 → post_handler
  2. 若 pre_handler 返回非 0,则跳过单步和 post_handler
  3. 若 pre_handler 或 post_handler 内部触发异常,则调用 fault_handler

中断上下文约束:所有回调均在中断上下文中执行,这意味着:

  • 不能调用可能睡眠的函数(如 schedule()、wait_event())
  • 不能获取可睡眠锁(如 mutex_lock)
  • 可以使用自旋锁(spin_lock)和原子操作
  • 栈空间有限(通常为 4KB~8KB),避免大量局部变量
性能提示:pre_handler 和 post_handler 的执行会显著增加函数调用的延迟。在性能敏感路径上,应尽量缩短回调代码,或使用条件判断快速返回 0 以跳过不必要的处理。
kprobe 回调执行状态机 断点触发 pre_handler 单步执行 post_handler fault_handler 返回 0 异常

3.6 代码示例:注册三个回调

以下示例展示了如何为一个内核函数注册完整的三个回调:

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

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, ip = 0x%lx\n",
           p->addr, regs->ip);
    /* 返回 0 表示继续执行单步和 post_handler */
    return 0;
}

/* post_handler:记录执行完成 */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
                         unsigned long flags)
{
    printk(KERN_INFO "post_handler: regs->ip = 0x%lx\n", regs->ip);
}

/* fault_handler:处理异常 */
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
    printk(KERN_INFO "fault_handler: trapnr = %d\n", trapnr);
    /* 返回 1 表示异常已处理 */
    return 1;
}

static int __init kprobe_init(void)
{
    kp.pre_handler = handler_pre;
    kp.post_handler = handler_post;
    kp.fault_handler = handler_fault;
    kp.symbol_name = "do_sys_open";

    if (register_kprobe(&kp) < 0) {
        printk(KERN_ERR "Failed to register kprobe\n");
        return -EINVAL;
    }
    printk(KERN_INFO "kprobe registered at %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");
调试建议:在开发阶段,可以在 fault_handler 中打印 trapnr 和 regs->ip 来定位异常来源。生产环境中建议将 fault_handler 返回 1 并记录错误,避免内核崩溃。