Fprobe - 函数进入/退出探针

简介

Fprobe 是一个基于 ftrace 中的函数图(function-graph)跟踪功能的函数进入/退出探针。如果你想在特定函数的进入和退出时附加回调,类似于 kprobes 和 kretprobes,但又不想跟踪所有函数,可以使用 fprobe。与 kprobes 和 kretprobes 相比,fprobe 能够通过单个处理程序(handler)为多个函数提供更快的插桩(instrumentation)。本文档描述了如何使用 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。例如,这会在除了 “func2()” 之外的 “func*()” 函数上启用 @fp。

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() 之后的一段时间内、且在其返回之前开始被调用。参见 ftrace - 函数跟踪器

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

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(!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

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

由于进入数据是通过每个任务的栈(per-task stack)传递的且其大小有限,因此每个探针的进入数据大小限制为 15 * sizeof(long)。你还需注意,如果不同的 fprobe 正在探测同一个函数,此限制会变小。进入数据大小按 sizeof(long) 对齐,并且每个带有退出处理程序的 fprobe 都在栈上占用一个 sizeof(long) 的空间,因此你应该尽量减少在同一个函数上的 fprobe 数量。

与 kprobes 共享回调

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

Kprobes 具有 per-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);

这将保护你的公共回调免受嵌套调用的影响。

丢失计数器

与 kprobes 类似,fprobe 数据结构具有 fprobe::nmissed 计数器字段。当以下情况发生时,此计数器会递增:

  • fprobe 获取 ftrace_recursion 锁失败。这通常意味着从 entry_handler 中调用了一个被其他 ftrace 用户跟踪的函数。

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

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 rhlist_head      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。

描述

这将软禁用(soft-disable)fp。注意,这不会从函数入口移除 ftrace 钩子(hooks)。

void enable_fprobe(struct fprobe *fp)

启用 fprobe

参数

struct fprobe *fp

要启用的 fprobe。

描述

这将软启用(soft-enable)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

不被探测符号的通配符模式。

描述

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_async(struct fprobe *fp)

无需 RCU GP 等待即可注销 fprobe

参数

struct fprobe *fp

要注销的 fprobe 数据结构。

描述

注销 fprobe(并从函数入口移除 ftrace 钩子)。此函数不会等待 fprobe 不再被使用。

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

int unregister_fprobe(struct fprobe *fp)

带有 RCU GP 等待的注销 fprobe

参数

struct fprobe *fp

要注销的 fprobe 数据结构。

描述

注销 fprobe(并从函数入口移除 ftrace 钩子)。此函数将阻塞,直到 fprobe 不再被使用。

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