1. 程式人生 > >看開原始碼利器—用Graphviz + CodeViz生成C/C++函式呼叫圖(call graph)

看開原始碼利器—用Graphviz + CodeViz生成C/C++函式呼叫圖(call graph)

一、Graphviz + CodeViz簡單介紹

CodeViz是《Understanding The Linux Virtual Memory Manager》的作者 Mel Gorman 寫的一款分析C/C++原始碼中函式呼叫關係的open source工具(類似的open source軟體有 egyptncc)。其基本原理是給 GCC 打個補丁(如果你的gcc版本不符合它的要求還得先下載正確的gcc版本),讓它在編譯每個原始檔時 dump 出其中函式的 call graph,然後用 Perl 指令碼收集並整理呼叫關係,轉交給Graphviz繪製圖形(Graphviz

屬於後端,CodeViz屬於前端)。

CodeViz 原本是作者用來分析 Linux virtual memory 的原始碼時寫的一個小工具,現在已經基本支援 C++ 語言,最新的 1.0.9 版能在 Windows + Cygwin 下順利地編譯使用。

基本介紹就到這兒,如果你對其原理比較感興趣,可以參考這篇文章:egypt分析函式呼叫關係圖(call graph)的幾種方法

二、Graphviz + CodeViz編譯安裝

1. 安裝 GraphViz

呼叫圖的生成依賴於 GraphViz,所以首先要安裝 GraphViz。可以下載原始碼包編譯、安裝(下載主頁:http://www.graphviz.org/Download.php

)。
如果是Ubuntu系統可以直接apt安裝: sudo apt-get install graphviz

2. 安裝 CodeViz

下載CodeVize原始碼包:http://www.csn.ul.ie/~mel/projects/codeviz/
解壓:tar xvf codeviz-1.0.12.tar.gz (目前最新版是1.0.12)

進入解壓後的目錄:cd codeviz-1.0.12/

CodeViz 使用了一個 patch 版本的 GCC 編譯器,而且不同的 CodeViz 版本使用的GCC 版本也不同,可以下載 CodeViz 的原始碼包後檢視 Makefile 檔案來確定要使用的 GCC 版

本,codeviz-1.0.12 使用 GCC-4.6.2。實際上安裝 CodeViz 時安裝指令碼make會檢查當前的GCC版本如果不符合則會自動下載對應的 GCC並打 patch,但由於GCC較大如果網速不好且在虛擬機器中的話容易下載失敗或系統錯誤什麼的,因此這裡我們還是分步安裝比較好,先安裝gcc再回來安裝 CodeViz。

 

(1)安裝 GCC

下載gcc-4.6.2.tar.gz到 cd codeviz-1.0.12目錄下的compilers裡。
下載地址:ftp://ftp.gnu.org/pub/gnu/gcc/gcc-4.6.2/gcc-4.6.2.tar.gz

CodeViz 的安裝指令碼 compilers/install_gcc-4.6.2.sh 會自動檢測 compilers 目錄下是否有 gcc 的原始碼包,若沒有則自動下載並打 patch。這裡前面已經下載,直接移到該目錄即可,則剩下的就是解壓安裝了。install_gcc-3.4.6.sh 會解壓縮 gcc打 patch,並將其安裝到指定目錄,若是沒有指定目錄,則預設使用$HOME/gcc-graph,通常指定安裝在/usr/
local/gcc-graph(這時需要 root 許可權)。

安裝: ./install_gcc-4.6.2.sh

注意:這裡可能安裝時有些錯誤,具體錯誤及解決方案見後面。


(2)安裝 CodeViz
./configure && make install-codeviz

注1:不需要 make ,因為make的作用就是檢測是否有gcc若沒有則下載原始碼包,所以這裡只要安裝 codeviz 即可。具體檢視 Makefile 檔案。

注意:這裡為什麼不是通常用的make install,因為這裡make install的作用是先安裝gcc再安裝codeviz,而前面已經安裝了 gcc,所以這裡只需要安裝 codeviz ,即make install-codeviz指令碼,該指令碼也就是將genfull 和 gengraph 複製到/usr/local/bin 目錄下。

目前為止,CodeViz 安裝完成了。

三、基本使用方法

GraphViz 支援生成不同風格的呼叫圖,但是一些需要安裝額外的支援工具或者庫程式,有興趣的朋友可以到官網上查詢相關資料。這裡重點講述 CodeViz 的使用方法,具體的影象風格控制不再詳述。

