可以將以圖形形式查看應用程序的調用過程看作是一個學習經歷。這樣做可以幫助您理解應用程序的內部行為,并獲得有關程序優化方面的信息。例如,通過對那些經常調用的函數進行優化,您就可以用最少的努力來獲得最佳的性能。另外,調用跟蹤還可以判斷用戶函數的最大調用深度,這可以用來對調用棧使用的內存進行有效限制(在嵌入式系統中,這是非常重要的一個考慮因素)。
為了捕獲并顯示調用圖,您需要 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.map 0x08048258 __libc_start_main@@GLIBC_2.0 0x08048258 main $ addr2line 0x08048258 -e test -f main /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: $ ls 10: graph.dot test trace.txt 11: instrument.c test.c 12: $ dot -Tjpg graph.dot -o graph.jpg 13: $ ls 14: graph.dot instrument.c test.c 15: graph.jpg test trace.txt 16: $ |
這個過程的示例輸出如圖 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 |