使用 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 的指针。这可以用于通过 private 指针将数据传递给回调。

@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 标志(如下所述),则将使用辅助 trampoline 来测试回调的递归,并且无需进行递归测试。 但是,这是以额外的函数调用带来的略微更多的开销为代价的。

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

if (!rcu_is_watching())
        return;

或者,如果在 ftrace_ops 上设置了 FTRACE_OPS_FL_RCU 标志(如下所述),则将使用辅助 trampoline 来测试回调的 rcu_is_watching,并且无需进行其他测试。 但是,这是以额外的函数调用带来的略微更多的开销为代价的。

ftrace FLAGS

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 ops 上设置了此标志,则无法通过将 0 写入 proc sysctl ftrace_enabled 来禁用跟踪。 同样,如果 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);

不同,因为后者在重置时间和新过滤器设置时间之间,将有一个短时间所有函数都会调用回调。