1. 程式人生 > >各類分析函式呼叫關係圖的工具

各類分析函式呼叫關係圖的工具

graphviz (在ubuntu/debian下直接用apt-get安裝即可,需要它的一個dot工具)
   http://www.graphviz.org/

1. introduction

    對於一個C語言編寫的專案,它的框架可以反應為一棵函式呼叫樹。如果在分析專案之前,能夠得到這樣一顆呼叫樹,那麼就可以瞭解專案的整體框架;如果在專案執行之後,能夠跟蹤到該次執行過程中的函式呼叫,那麼將有利於分析某些測試條件下專案的執行流程;而如果在專案執行過程中(比如除錯專案時)能夠跟蹤出某個位置之前的函式呼叫,那麼將有利於確定潛在bug可能存在的位置。

    對於這三種情況,雖然沒有任何一個工具能夠完全滿足,不過"聰明"和"樂於奉獻"的程式設計師們還是分別貢獻了不同的工具:

    無須執行專案本身,calltree就能夠根據整個專案的原始碼產生一棵函式呼叫樹,並可把該呼叫樹匯出為dot格式的圖形。因此可以說calltree能夠在不執行專案的條件下對專案進行函式級別的分析。

    gprof
則能夠在專案執行之後,把該次執行過程中的函式呼叫以文字的形式反應出來,不過善於思考的人們總是喜歡更美好的生活,於是kprof產生了,它不僅可以輔助gprof更好的分析程式程式碼級別的執行情況,而且能夠匯出當前執行過程中的函式呼叫樹,並同樣可以把呼叫樹匯出為dot格式的圖形。

    gdb(Gnu DeBugger),這個應該很熟悉吧,它是一個除錯工具。它提供專門的backtrace命令來跟蹤程式執行到某個位置(比如指定的斷點處)之前的函式呼叫。不過這個目前還是文字輸出的,感興趣的可以hack一下gdb,給它加上漂亮的輸出。
   
    上面提到了DOT格式的圖形。這個DOT[2]是什麼呢?是graphviz[3]定義的一種圖形描述語言,它可以通過graphviz提供的dot工具(安裝graphviz之後就有了)把用DOT描述的圖形轉化為各種其他格式的圖形。雖然有一些專門的DOT圖形瀏覽工具,如dotty,不過這個東西不怎麼好用,所以還是建議通過dot工具轉換為比較常見的圖片格式,如svg,jpg,gif,png,ps,它還可以轉換成dia格式,進而可以通過“超級牛力”的dia繪圖工具來進行進一步的編輯。

2. demo

    下面來介紹這幾個工具的具體用法,更多細節請參考它們自己的文件。
    為了方便演示,這裡寫一個非常“糟糕的”但是卻對這次演示很有用的程式碼。



Code:

  1. /** 
  2.  * test.c --  * a demo program for using calltree, gprof&kprof, gdb&backtrace command 
  3.  * */
  4. void a(void), b(void), c(void), d(void), e(void);  
  5. void a(void) {     b (); }  
  6. void b(void) {     c (); }  
  7. void c(void) {    d (); e (); }  
  8. void d(void) {     ; }  
  9. void e(void) {     ; }  
  10. int main (
    int argc, char **argv)  
  11. {  
  12.     if (argc < 2) {  
  13.         a ();  
  14.     } else {  
  15.         b ();  
  16.     }  
  17. }  


[Ctrl+A Select All]



    對這個程式碼而言,非常容易看出其呼叫關係,即:
    當main的引數個數少於2個時,呼叫關係為 a -> b -> c -> d,e
    當main的引數個數大於3個時,則呼叫關係為b -> c -> d,e
    不過,如果一個專案包含幾十個檔案或者幾千行甚至上萬行程式碼,這個呼叫關係恐怕就沒這麼容易看出來了,所以還得藉助後面的工具。

2.1 不用執行程式就可以列印整個專案的函式呼叫關係圖: calltree

    下載calltree後自己先編譯安裝好,放到/usr/bin下面。然後通過"calltree -help"檢視該工具的幫助,這裡通過使用-mb引數列印以main為樹根的函式呼叫關係圖。
Quote:

$ calltree -mb test.c
main:
|   a
|   |   b
|   |   |   c
|   |   |   |   d
|   |   |   |   e
|   b
|   |   c
|   |   |   d
|   |   |   e


    從這個結果可以非常方便的看出函式呼叫關係,不過還是不夠美觀哦,所以加上-dot引數,產生一個dot圖形吧。
Quote:

$ calltree -mb test.c -dot > test.dot


    okay,現在得到了一個關係調用圖,即test.dot,因為這個格式不太常用,我們給它轉換成jpg,見附圖calltree.jpg。

Quote:

$ dot -Tjpg test.dot -o calltree.jpg



    不過貌似函式d和e沒有打印出來,所以這個應該說是值得改進一下。還好我之前專門寫了一個指令碼,可以產生完整的輸出,這個指令碼見附件 tree2dot.sh.tar.gz,具體原理見資料[5],附圖calltree1.jpg是這個指令碼產生的。這裡簡單介紹它的用法:

Quote:

先通過指令碼tree2dot.sh得到一個DOT圖形
$ calltree -mb test.c | ./tree2dot.sh > test.dot
然後用dot轉換為jpg格式
$ dot -Tjpg test.dot -o calltree1.jpg



2.2 列印專案當次執行過程中的函式呼叫關係圖:gprof& kprof

    首先通過gcc加上-pg引數編譯程式(如果編譯和連結分開,都需要加上該引數)。這個引數就是為了產生一些用於gprof&kprof的資訊。gprof只有字元介面,而kprof提供了圖形介面,下面僅介紹kprof,因為它和gprof相比,可以產生圖形化的函式呼叫關係。

