使用 ftrace 钩住函数

编写于:4.14

简介

ftrace 基础设施最初是为了将回调附加到函数的开头,以便记录和跟踪内核的流程而创建的。但是,对函数开始处的回调可以有其他用例。无论是用于实时内核修补还是用于安全监控。本文档介绍了如何使用 ftrace 来实现您自己的函数回调。

ftrace 上下文

警告

能够将回调添加到内核中几乎任何函数的能力都存在风险。回调可以从任何上下文(正常、softirq、irq 和 NMI)中调用。回调也可以在进入空闲状态之前、CPU 启动和关闭期间或进入用户空间时调用。这需要格外小心回调内部可以执行的操作。回调可以在 RCU 的保护范围之外调用。

有一些辅助函数可以帮助防止递归,并确保 RCU 正在监视。这些将在下面解释。

ftrace_ops 结构体

要注册函数回调,需要一个 ftrace_ops。此结构用于告诉 ftrace 将哪个函数作为回调调用,以及回调将执行哪些保护并且不需要 ftrace 处理。

在向 ftrace 注册 ftrace_ops 时,只需要设置一个字段

struct ftrace_ops ops = {
      .func                    = my_callback_func,
      .flags                   = MY_FTRACE_FLAGS
      .private                 = any_private_data_structure,
};

.flags 和 .private 都是可选的。只需要 .func。

要启用跟踪,请调用

register_ftrace_function(&ops);

要禁用跟踪,请调用

unregister_ftrace_function(&ops);

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

#include <linux/ftrace.h>

注册的回调将在调用 register_ftrace_function() 之后和返回之前开始被调用。回调开始被调用的确切时间取决于体系结构和服务调度。如果回调必须在确切的时刻开始,则回调本身必须处理任何同步。

unregister_ftrace_function() 将保证在 unregister_ftrace_function() 返回后,回调不再被函数调用。请注意,为了执行此保证,unregister_ftrace_function() 可能需要一些时间才能完成。

回调函数

回调函数的原型如下(截至 v4.14)

void callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct pt_regs *regs);
@ip

这是被跟踪的函数的指令指针。(函数中 fentry 或 mcount 的位置)

@parent_ip

这是调用被跟踪的函数的函数的指令指针(函数调用发生的位置)。

@op

这是用于注册回调的 ftrace_ops 的指针。这可以用于通过私有指针将数据传递给回调。

@regs

如果在 ftrace_ops 结构体中设置了 FTRACE_OPS_FL_SAVE_REGS 或 FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED 标志,则此指针将指向 pt_regs 结构体,就像在 ftrace 正在跟踪的函数开头设置断点一样。否则,它要么包含垃圾,要么为 NULL。

保护你的回调

由于函数可以从任何地方调用,并且回调调用的函数也可能被跟踪,并调用同一个回调,因此必须使用递归保护。有两个辅助函数可以帮助解决这个问题。如果你的代码以以下代码开始

int bit;

bit = ftrace_test_recursion_trylock(ip, parent_ip);
if (bit < 0)
        return;

并以以下代码结束

ftrace_test_recursion_unlock(bit);

即使它最终调用了回调正在跟踪的函数,中间的代码也可以安全使用。请注意,在成功时,ftrace_test_recursion_trylock() 将禁用抢占,ftrace_test_recursion_unlock() 将再次启用它(如果先前已启用)。指令指针 (ip) 及其父指针 (parent_ip) 将传递给 ftrace_test_recursion_trylock(),以记录递归发生的位置(如果设置了 CONFIG_FTRACE_RECORD_RECURSION)。

或者,如果在 ftrace_ops 上设置了 FTRACE_OPS_FL_RECURSION 标志(如下所述),则将使用一个辅助跳转来测试回调的递归,并且不需要进行递归测试。但这会增加额外函数调用的开销。

如果你的回调访问任何需要 RCU 保护的数据或临界区,最好确保 RCU 正在“监视”,否则该数据或临界区将不会受到预期的保护。在这种情况下,请添加

if (!rcu_is_watching())
        return;

或者,如果在 ftrace_ops 上设置了 FTRACE_OPS_FL_RCU 标志(如下所述),则将使用一个辅助跳转来测试回调的 rcu_is_watching,并且不需要进行其他测试。但这会增加额外函数调用的开销。

ftrace 标志

ftrace_ops 标志都在 include/linux/ftrace.h 中定义和文档化。一些标志用于 ftrace 的内部基础设施,但用户应该注意的标志如下

FTRACE_OPS_FL_SAVE_REGS

如果回调需要读取或修改传递给回调的 pt_regs,则必须设置此标志。在不支持将 pt_regs 传递给回调的体系结构上注册设置此标志的 ftrace_ops 将会失败。

FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED

与 SAVE_REGS 类似,但在不支持传递 regs 的体系结构上注册 ftrace_ops 时,不会因设置此标志而失败。但是,回调必须检查 regs 是否为 NULL 以确定体系结构是否支持它。

FTRACE_OPS_FL_RECURSION

默认情况下,期望回调可以处理递归。但是,如果回调不担心开销,则设置此位将通过调用辅助函数来在回调周围添加递归保护,该辅助函数将进行递归保护,并且仅在未发生递归时调用回调。

