函数追踪器设计

作者:

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() 将传递给追踪器的参数为

  • “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

架构可以向函数的进入和退出传递一个唯一的值(帧指针)。在退出时,将比较该值,如果它不匹配,则会使内核崩溃。这主要是对 gcc 的不良代码生成进行健全性检查。如果您的端口的 gcc 在不同的优化级别下能够正确更新帧指针,则忽略此选项。

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

类似地,当您调用 ftrace_return_to_handler() 时,传递帧指针。

HAVE_SYSCALL_TRACEPOINTS

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

  • 支持 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()。其次,由于我们一次只有一个跟踪器处于活动状态,我们将修补 ftrace_caller() 函数本身以调用相关的特定跟踪器。这就是 ftrace_call 标签的意义所在。

考虑到这一点,让我们继续研究实际执行运行时修补的 C 代码。您需要了解一些您架构的操作码,才能顺利完成下一节。

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

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 调用点的地址,该地址在构建期间由 scripts/recordmcount.pl 收集。

最后一个函数用于运行时修补活动跟踪器。这将修改 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_call 位置,并调用 ftrace_graph_caller()

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