4. kprobe 注册与注销 API:register_kprobe / unregister_kprobe 与 struct kprobe 结构体详解

kprobe 机制的核心操作围绕 注册 与 注销 两个 API 展开。开发者通过填充 struct kprobe 结构体描述探针行为,然后调用 register_kprobe() 将其插入内核监控点;当不再需要时,调用 unregister_kprobe() 安全移除。本节将深入剖析这两个 API 的语义、参数、返回值,并逐字段解析 struct kprobe 结构体,为后续 USB 驱动加载与中断响应的实战调试打下坚实基础。

4.1 struct kprobe 结构体:探针的“身份证”

struct kprobe 是 kprobe 框架中最核心的数据结构,它定义了探针的插入位置、触发时的回调函数以及运行时状态。该结构体定义在 <linux/kprobes.h> 中,主要字段如下表所示:

字段名 类型 说明 使用要点
addr kprobe_opcode_t * 被探测指令的内存地址 必须指向内核代码段的有效指令;可通过 kallsyms_lookup_name() 获取函数名地址
symbol_name const char * 被探测函数的名称(字符串) 与 addr 二选一;内核会自动解析为地址
offset unsigned long 在 symbol_name 函数内的偏移量(字节) 用于探测函数内部特定指令;通常为 0 表示函数入口
pre_handler kprobe_pre_handler_t 在被探测指令执行前调用的回调函数 最常用的回调;可修改寄存器、记录参数、阻止原指令执行
post_handler kprobe_post_handler_t 在被探测指令执行后调用的回调函数 用于检查指令执行结果、返回值或副作用
fault_handler kprobe_fault_handler_t 当探针触发时发生异常(如缺页)时调用 用于错误恢复或记录异常上下文
break_handler kprobe_break_handler_t 当探针命中 kprobe 内部断点指令时调用 通常用于 kprobe 内部调试,普通用户很少使用
flags unsigned long 控制探针行为的标志位 如 KPROBE_FLAG_DISABLED 注册后默认不激活
list struct list_head 用于将探针链接到全局 kprobe 链表 由内核维护,用户无需手动操作
ainsn struct arch_specific_insn 架构相关的指令替换信息(如 x86 的 int3 断点) 由 register_kprobe() 内部填充
最佳实践: 在 USB 驱动调试场景中,通常只需设置 symbol_name、pre_handler 和 post_handler。例如探测 usb_submit_urb 函数时,symbol_name = "usb_submit_urb",offset = 0。

4.2 register_kprobe():将探针注入内核执行流

register_kprobe() 是 kprobe 机制的“安装”接口,其原型如下:

int register_kprobe(struct kprobe *p);

参数: p 指向一个已填充好关键字段的 struct kprobe 实例。

返回值:

  • 0:注册成功,探针已激活。
  • -EINVAL:参数无效,如 addr 和 symbol_name 均为 NULL,或 pre_handler 为 NULL。
  • -ENOENT:指定的 symbol_name 在内核符号表中未找到。
  • -EEXIST:该地址上已存在相同类型的探针(kprobe/jprobe/kretprobe 冲突)。
  • -EAGAIN:临时资源不足,可稍后重试。
  • -ENOMEM:内存分配失败。

注册流程(内部机制):

  1. 校验 p 的合法性(地址、回调函数非空)。
  2. 通过 symbol_name + offset 或直接 addr 确定目标指令位置。
  3. 在目标地址写入一条断点指令(x86 上为 int3,ARM64 上为 brk)。
  4. 保存原始指令到 p->ainsn 中,用于单步执行。
  5. 将 p 插入全局 kprobe 哈希表,使探针对所有 CPU 可见。
  6. 执行内存屏障,确保其他 CPU 看到更新后的指令。
关键点: 注册成功后,目标地址的指令被临时替换为断点。当任何 CPU 执行到该地址时,会触发 int3 异常,kprobe 框架接管控制流,依次调用 pre_handler、单步执行原始指令、post_handler。

4.3 unregister_kprobe():安全移除探针

unregister_kprobe() 是 kprobe 的“卸载”接口,其原型如下:

void unregister_kprobe(struct kprobe *p);

参数: p 必须指向之前通过 register_kprobe() 成功注册的同一个 struct kprobe 实例。

返回值: 无(void)。