Quote:

$ gcc -pg -o test test.c



    編譯完以後,執行一下就可以產生一個名為gmon.out的檔案,它記錄了該專案當次執行過程中的相關資訊,包括函式呼叫關係。

Quote:

$ ./test
$ls gmon.out
gmon.out



    這樣我們就可以用kprof來得到這個專案在這次執行過程中的函式呼叫關係圖了( 實際上指定./test是為了告訴kprof,gmon.out和test在同一個目錄下,kprof會去找gmon.out)。

Quote:

$ kprof -f ./test



    啟動kprof以後找到Graph View標籤,可以看到一個函式呼叫關係圖。如果要把這個圖匯出來,找到Tools選單,點選Generate Call Graph就可以匯出一個DOT圖形,我們命名為kprof_noargument.dot,然後我們就可以類似2.1用dot工具把它轉換為其他格式,得到的效果圖如kprof_noargument.jpg。

Quote:

$ dot -Tjpg kprof_noargument.dot -o kprof_noargument.jpg



    在上面,我們直接鍵入了./test執行它,如果給它傳遞上兩個引數呢,這個時候argc等於二,在main中就不會再呼叫a函式,而是呼叫c函式,這樣的話,函式呼叫關係圖就不一樣了,這次得到的結果圖如kprof_twoargument.jpg。

Quote:

帶上兩個引數執行test
$ ./test 1 2
通過kprof來檢視呼叫關係圖,並匯出一個名為kprof_twoargument.dot的圖形
$ kprof -f ./test
把DOT圖形轉換為jpg格式
$ dot -Tjpg kprof_twoargument.dot -o kprof_twoargument.jpg



    這裡沒有提到gprof,因為它只產生一些不太好看的文字呼叫關係圖,所以沒有演示,不過它還是有很大作用的,具體參考一下資料[4]吧。
    結合2.1和2.2,我們可以發現calltree和kprof兩者都能夠得到專案的函式呼叫關係圖,不過前者能夠得到整個專案的函式呼叫關係,而後者則能夠得到某次執行過程中的函式呼叫關係,各有不同作用。通過前者我們可以瞭解整個專案的框架;而通過後者,我們可以找出一個專案在某些測試條件下的執行路徑,從而更好地輔助原始碼的分析。
    有時候,這兩種結果還是無法滿足我們的要求,比如在除錯過程中,我們設定了一個斷點,並想了解一下這之前執行過哪些函式,進而找出潛在的bug可能出現的位置。

2.3 專案除錯過程中列印某個位置(如斷點)之前的函式呼叫關係圖:gdb & backtrace command

    為了能夠用gdb除錯程式,編譯時請使用-g選項。

Quote:

$ gcc -g -o test test.c



    通過gdb的backtrace命令列印程式執行到某個位置之前的函式呼叫資訊。

Quote:

$ gdb ./test
...
(gdb) set args 1 2         //這裡設定為兩個引數,所以選擇了路徑b->c->d,e
(gdb) l
6
7       void a(void) {  b (); }
8       void b(void) {  c (); }
9       void c(void) {  d (); e (); }
10      void d(void) {  ; }
11      void e(void) {  ; }
12
13      int main (int argc, char **argv)
14      {
15              if (argc < 2) {
(gdb) break c
Breakpoint 1 at 0x804833c: file test.c, line 9.
(gdb) r
Starting program: /home/falcon/Programming/test 1 2

Breakpoint 1, c () at test.c:9
9       void c(void) {  d (); e (); }
(gdb) backtrace
#0  c () at test.c:9
#1  0x08048334 in b () at test.c:8
#2  0x08048380 in main (argc=3, argv=0xbf924e24) at test.c:18



    在上面的除錯過程中,我們首先通過set命令設定了兩個引數,選擇了main函式的第二個分支,並在c函式的入口設定了一個斷點,然後執行程式直到該斷點處,之後通過backtrace命令打印出之前的函式呼叫資訊。通過最後幾行,我們看到c最後被呼叫,之前是b,再之前是main。

    到這裡,開頭提到的三種情況都介紹完了。不過呢,除了上面這些跟函式關係緊密的工具外,還有一個叫cscope[6]的工具,結合它和vim編輯器,在我們閱讀原始碼的過程中,可以利用它提供的":cs find d function"命令打印出函式function呼叫的所有函式,從而幫助我們瞭解某個函式內部的函式呼叫關係。當然該工具還有更豐富的用法,具體參考資料[6]。
    除了這些分析應用程式的工具外,還有一個叫KFT[7]的工具可以用來分析linux核心。作為linux核心的一個補丁,它能夠跟蹤核心中某個系統呼叫的函式呼叫關係圖,通過KFT提供的一個kd工具,可以得到一個文字格式的函式呼叫關係圖,結合我上面用到的tree2dot.sh(建議用資料[5]中的tree2dot.sh),可以得到一個圖形輸出。

    更多相關資料見後面。有任何建議和疑問,歡迎回帖交流,也可以直接給我發郵件。

PS: QQ不大好使,建議不要加我QQ號。

參考資料

[1][2] The DOT Language
http://www.graphviz.org/doc/info/lang.html
[3] Graphviz - Graph Visualization Software
http://www.graphviz.org/
[4] Coverage Measurement and Profiling
http://www.linuxjournal.com/article/6758
[5][6] cscope
http://cscope.sourceforge.net/
[7] KFT(Kernel Function Tracing)
http://elinux.org/Kernel_Function_Trace
ftp://dslab.lzu.edu.cn/pub/kft