3. kprobe API 详解:register_kprobe、unregister_kprobe、handler 回调函数原型
kprobe 驱动的核心操作围绕三个关键 API 展开:注册探针、注销探针以及回调函数的实现。理解这些 API 的原型、参数含义和生命周期管理,是编写稳定、高效 kprobe 模块的基础。本节将逐一拆解这些接口,并通过结构图和代码示例帮助您建立直观认知。
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 表示未处理,内核按默认异常处理流程执行。
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 缓冲区暂存,在进程上下文中统一处理。