Fprobe - 函数入口/出口探针

简介

Fprobe 是一个基于 ftrace 中的函数图跟踪特性的函数入口/出口探针。 如果你不想跟踪所有函数,而是想在特定函数的入口和出口处附加回调,类似于 kprobes 和 kretprobes,你可以使用 fprobe。 与 kprobes 和 kretprobes 相比,fprobe 可以用单个处理程序更快地对多个函数进行检测。 本文档描述如何使用 fprobe。

fprobe 的用法

fprobe 是 ftrace(+ 类似 kretprobe 的返回回调)的包装器,用于将回调附加到多个函数入口和出口。 用户需要设置 struct fprobe 并将其传递给 register_fprobe()

通常,fprobe 数据结构使用 entry_handler 和/或 exit_handler 初始化,如下所示。

struct fprobe fp = {
       .entry_handler  = my_entry_callback,
       .exit_handler   = my_exit_callback,
};

要启用 fprobe,请调用 register_fprobe(), register_fprobe_ips(), 和 register_fprobe_syms() 之一。 这些函数使用不同类型的参数注册 fprobe。

register_fprobe() 通过函数名过滤器启用 fprobe。 例如,这会在 “func*()” 函数上启用 @fp,但 “func2()” 除外。

register_fprobe(&fp, "func*", "func2");

register_fprobe_ips() 通过 ftrace 位置地址启用 fprobe。 例如:

unsigned long ips[] = { 0x.... };

register_fprobe_ips(&fp, ips, ARRAY_SIZE(ips));

并且 register_fprobe_syms() 通过符号名称启用 fprobe。 例如:

char syms[] = {"func1", "func2", "func3"};

register_fprobe_syms(&fp, syms, ARRAY_SIZE(syms));

要禁用(从函数中删除)此 fprobe,请调用

unregister_fprobe(&fp);

你可以暂时(软)禁用 fprobe,方法是:

disable_fprobe(&fp);

并通过以下方式恢复:

enable_fprobe(&fp);

以上是通过包含头文件定义的

#include <linux/fprobe.h>

与 ftrace 相同,注册的回调将在调用 register_fprobe() 之后和返回之前的一段时间后开始被调用。 请参阅 Documentation/trace/ftrace.rst

此外,unregister_fprobe() 将保证在 unregister_fprobe() 返回后,函数不再调用进入和退出处理程序,与 unregister_ftrace_function() 相同。

fprobe 进入/退出处理程序

进入/退出回调函数的原型如下:

int entry_callback(struct fprobe *fp, unsigned long entry_ip, unsigned long ret_ip, struct ftrace_regs *fregs, void *entry_data);

void exit_callback(struct fprobe *fp, unsigned long entry_ip, unsigned long ret_ip, struct ftrace_regs *fregs, void *entry_data);

请注意,@entry_ip 保存在函数入口处并传递给退出处理程序。 如果进入回调函数返回 !0,则将取消相应的退出回调。

@fp

这是与此处理程序相关的 fprobe 数据结构的地址。 你可以将 fprobe 嵌入到你的数据结构中,并通过 @fp 中的 container_of() 宏获取它。 @fp 不得为 NULL。

@entry_ip

这是被跟踪函数的 ftrace 地址(包括进入和退出)。 请注意,这可能不是函数的实际入口地址,而是 ftrace 被检测的地址。

@ret_ip

这是被跟踪函数将返回到的返回地址,位于调用者中的某个位置。 这可以在进入和退出时使用。

@fregs

这是进入和退出时的 ftrace_regs 数据结构。 这包括函数参数或返回值。 因此,用户可以通过适当的 ftrace_regs_* API 访问这些值。

@entry_data

这是一个本地存储,用于在进入和退出处理程序之间共享数据。 默认情况下,此存储为 NULL。 如果用户在注册 fprobe 时指定了 exit_handler 字段和 entry_data_size 字段,则会分配存储空间并将其传递给 entry_handlerexit_handler

同一函数上的进入数据大小和退出处理程序

由于进入数据通过每个任务的堆栈传递,并且其大小有限,因此每个探针的进入数据大小限制为 15 * sizeof(long)。 你还需要注意不同的 fprobe 正在探测同一个函数,此限制会变小。 进入数据大小与 sizeof(long) 对齐,并且每个具有退出处理程序的 fprobe 在堆栈上使用 sizeof(long) 空间,你应该尽可能减少同一函数上的 fprobe 数量。

与 kprobes 共享回调

由于 fprobe(和 ftrace)的递归安全性与 kprobes 略有不同,如果用户想要从 fprobe 和 kprobes 运行相同的代码,这可能会导致问题。

Kprobes 具有每个 CPU 的 ‘current_kprobe’ 变量,该变量在所有情况下都保护 kprobe 处理程序免受递归。 另一方面,fprobe 仅使用 ftrace_test_recursion_trylock()。 这允许中断上下文在 fprobe 用户处理程序运行时调用另一个(或相同的)fprobe。

如果通用回调代码有自己的递归检测,或者它可以处理不同上下文(普通/中断/NMI)中的递归,则这不是问题。 但是,如果它依赖于 ‘current_kprobe’ 递归锁,则必须检查 kprobe_running() 并使用 kprobe_busy_*() API。

