kprobe event使用详解(linux内核工程师提效神器)

1. kprobe event的使用场景

kprobe主要用来跟踪内核函数的调用、入参与返回值(还能获得全局变量的值)。与内核里的静态tracepoint

不同,kprobe几乎可以动态跟踪所有的内核函数(除了内联函数、使用NOKPROBE_SYMBOL标记的函数等,可通过cat /sys/kernel/debug/kprobes/blacklist查看)

2. kprobe event的使用前提

编译内核时打开以下选项:

CONFIG_KPROBES

=y

CONFIG_HAVE_KPROBES=y

CONFIG_KPROBE_EVENTS

=y

3. 通过shell添加kprobe event

kprobe event有两种类型:kprobe、kretprobe

.

3.1 脚本框架

ruby 复制代码
# 为了保证脚本执行前环境是干净的,执行以下命令恢复tracing的默认状态
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo 0 > /proc/sys/kernel/ftrace_enabled
echo 0 > /sys/kernel/debug/tracing/events/kprobes/enable
echo '' > /sys/kernel/debug/tracing/set_ftrace_filter

【这里根据需求添加kprobe的probe点,下文详述】

# 启动kprobe
echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable
echo 1 > /proc/sys/kernel/ftrace_enabled
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 查看结果信息
cat /sys/kernel/debug/tracing/trace_pipe

3.2 获得函数入参

比如想获得如下函数的tty指针

bash 复制代码
echo 'p:prob3 uart_write_room tty=$arg1:x64' >> /sys/kernel/debug/tracing/kprobe_events

(如未见以上输出,可能是函数并未调用,可通过ftrace的function trace功能先确认函数是否被调用)

有些平台可能不支持将kprobe时的参数打出来,需要做部分调整,笔者实验的平台需要打上这个patch:登录 - Gitee.com

3.3 入参是结构体指针,获得其成员(5星推荐的好用功能)

仍以上面uart_write_room的入参tty为例,其结构体定义如下:

在3.2节中获得了其结构体指针,更进一步如果想获得tty_struct的magic和name应该怎么做?首先需要先获得这两个成员在结构体中的偏移,可以用gdb vmlinux获得,也可以用crash工具解析vmcore获得

然后将如下命令写入3.1节的脚本框架中,执行脚本即可获得如下图结果

bash 复制代码
echo 'p:prob3 uart_write_room tty_magic=+0($arg1):x32 tty_name=+368($arg1):string' >> /sys/kernel/debug/tracing/kprobe_events

magic的值是符合预期的(如下图宏定义),name看起来也是合理的。

简单解释一下命令的含义:

(详细的用法介绍见内核文档:Documentation/trace/kprobetrace.rst)

(Documentation/trace/kprobetrace.rst部分摘录)

该功能可以非常方便的获得运行时的参数内容,在某些场景定位问题是神器

3.4 获得函数调用栈(5星推荐的好用功能)

在3.3节的基础上增加一行stacktrace的选项,即可打印出调用栈

ruby 复制代码
echo 1 > /sys/kernel/debug/tracing/options/stacktrace    # echo 0就是关闭调用栈
echo 'p:prob3 uart_write_room tty_magic=+0($arg1):x32 tty_name=+368($arg1):string' >> /sys/kernel/debug/tracing/kprobe_events

3.5 获得全局变量的值

有时候我们希望查看系统里全局变量的值,一个朴素的方式是增加一行日志打印,重新编译内核运行,但这种效率较低,可以借用kprobe来实现该诉求。

bash 复制代码
echo 'p:probe2 uart_write_room @pid_max:x32' >> /sys/kernel/debug/tracing/kprobe_events

pid_max是linux内核里的一个全局变量,和uart_write_room没有任何关系,但我们可以借助这个probe来打印出全局变量的内容,pid_max是0x8000是符合预期的。

3.6 获得函数返回值(5星推荐的好用功能)

bash 复制代码
echo 'r:probe1 uart_write_room $retval' >> /sys/kernel/debug/tracing/kprobe_events

又是一个常用的功能,可以获得两个信息:(1)uart_write_room的返回值是0xfff;(2)uart_write_room的调用者是tty_write_room。

4 简述kprobe event原理(主要摘抄参考文献3,侵删)

ARM64架构下kprobe event的实现原理基于指令替换异常处理机制,通过动态修改目标指令为断点指令(BRK),并在异常处理流程中执行用户定义的回调函数。示意如下:

pre_handler: 在CPU执行被探测指令之前 触发, 可做上下文捕获(各种寄存器)、参数获取、逻辑干预(可修改寄存器影响执行逻辑);

post_handler: 在CPU执行被探测指令之后触发(但尚未恢复原执行流), 常用场景如 统计函数耗时、验证执行正确性。

4.1 kprobe初始化

