函数追踪器设计

作者:

Mike Frysinger

注意

本文档已过时。 以下某些描述与当前的实现不符。

简介

在这里,我们将介绍通用函数追踪代码赖以正常运行的架构组件。 这些内容被分解为逐渐增加的复杂性,因此您可以从简单开始,并至少获得基本功能。

请注意,这仅侧重于架构实现细节。 如果您想更详细地了解通用代码中的某个功能,请查看通用的 ftrace - 函数追踪器 文件。

理想情况下,希望在内核中保持性能并支持追踪的每个人都应一直支持到动态 ftrace。

先决条件

Ftrace 依赖于以下功能的实现
  • STACKTRACE_SUPPORT - 实现 save_stack_trace()

  • TRACE_IRQFLAGS_SUPPORT - 实现 include/asm/irqflags.h

HAVE_FUNCTION_TRACER

您将需要实现 mcount 和 ftrace_stub 函数。

确切的 mcount 符号名称将取决于您的工具链。 有些将其称为 “mcount”,“_mcount”,甚至 “__mcount”。 您可以通过运行类似下面的命令来解决这个问题

$ echo 'main(){}' | gcc -x c -S -o - - -pg | grep mcount
        call    mcount

在下面的示例中,我们将假设该符号为“mcount”,以使事情变得简单明了。

请记住,mcount 函数中生效的 ABI 是 高度 架构/工具链特定的。 我们无法在这方面为您提供帮助,抱歉。 查找一些旧文档和/或找到比您更熟悉的人来一起讨论想法。 通常,寄存器使用(参数/暂存/等等...)是此时的主要问题,尤其是在 mcount 调用的位置(函数序言之前/之后)。 您可能还想看看 glibc 如何为您的体系结构实现 mcount 函数。 它可能是(半)相关的。

mcount 函数应检查函数指针 ftrace_trace_function,以查看它是否设置为 ftrace_stub。 如果是,则您无需执行任何操作,请立即返回。 如果不是,则以与 mcount 函数通常调用 __mcount_internal 相同的方式调用该函数 - 第一个参数是“frompc”,而第二个参数是“selfpc”(经过调整以删除嵌入在函数中的 mcount 调用的长度)。

例如,如果函数 foo() 调用 bar(),则当 bar() 函数调用 mcount() 时,mcount() 将传递给 tracer 的参数为

  • “frompc” - bar() 将用于返回到 foo() 的地址

  • “selfpc” - bar() 的地址(经过 mcount() 大小调整)

还要记住,此 mcount 函数将被调用 很多 次,因此,在禁用跟踪时,针对没有跟踪器的默认情况进行优化将有助于您的系统平稳运行。 因此,mcount 函数的开头通常是检查事物之前的最小裸代码。 这也意味着代码流通常应保持线性(即,在 nop 情况下没有分支)。 这当然是一种优化,而不是硬性要求。

以下是一些伪代码,应该有所帮助(这些函数实际上应该用汇编语言实现)

void ftrace_stub(void)
{
        return;
}

void mcount(void)
{
        /* save any bare state needed in order to do initial checking */

        extern void (*ftrace_trace_function)(unsigned long, unsigned long);
        if (ftrace_trace_function != ftrace_stub)
                goto do_trace;

        /* restore any bare state */

        return;

do_trace:

        /* save all state needed by the ABI (see paragraph above) */

        unsigned long frompc = ...;
        unsigned long selfpc = <return address> - MCOUNT_INSN_SIZE;
        ftrace_trace_function(frompc, selfpc);

        /* restore all state needed by the ABI */
}

不要忘记为模块导出 mcount!

extern void mcount(void);
EXPORT_SYMBOL(mcount);

HAVE_FUNCTION_GRAPH_TRACER

深呼吸... 是时候做一些真正的工作了。 在这里,您需要更新 mcount 函数以检查 ftrace 图函数指针,以及实现一些函数来保存(劫持)和恢复返回地址。

mcount 函数应检查函数指针 ftrace_graph_return(与 ftrace_stub 比较)和 ftrace_graph_entry(与 ftrace_graph_entry_stub 比较)。 如果这两个指针都没有设置为相关的存根函数,则调用特定于架构的函数 ftrace_graph_caller,该函数反过来又调用特定于架构的函数 prepare_ftrace_return。 这些函数名称都不是严格要求的,但是您仍然应该使用它们以保持跨架构端口的一致性 - 更容易比较和对比事物。

传递给 prepare_ftrace_return 的参数与传递给 ftrace_trace_function 的参数略有不同。 第二个参数“selfpc”是相同的,但是第一个参数应该是指向“frompc”的指针。 通常,它位于堆栈上。 这样,该函数可以临时劫持返回地址,以使其指向特定于架构的函数 return_to_handler。 该函数将仅调用通用 ftrace_return_to_handler 函数,该函数将返回原始的返回地址,您可以使用该地址返回到原始调用站点。

