ftrace function_graph 函数劫持实现方式文章目录ftrace function_graph 函数劫持实现方式准备阶段gcc -pg 编译插桩ftrace 模块初始化过滤设置过滤 set_ftrace_filter过滤规则生效逻辑graph 启动函数起始 trace_graph_entry函数返回 hook trace_graph_return总结准备阶段gcc -pg 编译插桩内核编译阶段当开启CONFIG_FUNCTION_TRACER配置项后gcc的-pg编译选项会在内核所有函数的入口处插入一条call _mcount指令同时将所有被插入该hook指令的函数地址、对应的指令偏移位置信息完整记录下来。# The arch Makefiles can override CC_FLAGS_FTRACE. We may also append it later. ifdef CONFIG_FUNCTION_TRACER CC_FLAGS_FTRACE : -pg endifftrace 模块初始化在内核启动的ftrace初始化流程中ftrace_init会调用ftrace_process_locs将编译阶段记录的所有带_mcount插桩的函数地址完成解析与持久化存储随后ftrace_process_locs会调用ftrace_update_code将所有函数入口处的call _mcount指令替换为nop空指令完成初始状态的无害化处理避免未开启追踪时产生性能开销。过滤该部分在 ftrace 具体实现上不是重点可跳过设置过滤 set_ftrace_filter用户态用于配置函数过滤规则的/sys/kernel/debug/tracing/set_ftrace_filter接口其对应的内核文件操作方法定义如下linux-5.10.202/kernel/trace/ftrace.c: 5706 static const struct file_operations ftrace_filter_fops { .open ftrace_filter_open, // 初始化工作 .write ftrace_filter_write, // 计算过滤 hash .release ftrace_regex_release, // 实际保存 };用户配置的过滤规则会被解析处理其中符合追踪条件的函数会被加入ops-func_hash-filter_hash哈希表而明确排除、不需要追踪的函数则会被加入ops-func_hash-notrace_hash哈希表。linux-5.10.202/kernel/trace/ftrace.c: 4107 static int ftrace_hash_move_and_update_ops(struct ftrace_ops *ops, struct ftrace_hash **orig_hash, struct ftrace_hash *hash, int enable) { struct ftrace_ops_hash old_hash_ops; struct ftrace_hash *old_hash; int ret; old_hash *orig_hash; old_hash_ops.filter_hash ops-func_hash-filter_hash; old_hash_ops.notrace_hash ops-func_hash-notrace_hash; ret ftrace_hash_move(ops, enable, orig_hash, hash); return ret; }该哈希表更新逻辑由ftrace_filter_fops-release回调触发执行完整调用栈如下#0 ftrace_hash_move_and_update_ops (ops0xffffffff82c458a0 global_ops, orig_hashorig_hashentry0xffffffff82c458d0 global_ops48, hash0xffff888009518ac0, enableenableentry1) at kernel/trace/ftrace.c:4116 #1 0xffffffff811e5341 in ftrace_regex_release (inodeoptimized out, file0xffff888007f29000) at kernel/trace/ftrace.c:5675 #2 0xffffffff8134fea6 in __fput (file0xffff888007f29000) at fs/file_table.c:281过滤规则生效逻辑过滤规则的最终生效发生在ftrace追踪启动的流程中核心调用栈如下#0 __ftrace_hash_rec_update (opsopsentry0xffffffff82c48580 graph_ops, filter_hashfilter_hashentry1, incincentrytrue) at kernel/trace/ftrace.c:1721 #1 0xffffffff811e3528 in __ftrace_hash_rec_update (inctrue, filter_hash1, ops0xffffffff82c48580 graph_ops) at kernel/trace/ftrace.c:1651 #2 ftrace_hash_rec_enable (filter_hash1, ops0xffffffff82c48580 graph_ops) at kernel/trace/ftrace.c:1817 #3 ftrace_startup (opsopsentry0xffffffff82c48580 graph_ops, commandcommandentry8) at kernel/trace/ftrace.c:2899 #4 0xffffffff811e6782 in ftrace_startup (opsopsentry0xffffffff82c48580 graph_ops, commandcommandentry8) at kernel/trace/ftrace.c:2917 #5 0xffffffff8120539d in register_ftrace_graph (gopsgopsentry0xffffffff82c48200 funcgraph_ops) at kernel/trace/fgraph.c:629 #6 0xffffffff811fffb8 in graph_trace_init (troptimized out) at kernel/trace/trace_functions_graph.c:308 #7 0xffffffff811f7de0 in tracer_init (tr0xffffffff82c45be0 global_trace, t0xffffffff82db8900 graph_trace) at kernel/trace/trace.c:5887ftrace_startup是追踪启动的核心入口会先完成过滤规则的校验与标记再执行指令替换操作linux-5.10.202/kernel/trace/ftrace.c: 2865 int ftrace_startup(struct ftrace_ops *ops, int command) { if (ftrace_hash_rec_enable(ops, 1)) // 过滤追踪和不追踪的函数设置到 rec-flags command | FTRACE_UPDATE_CALLS; ftrace_startup_enable(command); // 替换 nop 指令这里会检查 rec-flags实际替换指令除了发生在常规被gcc -pg记录的位置外还有一些跟踪点之类的位置过滤规则的核心标记逻辑在__ftrace_hash_rec_update函数中实现该函数会遍历所有可追踪的函数记录根据过滤哈希表更新对应记录的flags计数static bool __ftrace_hash_rec_update(struct ftrace_ops *ops, int filter_hash, bool inc) // 过滤追踪和不追踪的函数设置到 rec-flags { struct ftrace_hash *hash; struct ftrace_hash *other_hash; struct ftrace_page *pg; struct dyn_ftrace *rec; bool update false; int all false; /* * In the filter_hash case: * If the count is zero, we update all records. * Otherwise we just update the items in the hash. * * In the notrace_hash case: * We enable the update in the hash. * As disabling notrace means enabling the tracing, * and enabling notrace means disabling, the inc variable * gets inversed. */ if (filter_hash) { // 需要过滤 hash ops-func_hash-filter_hash; // 需要追踪的函数 other_hash ops-func_hash-notrace_hash; // 不需要追踪 } else { inc !inc; } do_for_each_ftrace_rec(pg, rec) { int in_other_hash 0; int in_hash 0; int match 0; if (rec-flags FTRACE_FL_DISABLED) continue; if (all) { ...... } else { in_hash !!ftrace_lookup_ip(hash, rec-ip); // 追踪过滤 in_other_hash !!ftrace_lookup_ip(other_hash, rec-ip); // 不追踪过滤 /* * If filter_hash is set, we want to match all functions * that are in the hash but not in the other hash. * * If filter_hash is not set, then we are decrementing. * That means we match anything that is in the hash * and also in the other_hash. That is, we need to turn * off functions in the other hash because they are disabled * by this hash. */ if (filter_hash in_hash !in_other_hash) // 开启了追踪追踪且不在不追踪里 match 1; } if (!match) // 条件过滤失败 continue; if (inc) { rec-flags; // 启用该函数在后续ftrace_startup_enable调用ftrace_replace_code执行指令替换的核心流程中会校验每个函数对应的struct dyn_ftrace结构体中的flags计数仅当计数大于0时才会对该函数的入口指令执行替换操作从而实现过滤规则的最终生效。graph 启动function_graph追踪能力的核心是通过注册fgraph_ops结构体中的入口与返回钩子函数实现对函数执行全链路的劫持与追踪。内核默认实现的两套钩子函数定义如下linux-5.10.202/kernel/trace/trace_functions_graph.c: 290 static struct fgraph_ops funcgraph_thresh_ops { // 和 /sys/kernel/debug/tracing/tracing_thresh 有关过滤时间 .entryfunc trace_graph_entry, .retfunc trace_graph_thresh_return, }; static struct fgraph_ops funcgraph_ops { .entryfunc trace_graph_entry, .retfunc trace_graph_return, };下文将围绕funcgraph_ops的注册链路分别拆解函数入口hooktrace_graph_entry与函数返回hooktrace_graph_return的完整执行链路。函数起始 trace_graph_entryftrace_push_return_trace是function_graph入口处理的核心函数其核心职责是完成函数调用上下文的保存为后续返回hook提供支撑linux-5.10.202/kernel/trace/fgraph.c: 58 /* Add a function return address to the trace stack on thread info.*/ static int ftrace_push_return_trace(unsigned long ret, unsigned long func, unsigned long frame_pointer, unsigned long *retp) { unsigned long long calltime; int index; calltime trace_clock_local(); // 用于记录函数执行时间 index current-curr_ret_stack; // 函数调用层级将会在函数返回时-- current-ret_stack[index].ret ret; // 返回地址用于函数返回时恢复返回地址 current-ret_stack[index].func func; current-ret_stack[index].calltime calltime;该函数的完整调用栈如下清晰展示了从汇编跳板到入口钩子函数的执行流程#0 ftrace_push_return_trace (retretentry18446744071579750464, funcfuncentry18446744071579749424, retpretpentry0xffffc900001ffec8, frame_pointer0) at kernel/trace/fgraph.c:83 #1 0xffffffff81204e7a in function_graph_enter (retretentry18446744071579750464, funcfuncentry18446744071579749424, frame_pointerframe_pointerentry0, retpretpentry0xffffc900001ffec8) at kernel/trace/fgraph.c:130 #2 0xffffffff8106085c in prepare_ftrace_return (self_addr18446744071579749424, parent0xffffc900001ffec8, frame_pointer0) at arch/x86/kernel/ftrace.c:686 #3 0xffffffff8106115b in ftrace_graph_caller () at arch/x86/kernel/ftrace_64.S:318 #4 0xffff88800c0264c0 in ?? ()调用栈第3层的ftrace_graph_caller是x86架构下的汇编跳板函数核心作用是完成函数入口hook的参数传递与上下文准备。x86架构中call指令执行时会自动将下一条指令的地址即函数返回地址压入栈顶该函数会将栈中的返回地址位置作为参数传入后续的处理函数linux-5.10.202/arch/x86/kernel/ftrace_64.S: 312 #ifdef CONFIG_FUNCTION_GRAPH_TRACER SYM_FUNC_START(ftrace_graph_caller) /* Saves rbp into %rdx and fills first parameter */ save_mcount_regs leaq MCOUNT_REG_SIZE8(%rsp), %rsi /* 返回地址 传入到 prepare_ftrace_return 第二个参数 */ movq $0, %rdx /* No framepointers needed */ call prepare_ftrace_returnprepare_ftrace_return是入口hook的核心逻辑实现函数负责完成函数返回路径的劫持与入口钩子的调用linux-5.10.202/arch/x86/kernel/ftrace.c: 631 void prepare_ftrace_return(unsigned long self_addr, unsigned long *parent, unsigned long frame_pointer) { unsigned long return_hooker (unsigned long)return_to_handler; // 函数返回时的跳板函数汇编 asm volatile( 2: _ASM_MOV %[return_hooker], (%[parent])\n // 替换函数返回地址 function_graph_enter(old, self_addr, frame_pointer, parent) // 继续跳转 ftrace 具体功能处其核心操作分为两步通过内联汇编将栈中保存的函数原始返回地址替换为function_graph返回hook的汇编跳板函数return_to_handler的地址完成函数返回路径的劫持调用function_graph_enter函数最终执行用户注册的入口钩子函数trace_graph_entry完成函数入口的追踪逻辑处理。函数返回 hook trace_graph_return当被追踪的函数执行完成、触发返回指令时会跳转到入口阶段被替换的返回跳板函数return_to_handler。该汇编函数负责完成返回hook的上下文准备、现场保护与恢复最终调用核心返回处理函数linux-5.10.202/arch/x86/kernel/ftrace_64.S: 325 SYM_CODE_START(return_to_handler) UNWIND_HINT_EMPTY subq $16, %rsp /* Save the return values */ movq %rax, (%rsp) movq %rdx, 8(%rsp) movq %rbp, %rdi call ftrace_return_to_handler movq %rax, %rdi // rax 是 ftrace_return_to_handler 返回地址即这个跳板原来应该返回的地址 movq 8(%rsp), %rdx movq (%rsp), %rax addq $16, %rsp // 栈恢复用以等下恢复跳板覆盖地址 /* * Jump back to the old return address. This cannot be JMP_NOSPEC rdi * since IBT would demand that contain ENDBR, which simply isnt so for * return addresses. Use a retpoline here to keep the RSB balanced. */ ANNOTATE_INTRA_FUNCTION_CALL call .Ldo_rop int3 .Ldo_rop: mov %rdi, (%rsp) // 覆盖返回地址即恢复这个跳板覆盖的地址ftrace_return_to_handler是返回hook的核心处理函数负责执行注册的返回钩子、还原函数调用上下文与原始返回地址linux-5.10.202/kernel/trace/fgraph.c: 232 unsigned long ftrace_return_to_handler(unsigned long frame_pointer) { ftrace_pop_return_trace(trace, ret, frame_pointer); // 函数原本返回地址 ftrace_graph_return(trace); // 跳转到具体的 ftrace 功能 current-curr_ret_stack--; // 对应 ftrace_push_return_trace 时函数层级现在层级-- return ret; // 返回真实的返回地址用以覆盖原始返回地址其中ftrace_pop_return_trace负责从进程的调用栈中取出函数入口阶段保存的原始返回地址与上下文信息linux-5.10.202/kernel/trace/fgraph.c: 146 static void ftrace_pop_return_trace(struct ftrace_graph_ret *trace, unsigned long *ret, unsigned long frame_pointer) { *ret current-ret_stack[index].ret; // 函数原本返回地址其核心执行逻辑如下调用ftrace_pop_return_trace从当前进程的ret_stack数组中取出函数入口阶段保存的原始返回地址、函数调用上下文等信息调用注册的返回钩子函数ftrace_graph_return即trace_graph_return执行函数返回的追踪逻辑处理递减当前进程的curr_ret_stack计数完成函数调用层级的回退将函数的原始返回地址返回给汇编跳板用于恢复函数的正常返回流程。总结内核编译阶段gcc-pg选项会为所有函数入口插入call _mcount指令同时将所有被插桩的函数地址与对应指令位置信息完整记录ftrace模块初始化阶段会解析并持久化保存所有插桩函数的地址信息同时将函数入口的call _mcount指令统一替换为nop空指令完成初始状态的无害化处理function_graph追踪启动时会根据配置的过滤规则将目标函数入口的nop指令替换为ftrace入口汇编跳板指令完成函数执行入口的劫持函数入口执行时会通过入口汇编跳板完成上下文保存将函数原始返回地址存储到当前进程的current-ret_stack[index].ret中同时将栈中的返回地址替换为返回hook的汇编跳板函数最终执行function_graph注册的入口钩子函数函数执行完成返回时会跳转到返回hook的汇编跳板执行function_graph注册的返回钩子函数随后从进程的ret_stack中恢复原始返回地址还原函数的正常返回流程