3. kprobe 回调函数:pre_handler、post_handler、fault_handler 的作用与执行时机
kprobe 的核心机制是在目标指令处插入断点(或跳转指令),并在断点触发时依次调用用户注册的回调函数。Linux 内核为每个 kprobe 探针提供了三个可选的回调钩子:pre_handler、post_handler 和 fault_handler。理解这三个回调的执行时机、调用上下文以及返回值语义,是编写安全、高效 kprobe 模块的基础。
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 回调执行顺序与中断上下文约束
三个回调的执行顺序严格固定:
- pre_handler → 若返回 0,则执行单步 → post_handler
- 若 pre_handler 返回非 0,则跳过单步和 post_handler
- 若 pre_handler 或 post_handler 内部触发异常,则调用 fault_handler
中断上下文约束:所有回调均在中断上下文中执行,这意味着:
- 不能调用可能睡眠的函数(如
schedule()、wait_event()) - 不能获取可睡眠锁(如
mutex_lock) - 可以使用自旋锁(
spin_lock)和原子操作 - 栈空间有限(通常为 4KB~8KB),避免大量局部变量
性能提示:pre_handler 和 post_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 并记录错误,避免内核崩溃。