CodeViz 使用兩個指令碼來生成呼叫圖,一個是 genfull,該指令碼可以生成專案的完整呼叫圖,因此呼叫圖可能很大很複雜,預設使用 cdepn 檔案來建立呼叫圖;另一個是gengraph,該指令碼可以對給定一組函式生成一個小的呼叫圖,還可以生成對應的postscript 檔案。安裝時這兩個指令碼被複制到/usr/local/bin 目錄下,所以可以直接使用而不需要指定路徑。其基本步驟如下:

下面以編譯一個簡單的test.c檔案為例進行說明:

1. 使用剛剛安裝的gcc-4.6.2來編譯當前目錄下所有.c檔案,gcc/g++為編譯的每個 C/C++檔案生成.cdepn 檔案。只要編譯(引數 -c)就行,無需連結。

即為:$ ~/gcc-graph/bin/gcc test.c


2.呼叫genful會在當前目錄生成一個full.graph檔案,該指令碼可以生成專案的完整呼叫圖資訊檔案,記錄了所有函式在原始碼中的位置和它們之間的呼叫關係。 因此呼叫圖資訊檔案可能很大很複雜,,預設使用 cdepn 檔案來建立呼叫圖資訊檔案。

即為:$ genfull

 

3. 使用gengraph可以對給定一組函式生成一個小的呼叫圖,顯示函式呼叫關係。

即為:$ gengraph

四、簡單示例演示

自己編寫個簡單的程式,看下效果再說~~~

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

// test.c

#include <stdio.h>

 

void test1();

void test2();

void test3();

 

int main()

{

    test1();

    test2();

 

    return 0;

}

 

void test1()

{

}

 

void test2()

{

    test2();

}

 

void test3()

{

}

 

 

按照上面的三個步驟依次進行如下圖所示:

開啟main.ps看到效果如下,一目瞭然:

 

五、進階使用

當然大家使用CodeViz都不是用來玩的,而是用於真正的專案中,四中簡單的使用根本不夠,下面來點稍微高深點的。

1. 先來分析下上面的執行流程

首先使用剛剛安裝的gcc編譯我們的.c檔案(PS:這裡一定要指定剛剛安裝gcc的 地方,否則用的是系統gcc而非我們安裝的gcc),然後genfull建立full.graph檔案,可以使用genfull --help或者genfull --man來檢視如何使用。最簡單的方式是在專案的頂級目錄以無引數方式執行。由於專案的完全呼叫資訊非常龐大,所以通常只是簡單的生成專案的 full.graph,然後在後面使用genfull獲取需要的呼叫資訊。若是需要完整資訊則將full.graph由dot處理然後檢視來生成的 postscript檔案。(dot是GraphViz中的一個工具,具體使用沒有深究過,感興趣的讀者可以自行查閱~~~)。到test.c所在目錄運 行genfull看到生成了full.graph檔案,大家可以用cat檢視下。接下來使用gengraph生成函式呼叫圖,可以使用gengraph --help或gengraph --man來檢視如何使用。對於我而言,目前只關注下面幾個選項就夠了,即:

-f:指定頂級函式,即入口函式,如main等(當然不限定是main了);

-o:指定輸出的postfile檔名,不指定的話就是函式名了,如上面的main;

--output-type:指定輸出型別,例如png、gif、html和ps,預設是ps,如上面的main.ps;

-d:指定最大呼叫層數;

-s:僅僅顯示指定的函式,而不對其呼叫進行展開;

-i:忽略指定的函式

-t:忽略Linux特有的核心函式集;

-k:保留由-s忽略的內部細節形成的中間檔案,為sub.graph

2. 使用gengraph時的選項引數值要使用""括起來,例如:

gengraph --output-type "png" -f main

3. 命名衝突問題

在一個複雜的專案中,full.graph並不十分完美。例如,kernel中的模組 有許多同名函式,這時genfull不能區分它們,有兩種方法可以解決,其中第一種方法太複雜易錯不推薦使用,這裡就介紹下第二種方法,即使用 genfull的-s選項,-s指定了檢測哪些子目錄。例如kernel中在mm目錄和drivers/char/drm目錄下都定義了 alloc_pages函式,那麼可以以下列方式呼叫genfull:

