分析函數調用關係圖(call graph)的幾種方法

繪製函數調用關係圖對理解大型程序大有幫助。我想大家都有過一邊讀源碼(並在頭腦中維護一個調用棧),一邊在紙上畫函數調用關係,然後整理成圖的經歷。如果運氣好一點,藉助調試器的單步跟蹤功能和call stack窗口,能節約一些腦力。不過如果要分析的是腳本語言的代碼,那多半隻好老老實實用第一種方法了。如果在讀代碼之前,手邊就有一份調用圖,豈不妙哉?下面舉出我知道的幾種免費的分析C/C++函數調用關係的工具。

函數調用關係圖(call graph)是圖(graph),而且是有向圖,多半還是無環圖(無圈圖)——如果代碼中沒有直接或間接的遞歸的話。Graphviz是專門繪製有向圖和無向圖的工具,所以很多call graph分析工具都以它爲後端(back end)。那麼前端呢?就看各家各顯神通了。

調用圖的分析分析大致可分爲“靜態”和“動態”兩種,所謂靜態分析是指在不運行待分析的程序的前提下進行分析,那麼動態分析自然就是記錄程序實際運行時的函數調用情況了。

靜態分析又有兩種方法,一是分析源碼,二是分析編譯後的目標文件。

分析源碼獲得的調用圖的質量取決於分析工具對編程語言的理解程度,比如能不能找出正確的C++重載函數。Doxygen是源碼文檔化工具,也能繪製調用圖,它似乎是自己分析源碼獲得函數調用關係的。GNU cflow也是類似的工具,不過它似乎偏重分析流程圖(flowchart)。

對編程語言的理解程度最好的當然是編譯器了,所以有人想出給編譯器打補丁,讓它在編譯時順便記錄函數調用關係。CodeViz(其靈感來自Martin Devera (Devik) 的工具)就屬於此類,它(1.0.9版)給GCC 3.4.1打了個補丁。另外一個工具egypt的思路更巧妙,不用大動干戈地給編譯器打補丁,而是讓編譯器自己dump出調用關係,然後分析分析,交給Graphviz去繪圖。不過也有人另起爐竈,自己寫個C語言編譯器(ncc),專門分析調用圖,勇氣可嘉。不如要是對C++語言也這麼幹,成本不免太高了。分析C++的調用圖,還是藉助編譯器比較實在。

分析目標文件聽起來挺高深,其實不然,反彙編的工作交給binutils的objdump去做,只要分析一下反匯編出來的文本文件就行了。下面是Cygwin下objdump -d a.exe的部分結果:

00401050 <_main>:
  401050:       55                      push   %ebp
  401051:       89 e5                   mov    %esp,%ebp
  401053:       83 ec 18                sub    $0x18,%esp
   ......
 40107a:       c7 44 24 04 00 20 40    movl   $0x402000,0x4(%esp)
  401081:       00
  401082:       c7 04 24 02 20 40 00    movl   $0x402002,(%esp)
  401089:       e8 f2 00 00 00          call   401180 <_fopen>

從中可以看出,main()調用了fopen()。CodeViz帶有分析目標文件的功能。

動態分析是在程序運行時記錄函數的調用,然後整理成調用圖。與靜態分析相比,它能獲得更多的信息,比如函數調用的先後順序和次數;不過也有一定的缺點,比如程序中語句的某些分支可能沒有執行到,這些分支中調用的函數自然就沒有記錄下來。

動態分析也有兩種方法,一是藉助gprof的call graph功能(參數-q),二是利用GCC的 -finstrument-functions 參數。

gprof生成的輸出如下:

index % time    self  children    called     name
                0.00    0.00       4/4           foo [4]
[3]      0.0    0.00    0.00       4         bar [3]
-----------------------------------------------
                0.00    0.00       1/2           init [5]
                0.00    0.00       1/2           main [45]
[4]      0.0    0.00    0.00       2         foo [4]
                0.00    0.00       4/4           bar [3]
-----------------------------------------------
                0.00    0.00       1/1           main [45]
[5]      0.0    0.00    0.00       1         init [5]
                0.00    0.00       1/2           foo [4]
-----------------------------------------------

從中可以看出,bar()被foo()調用了4次,foo()被init()和main()各調用了一次,init()被main()調用了一次。用Perl腳本分析gprof的輸出,生成Graphviz的dot輸入,就能繪製call graph了。這樣的腳本不止一個人寫過:http://www.graphviz.org/Resources.phphttp://www.ioplex.com/~miallen/

GCC的-finstrument-functions 參數的作用是在程序中加入hook,讓它在每次進入和退出函數的時候分別調用下面這兩個函數:

void __cyg_profile_func_enter( void *func_address, void *call_site )
                                __attribute__ ((no_instrument_function));

void __cyg_profile_func_exit ( void *func_address, void *call_site )
                                __attribute__ ((no_instrument_function));

當然,這兩個函數本身不能被鉤住(使用no_instrument_function這個__attribute__),不然就反反覆覆萬世不竭了:) 這裏獲得的是函數地址,需要用binutils中的addr2line這個小工具轉換爲函數名,如果是C++函數,還要用c++filt進行name demangle。具體方法在《

用Graphviz 可視化函數調用》中有詳細介紹,這裏不再贅述。

 

從適應能力上看,源碼分析法是最強的,即便源碼中有語法錯,頭文件不全也沒關係,它照樣能分析個八九不離十。而基於編譯器的分析法對源碼的要求要高一些,至少能編譯通過(gcc 參數 -c)——能產生object file,不一定要鏈接得到可執行文件。這至少要求源碼沒有語法錯,其中調用的函數不一定有定義(definition),但要有聲明(declaration),也就是說頭文件要齊全。當然,真的不全也沒關係,自己放幾個函數聲明在前面就能糊弄編譯器:) 至於動態分析,要求最高——程序需得運行起來。如果你要分析的是操作系統中某一部分,比如內存管理或網絡協議棧,那麼這裏提到的兩種動態分析法恐怕都不適用了。

我發現前面列舉的所有免費工具幾乎都和GCC、GNU Binutils脫不了干係。這裏在把它們整理一下,用Graphviz繪成圖:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章