注销流程(内部机制):

  1. 从全局 kprobe 哈希表中移除 p。
  2. 将目标地址的断点指令恢复为原始指令。
  3. 执行 synchronize_rcu(),等待所有正在执行探针回调的 CPU 完成当前处理。
  4. 释放架构相关的资源(如单步执行缓冲区)。
重要注意事项:
  • 注销后,p 指向的内存仍由调用者管理,内核不会自动释放。
  • 如果在探针回调函数内部调用 unregister_kprobe() 注销自身,会导致死锁或 use-after-free。应使用 kprobe_flush_task() 或延迟工作队列处理。
  • 模块卸载前必须注销所有由该模块注册的 kprobe,否则内核会 panic。

4.4 注册与注销的完整生命周期(SVG 流程图)

下图展示了从填充结构体到注册、触发、最终注销的完整生命周期:

kprobe 注册与注销生命周期流程图 kprobe 注册与注销生命周期 1. 填充 struct kprobe 2. register_kprobe() 3. 内核内部操作 写入断点指令 → 保存原始指令 → 加入哈希表 4. 探针激活,等待命中 5. 命中触发(pre_handler → 单步 → post_handler) CPU 执行到断点 → 异常处理 → 回调执行 6. unregister_kprobe() 说明 注册后探针立即生效 注销后原始指令恢复 回调中不可注销自身

4.5 实战代码模板:USB 驱动探测场景

以下代码展示了如何在 USB 驱动加载场景中注册和注销一个 kprobe,用于监控 usb_submit_urb 函数的调用:

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

static struct kprobe usb_kp;

/* pre_handler:在 usb_submit_urb 执行前调用 */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
    printk(KERN_INFO "kprobe: usb_submit_urb called, IP=0x%lx\n", regs->ip);
    /* 此处可读取参数:regs->di (第一个参数, struct urb *) */
    return 0;  /* 返回 0 继续执行原始指令 */
}

/* post_handler:在 usb_submit_urb 执行后调用 */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
                         unsigned long flags)
{
    printk(KERN_INFO "kprobe: usb_submit_urb returned, ret=0x%lx\n", regs->ax);
}

static int __init kprobe_usb_init(void)
{
    int ret;

    usb_kp.symbol_name = "usb_submit_urb";
    usb_kp.offset = 0;
    usb_kp.pre_handler = handler_pre;
    usb_kp.post_handler = handler_post;

    ret = register_kprobe(&usb_kp);
    if (ret < 0) {
        printk(KERN_ERR "register_kprobe failed, returned %d\n", ret);
        return ret;
    }
    printk(KERN_INFO "kprobe registered at %p\n", usb_kp.addr);
    return 0;
}

static void __exit kprobe_usb_exit(void)
{
    unregister_kprobe(&usb_kp);
    printk(KERN_INFO "kprobe unregistered\n");
}

module_init(kprobe_usb_init);
module_exit(kprobe_usb_exit);
MODULE_LICENSE("GPL");
调试技巧: 在 USB 设备插入瞬间,usb_submit_urb 会被频繁调用。通过上述 kprobe,可以捕获每次 URB 提交时的寄存器状态,进而分析驱动加载流程中的中断响应延迟。

4.6 常见错误与排查方法

错误码 常见原因 排查方法
-EINVAL pre_handler 为 NULL,或 addr/symbol_name 均未设置 检查结构体初始化,确保至少指定一个定位方式
-ENOENT 符号名拼写错误,或函数被内联/未导出 使用 cat /proc/kallsyms | grep usb_submit_urb 确认符号存在
-EEXIST 同一地址上已有其他探针(kprobe/jprobe/kretprobe) 检查是否有其他模块或 ftrace 在同一位置注册
内核 panic 模块卸载前未调用 unregister_kprobe() 在 __exit 函数中确保注销所有探针

4.7 本章小结

  • struct kprobe 是探针的完整描述,包含定位信息(symbol_name/addr)、回调函数(pre_handler/post_handler)和运行时状态。
  • register_kprobe() 将探针注入内核,通过替换指令为断点实现拦截;返回 0 表示成功。
  • unregister_kprobe() 安全移除探针,恢复原始指令,并等待所有 CPU 同步。
  • 在 USB 驱动调试中,合理利用 pre_handler 捕获函数参数,post_handler 检查返回值,是分析驱动加载与中断响应的有效手段。