gcc的-pg選項
ftrace 支持動態(tài)trace,即可以跟蹤內核和模塊中任意的全局函數。它利用了gcc的-pg編譯選項,在每個函數的開始增加一個stub,這樣在需要的時候可以控制函數跳轉到指定的代碼中去執(zhí)行。用過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
對于動態(tài)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據統(tǒng)計會帶來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正在執(zhí)行代碼,因此也不用擔心修改指令導致其它Core出現crash(系統(tǒng)運行時修改指令就要麻煩很多:被修改的指令正在其它Core上執(zhí)行,5個字節(jié)的指令有可能跨兩個cache line)。
- 由于ftrace_init執(zhí)行時間較早,所以.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之間的每個地址都創(chuàng)建一個struct dyn_ftrace結構,其中ip記錄著函數開始的stub地址,ftrace_code_disable函數會將這個地址的內容替換為nop指令,這樣在沒有trace時,系統(tǒng)的性能幾乎沒有影響。
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