Linux内核ftrace原理 (-pg -mfentry -fpic)

gcc的-pg选项

ftrace 支持动态trace,即可以跟踪内核和模块中任意的全局函数。它利用了gcc的-pg编译选项,在每个函数的开始增加一个stub,这样在需要的时候可以控制函数跳转到指定的代码中去执行。用过gprof工具应该对gcc的-pg选项不陌生了。

  • 当CONFIG_FUNCTION_TRACER打开时,编译时会增加-pg编译选项,gcc会在每个函数的入口处增加对mcount的调用。
  • gcc 4.6新增加了-pg -mfentry支持,这样可以在函数的最开始插入一条调用fentry的指令。
[root@localhost kernel-4.4.27]# echo 'void foo(){}' | gcc -x c -S -o - - -pg -mfentry

foo:
.LFB0:
    .cfi_startproc
    call    __fentry__
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

通过nm可以看到多了一个未定义的符号fentry

                 U __fentry__
0000000000000000 T foo

对于动态ftrace,有一个很重要的工作就是记录这些被-pg影响的函数,最终可以通过读debugfs的文件/sys/kernel/debug/tracing/available_filter_functions来查看哪些函数是支持trace的。

编译内核

内核在编译代码时,先指定-pg -fentry选项编译生成.o文件,然后通过scripts/recordmcount.pl脚本来处理.o文件

以一个简单的foo.c文件举例

static void foo() {}
static void foo2() {}
static void foo3() {}

经过scripts/recordmcount.pl处理之后,.o文件中新增了一个__mcount_loc段,在最终链接时被重定向,里面记录了所有插入了mcount或者fentry的函数地址。

[root@localhost kernel-4.4.27]# objdump -s foo.o

Contents of section __mcount_loc:
 0000 00000000 00000000 00000000 00000000  ................
 0010 00000000 00000000                    ........    
[root@localhost kernel-4.4.27]# objdump -r foo.o

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000001 R_X86_64_PC32     __fentry__-0x0000000000000004
000000000000000c R_X86_64_PC32     __fentry__-0x0000000000000004
0000000000000017 R_X86_64_PC32     __fentry__-0x0000000000000004


RELOCATION RECORDS FOR [__mcount_loc]:
OFFSET           TYPE              VALUE 
0000000000000000 R_X86_64_64       foo
0000000000000008 R_X86_64_64       foo+0x000000000000000b
0000000000000010 R_X86_64_64       foo+0x0000000000000016

最终内核的链接脚本include/asm-generic/vmlinux.lds.h将__mcount_loc段的内容放在.init.data段中,并且通过__start_mcount_loc和__stop_mcount_loc两个全局符号来访问。

#define MCOUNT_REC()    . = ALIGN(8);                           \
                        VMLINUX_SYMBOL(__start_mcount_loc) = .; \
                        *(__mcount_loc)                         \
                        VMLINUX_SYMBOL(__stop_mcount_loc) = .;
[root@localhost kernel-4.4.27] objdump -t vmlinux -j .init.data | egrep "__start_mcount_loc|__stop_mcount_loc"
ffffffff817109e0 g       .init.data 0000000000000000 __stop_mcount_loc
ffffffff816fb0c0 g       .init.data 0000000000000000 __start_mcount_loc

ftrace初始化

gcc的-pg -mfentry选项在每个函数开始处增加了一条callq指令,它和对应的retq据统计会带来13%的性能开销,因此在内核的初始化阶段将这些callq指令全部修改为5 Byte的NOP指令: 66 66 66 66 90H,同时将这些指令的地址记录下来。

  • scripts/recordmcount.pl过滤了kernel/trace/ftrace.o,没有为其增加__mcount_loc段,所以ftrace代码不会修改其自身的代码。
  • ftrace_init在start_kernel中调用,早于kernel_init,此时不会有其它Core正在执行代码,因此也不用担心修改指令导致其它Core出现crash(系统运行时修改指令就要麻烦很多:被修改的指令正在其它Core上执行,5个字节的指令有可能跨两个cache line)。
  • 由于ftrace_init执行时间较早,所以.initcall中的初始化函数都是可以被trace的(在cmdline中增加"ftrace_filter="参数来指定要trace的函数)。
void __init ftrace_init(void)
{
    extern unsigned long __start_mcount_loc[];
    extern unsigned long __stop_mcount_loc[];
    unsigned long count;

    count = __stop_mcount_loc - __start_mcount_loc;

    ret = ftrace_process_locs(NULL,
                  __start_mcount_loc,
                  __stop_mcount_loc);
}

在ftrace_process_locs函数中,内核为__start_mcount_loc和__stop_mcount_loc之间的每个地址都创建一个struct dyn_ftrace结构,其中ip记录着函数开始的stub地址,ftrace_code_disable函数会将这个地址的内容替换为nop指令,这样在没有trace时,系统的性能几乎没有影响。

struct dyn_ftrace {
    unsigned long       ip; /* address of mcount call-site */
    unsigned long       flags;
    struct dyn_arch_ftrace  arch;
};

当开始trace时,内核根据函数名找到ip,将该地址处的nop指令修改为call指令,以控制其跳转到指定的位置。

模块

编译模块时会用到内核源码树中的Makefile和.config文件(实际上是根据.config生成的include/config/auto.conf文件),如果内核源码树中的配置打开了CONFIG_FUNCTION_TRACER,那么在编译模块时也会增加-pg -mfentry,并将影响了的函数地址保存在__mcount_loc段中。

在加载.ko时首先根据模块放置的实际地址为__mcount_loc段重定向,并记录在mod->ftrace_callsites中,最后同样会调用ftrace_process_locs函数来处理。

如果当前运行的内核打开了CONFIG_FUNCTION_TRACER,但编译module时未打开,实际上编出来的.ko也能加载,只是其中的函数都不支持trace。

附:scripts/recordmcount.pl实现

首先是逐行处理objdump -hdr foo.o, 将插入了mcount或者fentry的函数地址记录到一个临时的.s文件中,并将临时.s文件编译成.o文件并和原来的.o文件链接到一起

[root@localhost kernel-4.4.27]# cat .tmp_mc_foo.s 
    .section __mcount_loc,"a",@progbits
    .align 8
    .quad foo + 0
    .quad foo + 11

需要注意的是如果.o文件中的第一个函数是static或者weak,需要先通过objcopy --globalize-symbol将其转换为全局符号,然后再和上面的.tmp_mc_foo.o一起链接

$cc -o $mcount_o -c $mcount_s

$objcopy $globallist $inputfile $globalobj

$ld -r $globalobj $mcount_o -o $globalmix

$objcopy $locallist $globalmix $inputfile

在动态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同时工作

未完待续



作者:goldhorn
链接:https://www.jianshu.com/p/d25ebe8f023a
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。


作者:goldhorn
链接:https://www.jianshu.com/p/56a96de4e879
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

猜你喜欢

转载自blog.csdn.net/yiyeguzhou100/article/details/85007982