函数追踪器设计¶
- 作者:
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 位置