函数跟踪器设计

作者:

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

体系结构可以向函数的进入和退出都传入一个唯一的值(帧指针)。在退出时,会对该值进行比较,如果不匹配,则会使内核产生 panic。这在很大程度上是对 gcc 不良代码生成的一种健全性检查。如果你所移植的 gcc 在不同的优化级别下能正常地更新帧指针,则可以忽略此选项。

然而,添加对它的支持并不是非常困难。在你调用 prepare_ftrace_return() 的汇编代码中,将帧指针作为第 3 个参数传递。然后在该函数的 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_DYNAMIC_FTRACE

有关更多信息,请参见 scripts/recordmcount.pl。只需填入关于如何通过 objdump 定位 mcount 调用点地址的体系结构特定细节即可。如果不实现动态 ftrace,这个选项就没有太大意义。

你首先需要 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;
}

<details to be filled>

最后,你需要自定义的 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 值是在构建期间由 scripts/recordmcount.pl 收集的 mcount 调用点的地址。

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

ftrace_update_ftrace_func()

HAVE_DYNAMIC_FTRACE + HAVE_FUNCTION_GRAPH_TRACER

函数图跟踪器需要进行一些调整才能与动态 ftrace 配合工作。基本上,你需要

  • update
    • ftrace_caller()

    • ftrace_graph_call()

    • ftrace_graph_caller()

  • 实现
    • ftrace_enable_ftrace_graph_caller()

    • ftrace_disable_ftrace_graph_caller()

<details to be filled>

简要说明

  • 在名为 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() 将用 nop 在运行时修补 ftrace_graph_call 位置