Linux内核ftrace原理

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

你可能感兴趣的:(Linux内核ftrace原理)