天天看點

使用gcc的-finstrument-functions選項進行函數跟蹤【轉】

版權聲明:本文為部落客原創文章,轉載請附上原博連結。

GCC Function instrumentation機制可以用來跟蹤函數的調用關系,在gcc中對應的選項為“-finstrument-functions”。可檢視gcc的man page來擷取更詳細資訊。

編譯時如果為gcc加上“-finstrument-functions”選項,那在每個函數的入口和出口處會各增加一個額外的hook函數的調用,增加的這兩個函數分别為:

void __cyg_profile_func_enter (void *this_fn, void *call_site);  

void __cyg_profile_func_exit  (void *this_fn, void *call_site);  

其中第一個參數為目前函數的起始位址,第二個參數為傳回位址,即caller函數中的位址。

這是什麼意思呢?例如我們寫了一個函數func_test(),定義如下:

static void func_test(v)  

{  

    /* your code... */  

}  

那通過-finstrument-functions選項編譯後,這個函數的定義就變成了:

    __cyg_profile_func_enter(this_fn, call_site);  

    __cyg_profile_func_exit(this_fn, call_site);  

我們可以按照自己的需要去實作這兩個hook函數,這樣我們就可以利用this_fn和call_site這兩個參數大做文章。

例如下面這段代碼:

instrfunc.c:   

#include <stdio.h>  

#define DUMP(func, call) \  

    printf("%s: func = %p, called by = %p\n", __FUNCTION__, func, call)  

void __attribute__((no_instrument_function))  

__cyg_profile_func_enter(void *this_func, void *call_site)  

    DUMP(this_func, call_site);  

__cyg_profile_func_exit(void *this_func, void *call_site)  

int do_multi(int a, int b)  

    return a * b;  

int do_calc(int a, int b)  

    return do_multi(a, b);  

int main()  

    int a = 4, b = 5;  

    printf("result: %d\n", do_calc(a, b));  

    return 0;  

這段代碼中實作了兩個hook函數,即列印出所在函數的函數位址以及傳回位址。

編譯代碼:

[zhenfg@ubuntu]code:$ gcc -finstrument-functions instrfunc.c -o instrfunc  

[zhenfg@ubuntu]code:$ ./instrfunc   

__cyg_profile_func_enter: func = 0x8048521, called by = 0xb75554e3  

__cyg_profile_func_enter: func = 0x80484d8, called by = 0x8048562  

__cyg_profile_func_enter: func = 0x804849a, called by = 0x8048504  

__cyg_profile_func_exit: func = 0x804849a, called by = 0x8048504  

__cyg_profile_func_exit: func = 0x80484d8, called by = 0x8048562  

result: 20  

__cyg_profile_func_exit: func = 0x8048521, called by = 0xb75554e3  

通過反彙編的代碼(objdump -D instrfunc)可以看到,這些位址和函數的對應關系為:

__cyg_profile_func_enter: func = 0x8048521(main), called by = 0xb75554e3  

__cyg_profile_func_enter: func = 0x80484d8(do_calc), called by = 0x8048562(main)  

__cyg_profile_func_enter: func = 0x804849a(do_multi), called by = 0x8048504(do_calc)  

__cyg_profile_func_exit: func = 0x804849a(do_multi), called by = 0x8048504(do_calc)  

__cyg_profile_func_exit: func = 0x80484d8(do_calc), called by = 0x8048562(main)  

__cyg_profile_func_exit: func = 0x8048521(main), called by = 0xb75554e3  

實際上這就給出了函數的調用關系。

如果不想跟蹤某個函數,可以給該函數指定“no_instrument_function”屬性。需要注意的是,__cyg_profile_func_enter()和__cyg_profile_func_exit()這兩個hook函數是一定要加上“no_instrument_function”屬性的,不然,自己跟蹤自己就會無限循環導緻程式崩潰,當然,也不能在這兩個hook函數中調用其他需要被跟蹤的函數。

得到一系列的位址看起來不太直覺,我們更希望看到函數名,幸運的是,addr2line工具為我們提供了這種可能。我們先看一下addr2line的使用方法:

[zhenfg@ubuntu]code:$ addr2line --help  

Usage: addr2line [option(s)] [addr(s)]  

 Convert addresses into line number/file name pairs.  

 If no addresses are specified on the command line, they will be read from stdin  

 The options are:  

  @<file>                Read options from <file>  

  -a --addresses         Show addresses  

  -b --target=<bfdname>  Set the binary file format  

  -e --exe=<executable>  Set the input file name (default is a.out)  

  -i --inlines           Unwind inlined functions  

  -j --section=<name>    Read section-relative offsets instead of addresses  

  -p --pretty-print      Make the output easier to read for humans  

  -s --basenames         Strip directory names  

  -f --functions         Show function names  

  -C --demangle[=style]  Demangle function names  

  -h --help              Display this information  

  -v --version           Display the program's version  

