内核ftrace的实现

在动态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 : 
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

内核提供了.ftrace_caller.ftrace_regs_caller两段汇编代码作为wrapper,用来完成保存/恢复寄存器等通用的工作,其中的call ftrace_stub会被修改为各种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)

但是内核也没有直接调用.ftrace_caller.ftrace_regs_caller,而是在内存中构造了一个trampoline,将.ftrace_caller拷贝到这段trampoline中,并修改其中的相对偏移。

多个tracer同时工作

未完待续

你可能感兴趣的:(内核ftrace的实现)