这是更新后的 mcount 伪代码

void mcount(void)
{
...
        if (ftrace_trace_function != ftrace_stub)
                goto do_trace;

+#ifdef CONFIG_FUNCTION_GRAPH_TRACER
+       extern void (*ftrace_graph_return)(...);
+       extern void (*ftrace_graph_entry)(...);
+       if (ftrace_graph_return != ftrace_stub ||
+           ftrace_graph_entry != ftrace_graph_entry_stub)
+               ftrace_graph_caller();
+#endif

        /* restore any bare state */
...

这是新的 ftrace_graph_caller 汇编函数的伪代码

#ifdef CONFIG_FUNCTION_GRAPH_TRACER
void ftrace_graph_caller(void)
{
        /* save all state needed by the ABI */

        unsigned long *frompc = &...;
        unsigned long selfpc = <return address> - MCOUNT_INSN_SIZE;
        /* passing frame pointer up is optional -- see below */
        prepare_ftrace_return(frompc, selfpc, frame_pointer);

        /* restore all state needed by the ABI */
}
#endif

有关如何实现 prepare_ftrace_return() 的信息,只需查看 x86 版本(帧指针传递是可选的;有关更多信息,请参见下一节)。 其中唯一特定于体系结构的部分是故障恢复表的设置(asm(...) 代码)。 其余代码在不同的体系结构中应相同。

这是新的 return_to_handler 汇编函数的伪代码。 请注意,此处应用的 ABI 与应用于 mcount 代码的 ABI 不同。 由于您是从一个函数返回(在函数尾声之后),因此您可能会跳过保存/恢复的内容(通常只是用于传递返回值的寄存器)。

#ifdef CONFIG_FUNCTION_GRAPH_TRACER
void return_to_handler(void)
{
        /* save all state needed by the ABI (see paragraph above) */

        void (*original_return_point)(void) = ftrace_return_to_handler();

        /* restore all state needed by the ABI */

        /* this is usually either a return or a jump */
        original_return_point();
}
#endif

HAVE_FUNCTION_GRAPH_FP_TEST

架构可以将一个唯一值(帧指针)传递到函数的进入和退出。 在退出时,将比较该值,如果该值不匹配,则内核将发生 panic。 这很大程度上是对 gcc 生成的错误代码的健全性检查。 如果您的端口的 gcc 可以在不同的优化级别下正确地更新帧指针,请忽略此选项。

但是,添加对其的支持并不十分困难。 在调用 prepare_ftrace_return() 的汇编代码中,将帧指针作为第三个参数传递。 然后,在该函数的 C 版本中,执行 x86 端口所做的事情,并将其传递给 ftrace_push_return_trace(),而不是传递存根值 0。

同样,当您调用 ftrace_return_to_handler() 时,请将帧指针传递给它。

HAVE_SYSCALL_TRACEPOINTS

您只需要很少的东西就可以在架构中进行系统调用追踪。

  • 支持 HAVE_ARCH_TRACEHOOK(请参见 arch/Kconfig)。

  • 在 <asm/unistd.h> 中具有 NR_syscalls 变量,该变量提供了架构支持的系统调用数量。

  • 支持 TIF_SYSCALL_TRACEPOINT 线程标志。

  • 将来自 ptrace 的 trace_sys_enter() 和 trace_sys_exit() 追踪点调用放入 ptrace 系统调用追踪路径中。

  • 如果此架构上的系统调用表比系统调用的简单地址数组更复杂,请实现 arch_syscall_addr 以返回给定系统调用的地址。

  • 如果此架构上的系统调用的符号名称与函数名称不匹配,请在 asm/ftrace.h 中定义 ARCH_HAS_SYSCALL_MATCH_SYM_NAME,并实现 arch_syscall_match_sym_name,并使用适当的逻辑来返回 true(如果函数名称与符号名称相对应)。

  • 将此架构标记为 HAVE_SYSCALL_TRACEPOINTS。

HAVE_FTRACE_MCOUNT_RECORD

有关更多信息,请参见 scripts/recordmcount.pl。 只需填写特定于架构的详细信息,以了解如何通过 objdump 查找 mcount 调用站点的地址。 如果没有同时实现动态 ftrace,则此选项没有多大意义。

HAVE_DYNAMIC_FTRACE

您将首先需要 HAVE_FTRACE_MCOUNT_RECORD 和 HAVE_FUNCTION_TRACER,因此,如果您过于渴望,请向上滚动您的阅读器。

完成这些之后,您将需要实现
  • asm/ftrace.h
    • MCOUNT_ADDR

    • ftrace_call_adjust()

    • struct dyn_arch_ftrace{}

  • 汇编代码
    • mcount() (新的存根)

    • ftrace_caller()

    • ftrace_call()

    • ftrace_stub()

  • C 代码
    • ftrace_dyn_arch_init()

    • ftrace_make_nop()

    • ftrace_make_call()

    • ftrace_update_ftrace_func()

首先,您需要在 asm/ftrace.h 中填写一些架构详细信息。

将 MCOUNT_ADDR 定义为 mcount 符号的地址,类似于

#define MCOUNT_ADDR ((unsigned long)mcount)

由于没有人会拥有该函数的声明,因此您需要

extern void mcount(void);

您还将需要辅助函数 ftrace_call_adjust()。 大多数人都可以像这样将其存根输出

static inline unsigned long ftrace_call_adjust(unsigned long addr)
{
        return addr;
}

<要填写的详细信息>

最后,您将需要自定义的 dyn_arch_ftrace 结构。 如果您在运行时修补任意调用站点时需要一些额外的状态,那么这里就是存放的地方。 但是,现在,创建一个空的结构

struct dyn_arch_ftrace {
        /* No extra data needed */
};

在头文件完成之后,我们可以填写汇编代码。 虽然我们之前已经创建了一个 mcount() 函数,但是动态 ftrace 只需要一个存根函数。 这是因为 mcount() 仅在启动期间使用,然后所有对它的引用都将被修补掉,永远不会返回。 相反,旧的 mcount() 的内部结构将用于创建一个新的 ftrace_caller() 函数。 由于两者很难合并,因此最好通过 #ifdef 将两个单独的定义分开。 ftrace_stub() 也是如此,因为它现在将内联在 ftrace_caller() 中。

在我们更加困惑之前,让我们看一下一些伪代码,以便您可以用汇编语言实现自己的东西

void mcount(void)
{
        return;
}

void ftrace_caller(void)
{
        /* save all state needed by the ABI (see paragraph above) */

        unsigned long frompc = ...;
        unsigned long selfpc = <return address> - MCOUNT_INSN_SIZE;

ftrace_call:
        ftrace_stub(frompc, selfpc);

        /* restore all state needed by the ABI */

ftrace_stub:
        return;
}

乍一看,这可能有点奇怪,但请记住,我们将运行时修补多件事。 首先,只有我们实际想要跟踪的函数才会被修补以调用 ftrace_caller()。 其次,由于我们一次只有一个 tracer 处于活动状态,因此我们将修补 ftrace_caller() 函数本身以调用有问题的特定 tracer。 这就是 ftrace_call 标签的意义所在。

考虑到这一点,让我们转到实际将进行运行时修补的 C 代码。 为了能够通过下一节,您需要了解您架构的指令代码。

每个架构都有一个 init 回调函数。 如果您需要在早期执行某些操作来初始化某些状态,那么这就是时候了。 否则,对于大多数人来说,以下简单函数应该足够了

int __init ftrace_dyn_arch_init(void)
{
        return 0;
}

有两个函数用于对任意函数进行运行时修补。 第一个用于将 mcount 调用站点转换为 nop(这有助于我们在不跟踪时保持运行时性能)。 第二个用于将 mcount 调用站点转换为对任意位置的调用(但通常是 ftracer_caller())。 有关这些函数的常规函数定义,请参见 linux/ftrace.h

ftrace_make_nop()
ftrace_make_call()

rec->ip 值是 mcount 调用站点的地址,该地址由 build time 期间的 scripts/recordmcount.pl 收集。

最后一个函数用于对活动 tracer 进行运行时修补。 这将修改 ftrace_caller() 函数内 ftrace_call 符号位置处的汇编代码。 因此,您应该在该位置具有足够的填充,以支持您将插入的新的函数调用。 有些人将使用“call”类型的指令,而另一些人将使用“branch”类型的指令。 具体来说,该函数是

ftrace_update_ftrace_func()

HAVE_DYNAMIC_FTRACE + HAVE_FUNCTION_GRAPH_TRACER

函数图需要进行一些调整才能与动态 ftrace 一起使用。 基本上,您将需要

  • 更新
    • ftrace_caller()

    • ftrace_graph_call()

    • ftrace_graph_caller()

  • 实现
    • ftrace_enable_ftrace_graph_caller()

    • ftrace_disable_ftrace_graph_caller()

<要填写的详细信息>

快速笔记

  • 在名为 ftrace_graph_call 的 ftrace_call 位置之后添加一个 nop 存根; 存根需要足够大以支持对 ftrace_graph_caller() 的调用

  • 更新 ftrace_graph_caller() 以使其能够通过新的 ftrace_caller() 进行调用,因为某些语义可能已更改

  • ftrace_enable_ftrace_graph_caller() 将使用对 ftrace_graph_caller() 的调用运行时修补 ftrace_graph_call 位置

  • ftrace_disable_ftrace_graph_caller() 将使用 nops 运行时修补 ftrace_graph_call 位置