首先要注意,使用addr2line工具時,需要用gcc的“-g”選項編譯程式增加調試資訊。

同樣是上面的程式,我們加上-g選項再編譯一次:

[zhenfg@ubuntu]code:$ gcc -g -finstrument-functions instrfunc.c -o instrfunc  

__cyg_profile_func_enter: func = 0x8048521, called by = 0xb757d4e3  

__cyg_profile_func_exit: func = 0x8048521, called by = 0xb757d4e3  

使用addr2line嘗試查找0x8048504位址所在的函數:

[zhenfg@ubuntu]code:$ addr2line -e instrfunc -a 0x8048504 -fp -s  

0x08048504: do_calc at instrfunc.c:25  

這樣一來,就可以通過gcc的“-finstrument-functions”選項結合addr2line工具,友善的對一個程式中的函數進行跟蹤。并且既然我們可以自己實作hook函數,那不僅僅可以用來跟蹤函數調用關系,你可以在hook函數中添加自己想做的事情,例如添加一些統計資訊。

另外,我們知道__builtin_return_address(level)宏可以獲得不同層級的函數傳回位址,但是在某些體系架構(如mips)中,__builtin_return_address(level)隻能獲得目前函數的直接調用者的位址,即level隻能是0,那這時,就可使用上述方法來跟蹤函數調用關系(mips中竟然能用,确實有些小吃驚)。

接下來可以看一下gcc是如何将hook函數嵌入各個函數中的,以反彙編代碼中的do_multi()函數為例(這是mips的彙編代碼),在mips中,ra寄存器用來存儲傳回位址,a0-a3用來做函數參數。

004006c8 <do_multi>:  

  4006c8:   27bdffd8    addiu   sp,sp,-40  

  4006cc:   afbf0024    sw  ra,36(sp)   ;;存儲ra寄存器(傳回位址)的值  

  4006d0:   afbe0020    sw  s8,32(sp)  

  4006d4:   afb1001c    sw  s1,28(sp)  

  4006d8:   afb00018    sw  s0,24(sp)  

  4006dc:   03a0f021    move    s8,sp  

  4006e0:   03e08021    move    s0,ra   ;;s0 = ra  

  4006e4:   afc40028    sw  a0,40(s8)  

  4006e8:   afc5002c    sw  a1,44(s8)  

  4006ec:   02001021    move    v0,s0   ;;v0 = s0  

  4006f0:   3c030040    lui v1,0x40  

  4006f4:   246406c8    addiu   a0,v1,1736  ;;将本函數的位址指派給a0寄存器  

  4006f8:   00402821    move    a1,v0       ;;将傳回位址ra的值指派給a1寄存器  

  4006fc:   0c100188    jal 400620 <__cyg_profile_func_enter> ;;調用hook函數  

  400700:   00000000    nop  

  400704:   8fc30028    lw  v1,40(s8)  

  400708:   8fc2002c    lw  v0,44(s8)  

  40070c:   00000000    nop  

  400710:   00620018    mult    v1,v0  

  400714:   00008812    mflo    s1  

  400718:   02001021    move    v0,s0  

  40071c:   3c030040    lui v1,0x40  

  400720:   246406c8    addiu   a0,v1,1736  ;;将本函數的位址指派給a0寄存器  

  400724:   00402821    move    a1,v0       ;;将傳回位址ra的值指派給a1寄存器  

  400728:   0c10019d    jal 400674 <__cyg_profile_func_exit> ;;調用hook函數  

  40072c:   00000000    nop  

  400730:   02201021    move    v0,s1  

  400734:   03c0e821    move    sp,s8  

  400738:   8fbf0024    lw  ra,36(sp)   ;;恢複ra寄存器(傳回位址)的值  

  40073c:   8fbe0020    lw  s8,32(sp)  

  400740:   8fb1001c    lw  s1,28(sp)  

  400744:   8fb00018    lw  s0,24(sp)  

  400748:   27bd0028    addiu   sp,sp,40  

  40074c:   03e00008    jr  ra  

  400750:   00000000    nop  

上述反彙編的代碼中,使用“-finstrument-functions”選項編譯程式所增加的指令都已注釋出來,實作沒什麼複雜的,在函數中獲得自己的位址和上一級caller的位址并不是什麼難事,然後将這兩個位址傳給__cyg_profile_func_enter和__cyg_profile_func_exit就好了。

【新浪微網誌】 張昺華--sky

【twitter】 @sky2030_

【facebook】 張昺華 zhangbinghua

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.

繼續閱讀