请注意,如果未设置此标志,并且发生递归,则可能会导致系统崩溃,并可能通过三重错误重新启动。

请注意,如果设置了此标志,则始终会在禁用抢占的情况下调用回调。如果未设置此标志,则可能(但不保证)会在可抢占的上下文中调用回调。

FTRACE_OPS_FL_IPMODIFY

需要设置 FTRACE_OPS_FL_SAVE_REGS。如果回调要“劫持”被跟踪的函数(调用另一个函数而不是被跟踪的函数),则需要设置此标志。这是实时内核补丁使用的。如果没有此标志,则无法修改 pt_regs->ip。

请注意,在任何给定时间,只能将一个设置了 FTRACE_OPS_FL_IPMODIFY 的 ftrace_ops 注册到任何给定函数。

FTRACE_OPS_FL_RCU

如果设置了此标志,则回调将仅由 RCU 正在“监视”的函数调用。如果回调函数执行任何 rcu_read_lock() 操作,则需要此标志。

当系统进入空闲状态、CPU 关闭并重新上线时,以及从内核进入用户空间并返回内核空间时,RCU 会停止监视。在这些转换过程中,可能会执行回调,并且 RCU 同步不会保护它。

FTRACE_OPS_FL_PERMANENT

如果在任何 ftrace 操作上设置了此标志,则无法通过向 proc sysctl ftrace_enabled 写入 0 来禁用跟踪。同样,如果 ftrace_enabled 为 0,则无法注册设置了此标志的回调。

Livepatch 使用它来不丢失函数重定向,以便系统保持受保护状态。

过滤要跟踪的函数

如果仅要从特定函数调用回调,则必须设置过滤器。过滤器按名称或 ip(如果已知)添加。

int ftrace_set_filter(struct ftrace_ops *ops, unsigned char *buf,
                      int len, int reset);
@ops

用于设置过滤器的 ops

@buf

包含函数过滤器文本的字符串。

@len

字符串的长度。

@reset

非零值表示在应用此过滤器之前重置所有过滤器。

过滤器表示启用跟踪时应启用哪些函数。如果 @buf 为 NULL 且设置了 reset,则将为跟踪启用所有函数。

@buf 也可以是 glob 表达式,以启用与特定模式匹配的所有函数。

请参阅 Documentation/trace/ftrace.rst 中的过滤器命令。

仅跟踪 schedule 函数

ret = ftrace_set_filter(&ops, "schedule", strlen("schedule"), 0);

要添加更多函数,请多次调用 ftrace_set_filter(),并将 @reset 参数设置为零。要删除当前过滤器集并将其替换为 @buf 定义的新函数,请将 @reset 设置为非零值。

移除所有已过滤的函数并跟踪所有函数

ret = ftrace_set_filter(&ops, NULL, 0, 1);

有时,多个函数可能具有相同的名称。在这种情况下,要仅跟踪特定函数,可以使用 ftrace_set_filter_ip()。

ret = ftrace_set_filter_ip(&ops, ip, 0, 0);

尽管 ip 必须是函数中调用 fentry 或 mcount 的地址。此函数由 perf 和 kprobes 使用,它们从用户获取 ip 地址(通常使用来自内核的调试信息)。

如果使用 glob 设置过滤器,则可以将函数添加到“notrace”列表中,这将阻止这些函数调用回调。 “notrace”列表的优先级高于“filter”列表。如果两个列表都不为空且包含相同的函数,则任何函数都不会调用回调。

空的“notrace”列表表示允许跟踪由过滤器定义的所有函数。

int ftrace_set_notrace(struct ftrace_ops *ops, unsigned char *buf,
                       int len, int reset);

这与 ftrace_set_filter() 采用相同的参数,但会将找到的要停止跟踪的函数添加到该列表中。这是一个与过滤器列表不同的单独列表,此函数不会修改过滤器列表。

非零的 @reset 会在添加与 @buf 匹配的函数之前清除“notrace”列表。

清除“notrace”列表与清除过滤器列表相同

ret = ftrace_set_notrace(&ops, NULL, 0, 1);

过滤器和 notrace 列表可以随时更改。如果只有一组函数应该调用回调,最好在注册回调之前设置过滤器。但更改也可能在注册回调之后发生。

如果存在过滤器,并且 @reset 为非零值,并且 @buf 包含与函数匹配的 glob,则切换将在 ftrace_set_filter() 调用期间发生。任何时候都不会有所有函数都调用回调的情况。

ftrace_set_filter(&ops, "schedule", strlen("schedule"), 1);

register_ftrace_function(&ops);

msleep(10);

ftrace_set_filter(&ops, "try_to_wake_up", strlen("try_to_wake_up"), 1);

与以下情况不同

ftrace_set_filter(&ops, "schedule", strlen("schedule"), 1);

register_ftrace_function(&ops);

msleep(10);

ftrace_set_filter(&ops, NULL, 0, 1);

ftrace_set_filter(&ops, "try_to_wake_up", strlen("try_to_wake_up"), 0);

后者会在重置和新设置过滤器之间的一小段时间内,让所有函数调用回调。