在动态ftrace原理中已经介绍了内核通过gcc -pg -fentry为函数增加5 Byte的stub,系统启动后这5 Byte被修改为NOP指令:66 66 66 66 90H。
开始trace时要将NOP指令修改为跳转指令,去执行各种trace对应的hook函数。function trace对应的hook函数就是function_trace_call。
本文将会介绍内核是如何修改代码段以控制函数去执行指定的hook函数。
运行时修改代码段
系统运行时修改代码段是一个很危险的操作,因为被修改的5 Byte有可能跨两个cache line,如果其它Core正在执行,有可能取到被修改了一半的结果,导致系统crash。
ftrace修改代码段是在ftrace_replace_code中完成的,这个函数里有三个大循环
- add_breakpoints: 首先找到需要trace的函数,将第一个字节修改为0xCC,即int 3(也叫break指令)
- add_update: 修改为callq trampoline指令,但是第一个字节保留为0xCC
- finish_update: 将0xCC修改为0xE8,即为call指令
# echo expand_files > set_ftrace_filter
# echo function > current_tracer
以上面的操作举例,配置ftrace跟踪expand_files函数,该函数前5 Byte变化如下面所示:
0xffffffff8114aae0 <expand_files>:
66 66 66 66 90H <-- NOP
|
|
V
CC 66 66 66 90H <-- int 3
|
|
V
CC 1b 55 eb 1eH <-- 跳转的偏移已经修改好了,但opcode还是int 3
|
|
V
e8 1b 55 eb 1eH <-- callq 0xffffffffa0000000
内核在修改代码段时先将第一个Byte修改为0xCC,如果有其它Core执行到这里会触发异常,但是在int 3异常处理程序中直接返回并再次触发异常,直至int 3被修改为call指令后才跳出循环
dotraplinkage void notrace do_int3(struct pt_regs *regs, long error_code)
{
#ifdef CONFIG_DYNAMIC_FTRACE
/*
* ftrace must be first, everything else may cause a recursive crash.
* See note by declaration of modifying_ftrace_code in ftrace.c
*/
if (unlikely(atomic_read(&modifying_ftrace_code)) &&
ftrace_int3_handler(regs))
return;
#endif
...
跳转目标
前面说到trace的原理是修改函数开始的5 Byte,让其先去执行指定的hook函数。不同的tracer有不同的hook函数,function tracer的hook函数是function_trace_call,这个函数的功能比较简单,只是向ring buffer中记录了ip和parent_ip
内核提供了<font color=cornflowerblue>.ftrace_caller</font>和<font color=cornflowerblue>.ftrace_regs_caller</font>两段汇编代码作为wrapper,用来完成保存/恢复寄存器等通用的工作,其中的<font color=cornflowerblue>call ftrace_stub</font >会被修改为各种tracer对应的hook function
ENTRY(ftrace_caller)
/* save_mcount_regs fills in first two parameters */
save_mcount_regs
GLOBAL(ftrace_caller_op_ptr)
/* Load the ftrace_ops into the 3rd parameter */
movq function_trace_op(%rip), %rdx
/* regs go into 4th parameter (but make it NULL) */
movq $0, %rcx
GLOBAL(ftrace_call)
call ftrace_stub
restore_mcount_regs
GLOBAL(ftrace_caller_end)
GLOBAL(ftrace_return)
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
GLOBAL(ftrace_graph_call)
jmp ftrace_stub
#endif
GLOBAL(ftrace_stub)
retq
END(ftrace_caller)
但是内核也没有直接调用<font color=cornflowerblue>.ftrace_caller</font>和<font color=cornflowerblue>.ftrace_regs_caller</font>,而是在内存中构造了一个trampoline,将<font color=cornflowerblue>.ftrace_caller</font>拷贝到这段trampoline中,并修改其中的相对偏移。
多个tracer同时工作
未完待续