genfull -s "mm include/linux drivers/block arch/i386"

實際的使用中,-s非常方便,請大家記住這個選項。

4. 使用Daemon/Client模式

當full.graph很大時,大量的時間花費到讀取輸入檔案上了,例如kernel的full.graph是很大的,前面生成的大約有15M,這還不是全部核心的函式呼叫分析資訊。為了節省時間,可以講gengraph以daemon方式執行,這藥使用-p選項:

gengraph -p -g linux-2.6.25/full.graph

該命令返回時gengraph以daemon方式執行,同時在/tmp目錄下生成了codeviz.pipe檔案。要生成函式呼叫圖,可以使用-q選項:

gengraph -q -t -d 2 -f alloc_pages

要終止gengraph的執行,使用如下命令:

echo QUIT > /tmpcodeviz.pipe

六、進階演示

以分析《嵌入式實時作業系統 uC/OS-II (第二版)》中的第一個範例程式為例,是什麼程式不要緊,這裡主要看的是如何使用及使用後的效果。

首先分析main():

1. gengraph --output-type gif -f main
分析main()的call graph,得到的圖如下,看不出要領:

2. gengraph --output-type gif -f main -s OSInit
暫時不關心OSInit()的內部實現細節(引數 -s),讓它顯示為一個節點。得到的圖如下,有點亂,不過好多了:

3. gengraph --output-type gif -f main -s OSInit -i "OSCPUSaveSR;OSCPURestoreSR"
基本上每個函式都會有進入/退出臨界區的程式碼,忽略之(引數 -i)。得到的圖如下,基本清楚了:

4. gengraph --output-type gif -f main -s "OSInit;OSSemCreate" -i "OSCPUSaveSR;OSCPURestoreSR" -k
OSSemCreate()的內部細節似乎也不用關心,不過保留中間檔案sub.graph(引數 -k),得到的圖如下,

5. dot -Tgif -o main.gif sub.graph
修改sub.graph,使圖形符合函式呼叫順序,最後得到的圖如下,有了這個都不用看程式碼了:)

接著分析OSTimeDly()的被呼叫關係:

gengraph --output-type gif -r -f OSTimeDly

看看哪些函式呼叫了OSTimeDly(),引數 -r ,Task()和TaskStart()都是使用者編寫的函式:

最後看看Task()直接呼叫了哪些函式:

gengraph --output-type gif -d 1 -f Task

只看從Task出發的第一層呼叫(引數 -d 1):

 

 

七、安裝過程出現的錯誤及解決方案

1. 在執行./install_gcc-4.6.2.sh時出現下面錯誤:

gcc configure: error: Building GCC requires GMP 4.2+, MPFR 2.3.1+ and MPC 0.8.0+

從錯誤中可以看出:GCC編譯需要GMP, MPFR, MPC這三個庫(有的系統已經安裝了就沒有這個提示,我的沒有安裝),有兩種安裝方法(建議第二種):

(1)二進位制原始碼安裝(強烈不推薦)

我使用的版本為gmp-4.3.2,mpfr-2.4.2和mpc-0.8.1,在“ftp://gcc.gnu.org/pub/gcc/infrastructure/" 下載,根據提示的順序分別安裝GMP,MPFR和MPC(mpfr依賴gmp,mpc依賴gmp和mpfr),這裡全部自己指定了安裝目錄,如果沒有指定 則預設分裝在在/usr/include、/usr/lib和/usr/share,管理起來不方便,比如想解除安裝的時候還得一個個去找:

安裝gmp: ./configure --prefix=/usr/local/gmp-4.3.2; make install

安裝mpfr: ./configure --prefix=/usr/local/mpfr-2.4.2 --with-gmp=/usr/local/gmp-4.3.2/; make install

安裝mpc: ./configure --prefix=/usr/local/mpc-0.8.1 --with-gmp=/usr/local/gmp-4.3.2/ --with-mpfr=/usr/local/mpfr-2.4.2/; make install

PS:安裝過程中可能又出現新的錯誤提示,請看2、3、4條。

配置環境變數:我這裡指定了安裝位置,如果沒有指定則 這幾個庫的預設位置是/usr/local/include和/usr/local/lib,不管有沒有指定GCC編譯時都可能會找不到這三個庫,需要確 認庫位置是否在環境變數LD_LIBRARY_PATH中,檢視環境變數內容可以用命令
echoechoLD_LIBRARY_PATH
設定該環境變數命令如下:

