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