ini 复制代码
static int __init init_kprobes(void)
{
    int i, err = 0;
    unsigned long offset = 0, size = 0;
    char *modname, namebuf[128];
    const char *symbol_name;
    void *addr;
    struct kprobe_blackpoint *kb;

    // 1) 初始化用于存储 kprobe 模块的哈希表
    for (i = 0; i < KPROBE_TABLE_SIZE; i++) {
        INIT_HLIST_HEAD(&kprobe_table[i]);
        ...
    }

    // 2) 初始化 kprobe 的黑名单函数列表(不能被 kprobe 跟踪的函数列表)
    for (kb = kprobe_blacklist; kb->name != NULL; kb++) {
        kprobe_lookup_name(kb->name, addr);
        if (!addr)
            continue;

        kb->start_addr = (unsigned long)addr;
        symbol_name = kallsyms_lookup(kb->start_addr, &size, &offset, &modname,
                                      namebuf);
        if (!symbol_name)
            kb->range = 0;
        else
            kb->range = size;
    }
    ...

    kprobes_all_disarmed = false;

    // 3) 初始化CPU架构相关的环境(arm64架构的实现为空)
    err = arch_init_kprobes();

    // 4) 注册die通知链
    if (!err)
        err = register_die_notifier(&kprobe_exceptions_nb);

    // 5) 注册模块通知链
    if (!err)
        err = register_module_notifier(&kprobe_module_nb);
    ...
    return err;
}

内核把被跟踪的指令地址作为键,然后将 kprobe 结构保存到哈希表中,这样就能通过指令的地址快速查找到对应的 kprobe 结构。

4.2 注册kprobe

scss 复制代码
int __kprobes register_kprobe(struct kprobe *p)
{
    ...
    // 1) 获取要跟踪的指令的内存地址
    addr = kprobe_addr(p);       
    ...
    p->addr = addr;
    ...
    // 2) 检测跟踪点是否合法
    //(①kprobe只能用作内核函数的探测,所以在注册前必须检查探测点的地址是否是在内核地址空间)
    // (②kprobe 与 ftrace 不能同时跟踪同一个地址)
    // (③跟踪点是否在 kprobe 的黑名单中,如果是就返回错误)
    ret = check_kprobe_address_safe(p, &probed_mod); 
    ...
    // 3) 保存被跟踪指令的值
    ret = prepare_kprobe(p);
    ...
    // 4) 将 kprobe 结构添加到 kprobe 模块哈希表中
    hlist_add_head_rcu(&p->hlist,
                       &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);

    // 5) 将要跟踪的指令替换成 BRK 指令
    if (!kprobes_all_disarmed && !kprobe_disabled(p))
        arm_kprobe(p);
    ...
    return ret;
}

替换指令函数实现如下:

scss 复制代码
145 /* arm kprobe: install breakpoint in text */
146 void __kprobes arch_arm_kprobe(struct kprobe *p)  
147 {
148         patch_text(p->addr, BRK64_OPCODE_KPROBES);
149 }

4.3 触发kprobe

CPU执行到BRK64_OPCODE_KPROBES时,触发同步异常(Synchronous Exception),进入ARM64的el1_sync异常处理流程,根据​ESR寄存器的内容,最终执行到el1_dbg.

el1_sync-->el1_dbg-->do_debug_exception-->brk_handler-->kprobe_handler

kprobe_handler里先是进入pre_handler,然后通过setup_singlestep设置single-step相关寄存器,为下一步执行原指令时发生single-step异常做准备。

setup_singlestep() 执行完毕后,程序继续执行保存的被探测点的指令,由于开启了单步调试模式,执行完指令后会继续触发异常,单步执行探测点的指令后,会触发单步异常,进入single_step_handler,调用kprobe_breakpoint_ss_handler,主要任务是恢复执行路径,调用用户注册的post_handler

4.4 kprobe event原理

常见的使用kprobe的方式有两种:

(1)编写内核模块 直接调用register_kprobe API

  • 第一步:根据需要来编写探测函数,如 pre_handler 和 post_handler 回调函数。
  • 第二步:定义 struct kprobe 结构并且填充其各个字段,如要探测的内核函数名和各个探测回调函数。
  • 第三步:通过调用 register_kprobe 函数注册一个探测点。
  • 第四步:编写 Makefile 文件。
  • 第五步:编译并安装内核模块。

可参考samples/kprobes/kprobe_example.c

(2)使用kprobe event

通过/sys/kernel/debug/tracing/目录下的trace等属性文件来探测用户指定的函数,用户可添加kprobe支持的任意函数并设置探测格式与过滤条件,无需再编写内核模块,使用更为简便,但需要内核的debugfs和ftrace功能的支持。

sys/kernel/debug/tracing/kprobe_events文件节点的代码逻辑如下:

probes_write-->create_or_delete_trace_kprobe-->trace_kprobe_create-->register_trace_kprobe-->register_kprobe

通过这一系列调用注册kprobe event。

相关推荐
孟健3 小时前
我创业了!从大厂高薪到独立创业者的真实经历
程序员
用户237390331476 小时前
ESP32头文件路径
程序员
SimonKing6 小时前
告别繁琐配置!Retrofit-Spring-Boot-Starter让HTTP调用更优雅
java·后端·程序员
xiezhr7 小时前
一款带有AI功能的markdown笔记工具
笔记·程序员·产品
袁煦丞19 小时前
9.12 Halo的“傻瓜建站魔法”:cpolar内网穿透实验室第637个成功挑战
前端·程序员·远程工作
程序员鱼皮21 小时前
我做了个 AI 文档阅读神器,免费开源!
人工智能·程序员·ai编程
buddy_red1 天前
Knox工具调用功能测试
人工智能·后端·程序员
雾恋2 天前
最近一年的感悟
前端·javascript·程序员