Linux內核ftrace原理

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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容