4. kprobe回调函数编写
kprobe的核心价值在于其回调函数——pre_handler和post_handler。这两个函数是开发者与内核执行流交互的接口,通过它们可以捕获函数入口和出口处的关键状态。本章将深入剖析这两个回调函数的参数结构,重点讲解如何通过struct kprobe和struct pt_regs读取函数参数与返回值。
4.1 回调函数原型与注册机制
每个kprobe实例在注册时,需要填充struct kprobe结构体中的回调函数指针。内核在探测点命中时,会根据当前执行位置自动调用对应的处理函数。
| 回调类型 | 函数原型 | 触发时机 | 典型用途 |
|---|---|---|---|
| pre_handler | int (*pre_handler)(struct kprobe *p, struct pt_regs *regs) |
在被探测指令执行前 | 读取函数参数、修改寄存器、决定是否跳过原指令 |
| post_handler | void (*post_handler)(struct kprobe *p, struct pt_regs *regs, unsigned long flags) |
在被探测指令执行后 | 读取函数返回值、捕获副作用、记录性能数据 |
| fault_handler | int (*fault_handler)(struct kprobe *p, struct pt_regs *regs, int trapnr) |
回调执行期间发生异常时 | 处理页错误等异常,防止系统崩溃 |
pre_handler返回非零值可以阻止原始指令执行,而post_handler无返回值且仅在pre_handler允许执行原指令后才会被调用。
4.2 struct kprobe:探测点的元数据容器
struct kprobe是kprobe机制的核心数据结构,它既承载了探测点的配置信息,也包含了运行时状态。在回调函数中,通过该结构体可以获取探测点的地址、符号名称、以及用户自定义数据。
struct kprobe {
struct hlist_node hlist; /* 哈希链表节点,用于内核内部管理 */
struct list_head list; /* 链表节点,用于链接多个kprobe */
unsigned long addr; /* 被探测指令的虚拟地址 */
kprobe_pre_handler_t pre_handler; /* 预处理回调 */
kprobe_post_handler_t post_handler; /* 后处理回调 */
kprobe_fault_handler_t fault_handler; /* 异常处理回调 */
unsigned long symbol_addr; /* 符号地址(如果通过符号注册) */
const char *symbol_name; /* 符号名称 */
unsigned int offset; /* 符号内的偏移量 */
unsigned long flags; /* 标志位,如KPROBE_FLAG_DISABLED */
void *user_data; /* 用户自定义数据指针 */
};
p->user_data字段可以在多个回调之间传递上下文信息。例如,在pre_handler中记录时间戳,在post_handler中计算执行耗时。
4.3 struct pt_regs:寄存器窗口的钥匙
struct pt_regs是内核在中断或异常发生时保存的寄存器快照。在kprobe回调中,它反映了被探测指令执行前(pre_handler)或执行后(post_handler)的CPU寄存器状态。通过解析该结构体,可以读取函数参数、返回值以及栈指针等信息。
不同架构下struct pt_regs的成员不同,但核心访问模式一致。以x86_64架构为例:
| 寄存器成员 | ABI用途 | 在kprobe回调中的含义 |
|---|---|---|
regs->di |
第一个整数参数(rdi) | 函数第1个参数 |
regs->si |
第二个整数参数(rsi) | 函数第2个参数 |
regs->dx |
第三个整数参数(rdx) | 函数第3个参数 |
regs->cx |
第四个整数参数(rcx) | 函数第4个参数 |
regs->r8 |
第五个整数参数(r8) | 函数第5个参数 |
regs->r9 |
第六个整数参数(r9) | 函数第6个参数 |
regs->ax |
函数返回值(rax) | 在post_handler中读取返回值 |
regs->sp |
栈指针(rsp) | 访问栈上额外参数或局部变量 |
regs->ip |
指令指针(rip) | 当前执行位置,可用于计算偏移 |
regs->regs[0]到regs->regs[7]传递参数,返回值在regs->regs[0]。编写跨架构代码时,应使用#ifdef条件编译或封装宏。
4.4 读取函数参数:pre_handler实战
在pre_handler中,寄存器保存的是调用者传入的参数值。以下示例演示如何读取sys_open系统调用的参数:
static int sys_open_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
const char *filename;
int flags;
umode_t mode;
/* x86_64: 参数1(dfd)在rdi, 参数2(filename)在rsi, 参数3(flags)在rdx, 参数4(mode)在rcx */
/* 注意:sys_open的签名是 long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode) */
/* 读取第二个参数:文件名指针(用户空间地址) */
filename = (const char *)regs->si;
/* 读取第三个参数:flags */
flags = (int)regs->dx;
/* 读取第四个参数:mode */
mode = (umode_t)regs->cx;
/* 安全地拷贝用户空间字符串(需要__copy_from_user或strncpy_from_user) */
char buf[256];
long ret = strncpy_from_user(buf, filename, sizeof(buf) - 1);
if (ret > 0) {
buf[ret] = '\0';
printk(KERN_INFO "kprobe: do_sys_open called, filename=%s, flags=0x%x, mode=0x%x\n",
buf, flags, mode);
}
return 0; /* 允许原指令继续执行 */
}
pt_regs读取的指针可能是用户空间地址,必须使用strncpy_from_user、copy_from_user等函数安全访问,直接解引用会导致内核崩溃。
4.5 读取返回值:post_handler实战
在post_handler中,regs->ax(x86_64)保存了函数的返回值。以下示例演示如何捕获kmalloc的返回值:
static void kmalloc_post_handler(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
unsigned long retval;
/* 读取返回值(rax寄存器) */
retval = regs->ax;
/* 读取第一个参数(size_t size) */
size_t size = (size_t)regs->di;
printk(KERN_INFO "kprobe: kmalloc(size=%zu) returned 0x%lx\n", size, retval);
if (retval == 0) {
printk(KERN_WARNING "kprobe: kmalloc allocation FAILED for size=%zu\n", size);
}
}
regs->ax直接包含指针值;对于返回long或int的函数,低32位有效。对于返回void的函数,regs->ax的值无意义。
4.6 参数与返回值读取流程总览
下图展示了从kprobe注册到回调中读取参数/返回值的完整数据流:
4.7 完整示例:跟踪do_sys_open并记录参数与返回值
以下是一个完整的kprobe模块,同时注册pre_handler和post_handler,完整捕获do_sys_open的输入参数和返回的文件描述符:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
static struct kprobe kp;
/* 保存每个探测点的调用上下文 */
struct open_context {
int dfd;
char filename[256];
int flags;
umode_t mode;
ktime_t start_time;
};
static int open_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
struct open_context *ctx;
const char __user *filename;
long ret;
/* 分配上下文(实际项目中建议使用percpu或预分配) */
ctx = kzalloc(sizeof(*ctx), GFP_ATOMIC);
if (!ctx)
return 0;
/* 读取参数 */
ctx->dfd = (int)regs->di;
filename = (const char __user *)regs->si;
ctx->flags = (int)regs->dx;
ctx->mode = (umode_t)regs->cx;
/* 安全拷贝文件名 */
ret = strncpy_from_user(ctx->filename, filename, sizeof(ctx->filename) - 1);
if (ret > 0)
ctx->filename[ret] = '\0';
else
ctx->filename[0] = '\0';
/* 记录开始时间 */
ctx->start_time = ktime_get();
/* 将上下文存入kprobe的user_data */
p->user_data = ctx;
printk(KERN_INFO "open_pre: dfd=%d, filename=%s, flags=0x%x, mode=0x%x\n",
ctx->dfd, ctx->filename, ctx->flags, ctx->mode);
return 0;
}
static void open_post_handler(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
struct open_context *ctx = p->user_data;
long fd = regs->ax; /* 返回值 */
ktime_t end_time;
s64 delta_ns;
if (!ctx)
return;
end_time = ktime_get();
delta_ns = ktime_to_ns(ktime_sub(end_time, ctx->start_time));
printk(KERN_INFO "open_post: filename=%s, returned fd=%ld, duration=%lld ns\n",
ctx->filename, fd, delta_ns);
/* 清理上下文 */
kfree(ctx);
p->user_data = NULL;
}
static int __init kprobe_demo_init(void)
{
int ret;
kp.symbol_name = "do_sys_open";
kp.pre_handler = open_pre_handler;
kp.post_handler = open_post_handler;
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 %p (symbol: %s)\n",
kp.addr, kp.symbol_name);
return 0;
}
static void __exit kprobe_demo_exit(void)
{
unregister_kprobe(&kp);
printk(KERN_INFO "kprobe unregistered\n");
}
module_init(kprobe_demo_init);
module_exit(kprobe_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Debug Expert");
4.8 常见陷阱与最佳实践
- 原子上下文限制: kprobe回调在原子上下文中执行,不能调用可能睡眠的函数(如
kmalloc(GFP_KERNEL)、mutex_lock)。应使用GFP_ATOMIC分配内存。 - 用户空间指针安全: 从
pt_regs读取的指针可能是用户空间地址,必须使用copy_from_user系列函数,不能直接解引用。 - 寄存器值有效性: 在
pre_handler中,regs->ax可能包含调用者的临时值,不代表返回值。只有在post_handler中regs->ax才包含函数返回值。 - 栈上参数访问: 当参数超过6个(x86_64)时,多余参数通过栈传递。需要通过
regs->sp加上偏移量访问栈上的参数。 - 性能开销: 每个kprobe命中都会触发两次陷阱(int3和单步),回调中的代码应尽量精简,避免长时间操作。
printk输出关键信息。生产环境中建议使用trace_printk或通过perf ring buffer传递数据,减少对系统性能的影响。
4.9 小结
本章深入剖析了kprobe回调函数的参数结构:struct kprobe提供了探测点的元数据和用户上下文,struct pt_regs提供了寄存器级别的执行状态快照。通过pre_handler可以读取函数参数,通过post_handler可以捕获返回值。掌握这些基础操作后,开发者可以构建强大的动态跟踪工具,在不修改内核代码的情况下深入洞察内核函数的执行细节。