指定安裝:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/gmp-4.3.2/lib:/usr/local/mpfr-2.4.2/lib:/usr/local/mpc-0.8.1/lib

預設安裝:exportLDLIBRARYPATH="exportLDLIBRARYPATH="LD_LIBRARY_PATH:/usr/local/lib

PS:十分不推薦這種安裝方法,一般來說這樣的確可以成功安裝,但是也不排除安裝過程中又出現新的問題,具體看問題5。

 

(2)gcc自帶指令碼安裝(強烈推薦)

方法(1)的安裝方法十分繁瑣,安裝過程中可能出現各種預料不到的新錯誤,因此gcc 原始碼包中自帶了一個gcc依賴庫安裝指令碼download_prerequisites,位置在gcc原始碼目錄中的 contrib/download_prerequisites,因此只需要進入該目錄,直接執行指令碼安裝即可:. /download_prerequisites

PS:該指令碼內容如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

#! /bin/sh

 

# Download some prerequisites needed by gcc.

# Run this from the top level of the gcc source tree and the gcc

# build will do the right thing.

#

# (C) 2010 Free Software Foundation

#

# This program is free software: you can redistribute it and/or modify

# it under the terms of the GNU General Public License as published by

# the Free Software Foundation, either version 3 of the License, or

# (at your option) any later version.

#

# This program is distributed in the hope that it will be useful, but

# WITHOUT ANY WARRANTY; without even the implied warranty of

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

# General Public License for more details.

#

# You should have received a copy of the GNU General Public License

# along with this program. If not, see http://www.gnu.org/licenses/.

 

MPFR=mpfr-2.4.2

GMP=gmp-4.3.2

MPC=mpc-0.8.1

 

wget ftp://gcc.gnu.org/pub/gcc/infrastructure/$MPFR.tar.bz2 || exit 1

tar xjf $MPFR.tar.bz2 || exit 1

ln -sf $MPFR mpfr || exit 1

 

wget ftp://gcc.gnu.org/pub/gcc/infrastructure/$GMP.tar.bz2 || exit 1

tar xjf $GMP.tar.bz2  || exit 1

ln -sf $GMP gmp || exit 1

 

wget ftp://gcc.gnu.org/pub/gcc/infrastructure/$MPC.tar.gz || exit 1

tar xzf $MPC.tar.gz || exit 1

ln -sf $MPC mpc || exit 1

 

rm $MPFR.tar.bz2 $GMP.tar.bz2 $MPC.tar.gz || exit 1

可見是通過wget的方式下載安裝,因此如果沒有安裝wget則需要先安裝下。

大家仔細看下這個指令碼,發現非常簡單,就是從網上自動下載三個依賴庫並解壓,然後建立三個改名後的軟連結分別指向這三個庫,這裡建立軟連結過程中也可能出錯,具體看問題6,大家也可以自己修改指令碼,改成直接修改名稱然後移到gcc目錄下。

技巧:從這裡也可以看出,gcc所依賴的庫其實只要解壓了放在gcc當前目錄下就行了,方法(1)的那麼多步驟其實都可以省掉,直接將下載的三個壓縮包解壓後改名移到gcc下面即可,也不用設定環境變量了。

 

2. 編譯gmp時出現錯誤:

No usable m4 in $PATH or /usr/5bin (see config.log for reasons).
由此可以看出是缺少M4檔案。可以去這裡下載:http://ftp.gnu.org/gnu/m4/然後編譯安裝,我由於是Ubuntu系統,就直接

sudo apt-get install m4安裝了。

 

3. 安裝mpfr時出現錯誤:

configure: error: gmp.h can't be found, or is unusable.

這是因為在安裝mpfr時未先安裝gmp導致的,mpfr依賴於gmp。

 

4. 安裝mpc時出現錯誤:

configure: error: libgmp not found or uses a different ABI.和configure: error: libmpfr not found or uses a different ABI.“。

同樣是因為未安裝mpc依賴的庫gmp和mpfr。

 

5. 在執行./install_gcc-4.6.2.sh過程中出現錯誤,即按照gcc過程中出現的問題:

(1)libmpfr.so.1: cannot open shared object file: No such file or directory