Fprobe 具有 FPROBE_FL_KPROBE_SHARED 标志来执行此操作。 如果你的通用回调代码将与 kprobes 共享,请在注册 fprobe 之前 设置 FPROBE_FL_KPROBE_SHARED,如下所示

fprobe.flags = FPROBE_FL_KPROBE_SHARED;

register_fprobe(&fprobe, "func*", NULL);

这将保护你的通用回调免受嵌套调用。

丢失计数器

fprobe 数据结构具有 fprobe::nmissed 计数器字段,与 kprobes 相同。 在以下情况下,此计数器会递增:

  • fprobe 无法获取 ftrace_recursion 锁。 这通常意味着其他 ftrace 用户跟踪的函数是从 entry_handler 调用的。

  • 由于无法从每个任务的影子堆栈中分配数据缓冲区,fprobe 无法设置函数退出。

fprobe::nmissed 字段在这两种情况下都会递增。 因此,前者跳过进入和退出回调,后者跳过退出回调,但在两种情况下,计数器都将增加 1。

请注意,如果在注册 fprobe 时,你将 FTRACE_OPS_FL_RECURSION 和/或 FTRACE_OPS_FL_RCU 设置为 fprobe::ops::flags (ftrace_ops::flags),则此计数器可能无法正常工作,因为 ftrace 会跳过增加计数器的 fprobe 函数。

函数和结构

struct fprobe_hlist_node

基于地址的 fprobe 哈希列表节点。

定义:

struct fprobe_hlist_node {
    struct hlist_node       hlist;
    unsigned long           addr;
    struct fprobe           *fp;
};

成员

hlist

地址搜索哈希表的 hlist 节点。

addr

fp 的探测地址之一。

fp

拥有此节点的 fprobe。

struct fprobe_hlist

fprobe 的哈希列表节点。

定义:

struct fprobe_hlist {
    struct hlist_node               hlist;
    struct rcu_head                 rcu;
    struct fprobe                   *fp;
    int size;
    struct fprobe_hlist_node        array[] ;
};

成员

hlist

用于存在性检查哈希表的 hlist 节点。

rcu

用于 RCU 延迟释放的 rcu_head。

fp

拥有此 fprobe_hlist 的 fprobe。

size

array 的大小。

array

每个要探测的地址的 fprobe_hlist_node。

struct fprobe

基于 ftrace 的探针。

定义:

struct fprobe {
    unsigned long           nmissed;
    unsigned int            flags;
    size_t entry_data_size;
    fprobe_entry_cb entry_handler;
    fprobe_exit_cb exit_handler;
    struct fprobe_hlist     *hlist_array;
};

成员

nmissed

用于丢失事件的计数器。

flags

状态标志。

entry_data_size

私有数据存储大小。

entry_handler

函数入口的回调函数。

exit_handler

函数退出的回调函数。

hlist_array

用于从 IP 哈希表搜索 fprobe 的 fprobe_hlist。

void disable_fprobe(struct fprobe *fp)

禁用 fprobe

参数

struct fprobe *fp

要禁用的 fprobe。

描述

这将软禁用 fp。 请注意,这不会从函数入口中删除 ftrace 挂钩。

void enable_fprobe(struct fprobe *fp)

启用 fprobe

参数

struct fprobe *fp

要启用的 fprobe。

描述

这将软启用 fp

int register_fprobe(struct fprobe *fp, const char *filter, const char *notfilter)

通过模式将 fprobe 注册到 ftrace。

参数

struct fprobe *fp

要注册的 fprobe 数据结构。

const char *filter

探测符号的通配符模式。

const char *notfilter

NOT 探测符号的通配符模式。

描述

fp 注册到 ftrace,以在与 filter 匹配的符号上启用探测。 如果 notfilter 不为 NULL,则不会探测与 notfilter 匹配的符号。

如果 fp 注册成功,则返回 0,否则返回 -errno。

int register_fprobe_ips(struct fprobe *fp, unsigned long *addrs, int num)

通过地址将 fprobe 注册到 ftrace。

参数

struct fprobe *fp

要注册的 fprobe 数据结构。

unsigned long *addrs

目标函数地址的数组。

int num

addrs 的条目数。

描述

fp 注册到 ftrace,以在 addrs 给定的地址上启用探测。 addrs 必须是 ftrace 位置地址的地址,该地址可能是符号地址 + 架构相关的偏移量。 如果你不确定这意味着什么,请使用其他注册函数。

如果 fp 注册成功,则返回 0,否则返回 -errno。

int register_fprobe_syms(struct fprobe *fp, const char **syms, int num)

通过符号将 fprobe 注册到 ftrace。

参数

struct fprobe *fp

要注册的 fprobe 数据结构。

const char **syms

目标符号的数组。

int num

syms 的条目数。

描述

fp 注册到 syms 数组给定的符号。 如果你确定符号存在于内核中,这将非常有用。

如果 fp 注册成功,则返回 0,否则返回 -errno。

int unregister_fprobe(struct fprobe *fp)

注销 fprobe。

参数

struct fprobe *fp

要注销的 fprobe 数据结构。

描述

注销 fprobe(并从函数条目中删除 ftrace 挂钩)。

如果 fp 注销成功,则返回 0,否则返回 -errno。