原文連結
可以將以圖形形式查看應用程式的調用過程看作是一個學習經曆。這樣做可以協助您理解應用程式的內部行為,並獲得有關程式最佳化方面的資訊。例如,通過對那些經常調用的函數進行最佳化,您就可以用最少的努力來獲得最佳的效能。另外,調用跟蹤還可以判斷使用者函數的最大調用深度,這可以用來對調用棧使用的記憶體進行有效限制(在嵌入式系統中,這是非常重要的一個考慮因素)。
為了捕獲並顯示調用圖,您需要 4 個元素:GNU 編譯器工具鏈、Addr2line 工具、定製的中間代碼和一個名為 Graphviz 的代碼。Addr2line 工具可以識別函數、給定地址的原始碼行數和可執行映像。定製的中間代碼是一個非常簡單的工具,它可以減少對圖形規範的地址跟蹤。Graphviz 工具可以產生圖形映像。整個過程 1 所示。
圖 1. 搜集、簡化和可視化跟蹤路徑的過程
資料搜集:捕獲函數調用路徑
要收集一個函數調用的蹤跡,您需要確定每個函數在應用程式中調用的時間。在過去,都是通過在函數的入口處和退出處插入一個惟一的符號來手工檢測每個函數的。這個過程非常繁瑣,而且很容易出錯,通常需要對原始碼進行大量的修改。
幸運的是,GNU 編譯器工具鏈(也稱為 gcc)提供了一種自動檢測應用程式中的各個函數的方法。在執行應用程式時,就可以收集相關的分析資料。您只需要提供兩個特殊的分析函數即可。其中一個函數在每次執行想要跟蹤的函數時都會調用;而另外一個函數則在每次退出想要跟蹤的函數時調用(參見清單 1)。這兩個函數都是特別指定的,因此,編譯器可以識別它們。
清單 1. GNU 的入口和出口配置函數
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)); |
避免使用特殊的檢測函數
您或許會產生疑惑,如果 gcc 就是我們需要的檢測函數,那麼為什麼它不檢測 __cyg_* 分析函數呢?gcc 的開發人員曾思考過這個問題,他們提供了一個名為no_instrument_function 的函數屬性,這個函數屬性可以應用於函數原型,禁止對它們進行檢測。不要將這個函數屬性應用到分析函數上,這樣會導致無限遞迴分析迴圈和大量的無用資料。
在調用一個檢測函數時,__cyg_profile_func_enter 同時也會被調用,並以 func_address 形式傳遞調用的函數地址,以及從中調用該函數的call_site 形式的地址。反之,當一個函數退出時,也會調用__cyg_profile_func_exit 函數,並傳遞 func_address 形式的函數地址,以及函數從中退出的真真實位址,該地址的表示形式為 call_site。
在這些分析函數中,您可以記錄下地址對,以供以後再進行分析使用。要請求 gcc 所有的檢測函數,每個檔案都必須使用 -finstrument-functions 和 -g 選項進行編譯,這樣可以保留偵錯符號。
因此,現在您就可以為 gcc 提供一些分析函數了,這些函數可以透明地插入應用程式中的函數進入點和函數退出點。但在調用分析函數時,又應該怎樣處理所提供的地址呢?您有很多選擇,但是為了簡便起見,可以將這個地址簡單地寫入一個檔案,要注意哪個地址是函數的入口地址,哪個地址是函數的出口地址(參見清單 2)。
注意:在清單 2 中並沒有使用調用 Callsite 資訊,因為這些資訊對於剖析器來說是不必要的。
清單 2. 分析函數
void __cyg_profile_func_enter( void *this, void *callsite ){ /* Function Entry Address */ fprintf(fp, "E%p\n", (int *)this);}void __cyg_profile_func_exit( void *this, void *callsite ){ /* Function Exit Address */ fprintf(fp, "X%p\n", (int *)this);} |
現在您可以搜集分析資料了,但是您應該在什麼地方開啟或關閉您的跟蹤輸出檔案呢?到現在為止,還不需要為了進行分析而對來源程式進行任何修改。因此,您該如何檢測整個應用程式(包括 main 函數)而不用對分析資料的輸出結果進行初始化呢?gcc 的開發人員也考慮過這個問題,它們為 main 函數的 constructor 函數和 destructor 函數提供了一些碰巧能夠滿足這個要求一些方法。constructor函數是在調用 main 函數之前調用的,而 destructor 函數則是在應用程式退出時調用的。
要建立 constructor 和 destructor 函數,則需要聲明兩個函數,然後對這兩個函數應用 constructor 和 destructor 函數屬性。在constructor 函數中,會開啟一個新的追蹤檔案,分析資料的地址跟蹤就是寫入這個檔案的;在 destructor 函數中,會關閉這個追蹤檔案(參見清單 3)。
清單 3. 分析 constructor 和 destructor 函數
/* Constructor and Destructor Prototypes */void main_constructor( void )__attribute__ ((no_instrument_function, constructor));void main_destructor( void )__attribute__ ((no_instrument_function, destructor));/* Output trace file pointer */static FILE *fp;void main_constructor( void ){ fp = fopen( "trace.txt", "w" ); if (fp == NULL) exit(-1);}void main_deconstructor( void ){ fclose( fp );} |
如果編譯分析函數(在 instrument.c)並將它們與目標應用程式連結在一起,然後再執行目標應用程式,結果會產生一個應用程式的調用追蹤,追蹤記錄被寫入 trace.txt 檔案。追蹤檔案與調用的應用程式處於相同的目錄中。最終結果是,您可能會得到一個其中滿是地址的非常大的檔案。為了能夠讓這些資料更有意義,您可以使用一個不太出名的叫做 Addr2line 的 GNU 工具。
回頁首
使用 Addr2line 將函數位址解析為函數名
Addr2line 工具(它是標準的 GNU Binutils 中的一部分)是一個可以將指令的地址和可執行映像轉換成檔案名稱、函數名和原始碼行數的工具。這種功能對於將跟蹤地址轉換成更有意義的內容來說簡直是太棒了。
要瞭解這個過程是怎樣工作的,我們可以實驗一個簡單的互動例子。(我直接從 shell 中進行操作,因為這是最簡單地展示這個過程的方法,如清單 4 所示。)這個樣本 C 檔案(test.c)是通過 cat 一個簡單的應用程式實現的(也就是說,將標準輸出的文本重新導向到一個檔案中)。然後使用 gcc 來編譯這個檔案,它會傳遞一些特殊的選項。首先,要(使用 -Wl 選項)通知連結器產生一個映像檔案,並(使用 -g 選項)通知編譯器產生偵錯符號。最終產生可執行檔 test。得到新的可執行應用程式之後,您就可以使用grep 工具在映像檔案中尋找 main 來尋找它的地址了。使用這個地址和 Addr2line 工具,就可以判斷出函數名(main)、源檔案(/home/mtj/test/test.c)以及它在源檔案中的行號(4)。
在調用 Addr2line 工具時,要使用 -e 選項來指定可執行映像是 test。通過使用 -f 選項,可以告訴工具輸出函數名。
清單 4. addr2line 的一個互動式例子
$ cat >> test.c#include <stdio.h>int main(){ printf("Hello World\n"); return 0;}<ctld-d>$ gcc -Wl,-Map=test.map -g -o test test.c$ grep main test.map0x08048258__libc_start_main@@GLIBC_2.00x08048258main$ addr2line 0x08048258 -e test -fmain/home/mtj/test/test.c:4$ |
Addr2line 和調試器
Addr2line 工具提供了基本的符號調試資訊,不過 GNU Debugger (GDB)使用的是其他一些內部方法。
回頁首
精簡函數跟蹤資料
現在您有了一個可以搜集合函式函數地址的追蹤資料的方法,還可以使用 Addr2line 工具將地址轉換為函數名。然而,從應用程式中產生大量的跟蹤資料之後,如何對這些資料進行精簡,從而使其更有意義呢?這就是使用一些定製的中間代碼在開源工具之間建立聯絡的地方。本文提供了這個工具(Pvtrace)的帶有注釋的完整代碼,包括如何編譯和使用該工具的一些說明。(有關的更多資訊,請參閱 下載 一節。)
回想一 1 中的內容,在執行設定了檢測函數的應用程式時,會建立一個名為 trace.txt 的文字檔。這個人們可以讀取的檔案中包含了一系列地址資訊 —— 每行一個地址,每行都有一個前置詞字元。如果首碼是 E,那麼這個地址就是一個函數的入口地址(也就是說,您正在調用這個函數)。如果首碼是一個 X 字元,那麼這個地址就是一個出口地址(也就是說,您正在從這個函數中退出)。
因此,如果在追蹤檔案中有一個入口地址(A)緊跟著另外一個入口地址(B),那麼您就可以推斷是 A 調用了 B。如果一個入口地址(A)後面跟著一個出口地址(A),那麼就說明這個函數(A)被調用後就直接返回了。當涉及大量的調用鏈時,就很難分析究竟是誰調用了誰,因此,一種簡單的解決方案是維護一個整個地址的堆棧。每次在追蹤檔案中碰到一個入口地址時,就將其壓入堆棧。棧頂的地址就代表最後一次被調用的函數(也就是當前的活動函數)。如果後面緊接著是另外一個入口地址,這說明堆棧中的地址調用了這個剛從追蹤檔案處讀出的地址。在碰到退出函數時,當前的活動函數就會返回,並釋放棧頂元素。這會將上下文返到回前一個函數,由此,就可以產生正確的調用鏈過程。
圖 2 介紹了這個概念,以及精簡資料的方法。在分析追蹤檔案中的調用鏈時,會構建一個連通矩陣,用來表示哪個函數調用了其他哪些函數。這個矩陣的行表示調用函數的地址,列表示被調用的地址。對於每個調用對來說,行與列的交叉點不斷進行累加(調用次數)。當處理完整個追蹤檔案時,其結果是該應用程式的整個調用曆史的一個非常簡單的表示,其中包含了調用的次數。
圖 2. 對跟蹤資料進行處理和精簡,並產生矩陣格式
編譯並安裝工具
在下載並解壓 Pvtrace 工具之後,只需在子目錄中輸入make 命令,就可以編譯 Pvtrace 工具了。也可以使用下面的代碼將這個工具安裝到 /usr/local/bin 目錄中:
$ unzip pvtrace.zip -d pvtrace
$ cd pvtrace
$ make
$ make install
現在我們已經構建了簡化的函數連通性矩陣,接下來應該構建圖形的表示了。讓我們深入研究 Graphviz,了便理解如何從連通矩陣產生一個調用圖。
回頁首
使用 Graphviz
Graphviz 或 Graph Visualization 是由 AT&T 開發的一個開源的圖形視覺化檢視。它提供了多種畫圖能力,但是我們重點關注的是它使用 Dot 語言直連圖的能力。在本文中,我們將簡單介紹如何使用 Dot 來建立一個圖形,並展示如何將分析資料轉換成 Graphviz 可以使用的規範。(請參閱 參考資料 一節,以獲得有關下載這個開源軟體的資訊。)
Dot 使用的圖形規範
使用 Dot 語言,您可以指定三種對象:圖、節點和邊。為了讓您理解這些對象的含義,我們將構建一個例子來展示這些元素的用法。
清單 5 給出了一個簡單的定向圖(directed graph),其中包含 3 個節點。第一行聲明這個圖為 G,並且聲明了該圖的類型(digraph)。接下來的三行代碼用於建立該圖的節點,這些節點分別名為 node1、node2 和 node3。節點是在它們的名字出現在圖規範中時建立的。邊是在在兩個節點使用邊操作(->)串連在一起時建立的,如第 6 行到第 8 行所示。我還對邊使用了一個可選的屬性 label,用它來表示邊在圖中的名稱。最後,在第 9 行完成對該圖規範的定義。
清單 5. 使用 Dot 符號表示的樣本圖(test.dot)
1: digraph G {2: node1;3: node2;4: node3;5:6: node1 -> node2 [label="edge_1_2"];7: node1 -> node3 [label="edge_1_3"];8: node2 -> node3 [label="edge_2_3"];9: } |
要將這個 .dot 檔案轉換成一個圖形映像,則需要使用 Dot 工具,這個工具是在 Graphviz 包中提供的。清單 6 介紹了這種轉換。
清單 6. 使用 Dot 來建立 JPG 映像
$ dot -Tjpg test.dot -o test.jpg$ |
在這段代碼中,我告訴 Dot 使用 test.dot 圖形規範,並產生一個 JPG 映像,將其儲存在檔案 test.jpg 中。所產生的映像 3 所示。在此處,我使用了 JPG 格式,但是 Dot 工具也可以支援其他格式,其中包括 GIF、PNG 和 postscript。
圖 3. Dot 建立的樣本圖
Dot 語言還可以支援其他一些選項,包括外形、顏色和很多屬性。但是就我們想要實現的功能而言,這個選項就足夠了。
回頁首
綜合
現在我們已經看到了整個過程的各個階段了,下面可以採用一個例子來展示如何將這些階段合并在一起了。現在,您應該已經展開並安裝了 Pvtrace 工具,然後還需要將 instrument.c 檔案複製到工作原始碼目錄中。
在這個例子中,我使用了一個源檔案 test.c 進行檢測。清單 7 給出了整個過程。在第 3 行中,我使用檢測源(instrument.c)來構建(編譯並串連)應用程式。然後在第 4 行執行 test,再使用 ls 命令驗證已經產生了 trace.txt 檔案。在第 8 行,我調用了 Pvtrace 工具,並提供這個映像檔案作為它惟一的參數。映像名是必需的,這樣 Addr2line(在 Pvtrace 中調用)就可以訪問這個映像中的調試資訊。在第 9 行中,我又執行了一次 ls 命令,以確保 Pvtrace 產生了 graph.dot 檔案。最後,在第 12 行,使用 Dot 將這個圖形規範轉換成一個 JPG 圖形映像。
清單 7. 建立調用跟蹤圖的整個過程
1: $ ls 2: instrument.c test.c 3: $ $ gcc -g -finstrument-functions test.c instrument.c -o test 4: $ ./test 5: $ ls 6: instrument.c test.c 7: test trace.txt 8: $ pvtrace test 9: $ ls10: graph.dot test trace.txt11: instrument.c test.c12: $ dot -Tjpg graph.dot -o graph.jpg13: $ ls14: graph.dot instrument.c test.c15: graph.jpg test trace.txt16: $ |
這個過程的樣本輸出 4 所示。這個樣本圖是從使用 Q 學習的一個簡單增強式學習應用程式中得到的。
圖 4. 應用程式範例的跟蹤結果
您也可以使用這種方法對更大的應用程式進行分析。我要展示的最後一個例子是 Gzip 工具。我簡單地將 instrument.c 加入 Gzip 的 Makefile 中,作為其依賴的一個源檔案,然後編譯 Gzip,並使用它產生一個追蹤檔案。這個圖形太大了,不太容易進行更詳細的分析,但是表示了 Gzip 對一個小檔案進行壓縮時的處理過程。
圖 5. Gzip 跟蹤結果
回頁首
結束語
使用開源軟體和少量的中間代碼,只需要花很少的時間就可以開發出非常有用的項目。通過使用對應用程式進行分析的幾個 GNU 編譯器擴充,可以使用 Addr2line 工具進行地址轉換,並對 Graphviz 應用程式進行圖形可視化,然後您就可以得到一個程式,該程式可以對應用程式進行分析,並展示一個說明調用鏈的定向圖。通過圖形來查看一個應用程式的調用鏈對於理解應用程式的內部行為來說非常重要。在正確瞭解調用鏈及其各自的頻率之後,這些知識可能對調試和最佳化應用程式非常有用。
回頁首
下載
| 描述 |
名字 |
大小 |
下載方法 |
| Instrumentation source and Pvtrace source |
pvtrace.zip |
4 KB |
HTTP |
關於下載方法的資訊