分析:該指令碼就是安裝gcc,但是如果你出現了問題1,並且使用方法(1)解決該問題,那麼你後期就可能出現這樣的問題,當然你運氣沒那麼背的話一般不會出現這樣的問題,反正我執行比較背,出現了這樣的問題。

解決方法:可以參考這篇文章http://blog.csdn.net/leo115/article/details/7671819解決。

 

(2)../../gcc-4.6.2/gcc/realmpfr.h:27:17: fatal error: mpc.h: No such file or directory
分析:gcc沒找到所依賴的庫mpc,原因很多,最有可能是你沒設定環境變數或mpc放的地方不對。

解決方法:設定環境變數,看問題1。

 

(3) /usr/include/stdc-predef.h:30:26: fatal error: bits/predefs.h: No such file or directory

分析:用命令“locate bits/predefs.h”找下該標頭檔案的路徑,發現是在'/usr/include/x86_64-linux-gnu'
解決方法:設定環境變數:
#export C_INCLUDE_PATH=/usr/include/i386-linux-gnu && export CPLUS_INCLUDE_PATH=$C_INCLUDE_PATH

 

(4) /usr/bin/ld: cannot find crti.o: No such file or directory

分析:同樣用“locate crti.o” 找下這個檔案,在'/usr/lib/i386-linux-gnu/crti.o'。

解決方法:設定LIBRARY_PATH (LDFLAGS)這個環境變數如下:
#export LIBRARY_PATH=/usr/lib/i386-linux-gnu
 

(5)unwind-dw2.c:1031: error: field `info' has incomplete type

分析:這個錯誤搞了好久,因為網上找不到對應的解決方法,只說這是gcc的一個bug。

解決方法:深入到原始檔中,發現錯誤的地方是這樣的:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

static _Unwind_Reason_Code

uw_frame_state_for (struct _Unwind_Context *context, _Unwind_FrameState *fs)

{

  struct dwarf_fde *fde;

  struct dwarf_cie *cie;

  const unsigned char *aug, *insn, *end;

  

  memset (fs, 0, sizeof (*fs));

  context->args_size = 0;

  context->lsda = 0;

  

  fde = _Unwind_Find_FDE (context->ra - 1, &context->bases); //這裡返回了NULL

  if (fde == NULL)

    {

      /* Couldn't find frame unwind info for this function.  Try a

     target-specific fallback mechanism.  This will necessarily

     not profide a personality routine or LSDA.  */

#ifdef MD_FALLBACK_FRAME_STATE_FOR

      MD_FALLBACK_FRAME_STATE_FOR (context, fs, success); // 出錯的地方

      return _URC_END_OF_STACK;

    success:

      return _URC_NO_REASON;

#else

      return _URC_END_OF_STACK;  //出錯返回

#endif

    }

.....

}

 

 

出錯的地方用標註了,因為fde返回了NULL,導致不能找到frame unwind info,最重要的是下面這個方法

1

MD_FALLBACK_FRAME_STATE_FOR (context, fs, success); 

出錯了,為什麼返回NULL我肯定研究不出來,只知道這個函式呼叫失敗了,導致不成功,於是我的解決方法十分偷懶,就是將下面的兩行註釋掉了,直接success,哈哈,勿噴我,因為這樣做過後就解決了,後面一路成功~~~

1

2

// MD_FALLBACK_FRAME_STATE_FOR (context, fs, success); // 出錯的地方 

// return _URC_END_OF_STACK;

 

6.  解決ln -s 軟連結產生Too many levels of symbolic links錯誤

從網上查找了一下原因,原來是建立軟連線的時候採用的是相對路徑,所以才會產生這樣的錯誤,解決方式是採用絕對路徑建立軟連結:這樣問題就解決了。

八、小結

本文查閱了網上的許多資料比較詳細的講解了CodeViz的安裝和使用。CodeViz依賴於GraphViz,因而可以生成十分豐富的函式呼叫圖。具體選項的使用及影象格式的選擇可由讀者根據個人需要和偏好自己揣摩使用。在分析原始碼的時候,把這些圖形列印在手邊,在上面做筆記,實在方便收益頗多。

九、參考資料:

1. http://blog.csdn.net/delphiwcdj/article/details/9936717

2. http://www.cppblog.com/hacrwang/archive/2007/06/30/27296.html

3. http://www.cnblogs.com/xuxm2007/archive/2010/10/14/1851086.html