Linux内核ftrace原理 (-pg -mfentry -fpic)
gcc的-pg选项ftrace 支持动态trace,即可以跟踪内核和模块中任意的全局函数。它利用了gcc的-pg编译选项,在每个函数的开始增加一个stub,这样在需要的时候可以控制函数跳转到指定的代码中去执行。用过gprof工具应该对gcc的-pg选项不陌生了。当CONFIG_FUNCTION_TRACER打开时,编译时会增加-pg编译选项,gcc会在每个函数的入口处增加对mcount的调...
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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
更多推荐
所有评论(0)