作者:M. Tim Jones
原文連結:http://www.ibm.com/developerworks/linux/library/l-graphvis/
摘要:你可以花時間在大量的程式碼之間穿梭進而了解function的流程,但當function pointer牽涉其中,並且程式碼非常冗長與糾纏時,這個過程會變得相當困難。本篇文章使用開放原始碼軟體以及一點膠合用的代碼,為你展示如何建立一個動態的function call圖表產生器。
透過圖形化的方式去觀察一個應用程式的呼叫流程是非常具有教育性的經驗。這麼做可以幫助你了解應用程式的內部行為以及獲取對程式進行最佳化的資訊。舉例來說,透過最佳化那些被呼叫最多次的function,你就可以用最少的力氣去得到最大的改進。除此以外,呼叫流程可以識別出使用者的function中最深度的call depth,然後你就可以為stack記憶體作適當的安排使用(在嵌入式系統中,這是一個很重要的考量)。
要獲取並顯示一幅call graph,你需要4個元素:一個GNU toolchain、addr2line程式、一些自定義的膠合代碼、以及一個叫作Graphviz的工具程式。addr2line程式讓你能夠在知道一個執行檔的某個位址後,識別出function名稱以及在源代碼中的行號。自定義的膠合代碼則是一個簡單的工具,可以將位址的追蹤流程轉變成一個圖形的規格。Graphviz則是讓你能夠產生出那些圖形。整個流程如Figure 1所示:
要產生function呼叫的追蹤資訊,你必須知道在程式中的每個function何時被呼叫。在美好的舊時光中,你在每個function的進入點與離開點手動插入可產出獨一無二的符號。這個過程很冗長,令人生厭並且容易出錯,而且還會讓代碼變得混亂。
幸運的是,GNU toolchain(也就是gcc)提供一個為程式自動嵌入自訂function的方式。當被嵌入的程式執行時,profiling程式行為的資料就可以進行收集。你只需提供兩個特殊的此類functions。一個會在被嵌入的function被呼叫時執行;另一個則是當被嵌入的function離開時被呼叫(看Listing 1.)。這些functions有著特別的名稱,所以它們可以被編譯器所識別。
Listing 1. GNU對function進入與離開時的profiling functions
如果profiling functions(在instrument.c中)被編譯並連結進目標應用程式,結果就會是一個你的應用程式所產生的call trace,並存於trace.txt中。這個trace file會在應用程式被呼叫的同一個目錄下。你會獲得一個相當大的檔案,充斥著一堆位址。為了對這些資料有感覺,接著要使用一個較少人知道的GNU工具程式,叫作addr2line。
利用addr2line將function位址轉為function名稱
addr2line(GNU binutils的一部份)是一個將執行檔中的指令位址轉為檔案名稱、function名稱,以及在代碼中所屬行數的工具程式。這個功能對於將trace的位址轉換為某些有意義的東西相當完美。
透過Dot語言,你可以定義三種物件:圖、節點、邊。為了了解這些物件的意思,讓我們建立一個例子來解釋吧。
Listing 5表示一個簡單的directed圖,它有3個節點,以Dot的表示法呈現。第1行宣告了你的圖,叫作G,以及它的類型(一個digraph)。接著的三行建立了圖的節點,叫作node1、node2、node3。當節點的名稱出現在圖的格式中時,它們就會被建立。邊則是透過->運算子將兩個節點合在一起時被建立,如6到8行所示。我也用了一個可選用的邊的屬性 -- label -- 也就是這個邊的名稱。最後,整張圖就在九行內完成了。
為了將這個.dot檔轉成圖檔,你要使用由Graphviz所提供的Dot工具程式,Listing 6展示了這個轉換:
原文連結:http://www.ibm.com/developerworks/linux/library/l-graphvis/
摘要:你可以花時間在大量的程式碼之間穿梭進而了解function的流程,但當function pointer牽涉其中,並且程式碼非常冗長與糾纏時,這個過程會變得相當困難。本篇文章使用開放原始碼軟體以及一點膠合用的代碼,為你展示如何建立一個動態的function call圖表產生器。
透過圖形化的方式去觀察一個應用程式的呼叫流程是非常具有教育性的經驗。這麼做可以幫助你了解應用程式的內部行為以及獲取對程式進行最佳化的資訊。舉例來說,透過最佳化那些被呼叫最多次的function,你就可以用最少的力氣去得到最大的改進。除此以外,呼叫流程可以識別出使用者的function中最深度的call depth,然後你就可以為stack記憶體作適當的安排使用(在嵌入式系統中,這是一個很重要的考量)。
要獲取並顯示一幅call graph,你需要4個元素:一個GNU toolchain、addr2line程式、一些自定義的膠合代碼、以及一個叫作Graphviz的工具程式。addr2line程式讓你能夠在知道一個執行檔的某個位址後,識別出function名稱以及在源代碼中的行號。自定義的膠合代碼則是一個簡單的工具,可以將位址的追蹤流程轉變成一個圖形的規格。Graphviz則是讓你能夠產生出那些圖形。整個流程如Figure 1所示:
Figure 1. 收集追蹤資訊、轉化、並產生圖形的流程
資料收集:追蹤function呼叫要產生function呼叫的追蹤資訊,你必須知道在程式中的每個function何時被呼叫。在美好的舊時光中,你在每個function的進入點與離開點手動插入可產出獨一無二的符號。這個過程很冗長,令人生厭並且容易出錯,而且還會讓代碼變得混亂。
幸運的是,GNU toolchain(也就是gcc)提供一個為程式自動嵌入自訂function的方式。當被嵌入的程式執行時,profiling程式行為的資料就可以進行收集。你只需提供兩個特殊的此類functions。一個會在被嵌入的function被呼叫時執行;另一個則是當被嵌入的function離開時被呼叫(看Listing 1.)。這些functions有著特別的名稱,所以它們可以被編譯器所識別。
Listing 1. GNU對function進入與離開時的profiling functions
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));
|
避免特定的function被嵌入profiling functions
你或許會覺得奇怪,如果gcc會嵌入profiling functions,為何沒有將__cyg_* profiling functions也再度嵌入?gcc的開發者思考過這個問題,並且提供了一個function attribute,叫作no_instrument_function,這可以施於function的原型上,並讓嵌入的效果失效。如果不在profiling functions上給予這兩個attribute,就會造成無限遞迴並且產生一大堆沒用的資料。
當一個被嵌入的function被呼叫時,__cyg_profile_func_enter也會被呼叫,並將該function的位址透過func_address傳入,而呼叫此functions的發生點的位址也會透過call_site傳入。相反地,當一個function離開時,__cyg_profile_func_exit會被呼叫,透過func_address傳入該function的位址,而呼叫此functions的發生點的位址也會透過call_site傳入。
在這些profiling functions中,你可以紀錄這對位址並在稍候分析它們。要讓gcc對所有函式都有嵌入的profiling functions,每個檔案都必須以-finstrument-functions以及-g編譯選項進行編譯以獲得除錯資訊。
所以,現在你可以提供profiling functions給gcc,然後它就會無縫地為你在應用程式的每個function的進入點與離開點插入profiling functions了。但是當profiling functions被呼叫時,你要對獲得的位址做些什麼事呢?你有許多選擇,但為了簡單起見,把位址寫入到一個檔案就好。注意哪個位址是進入點以及離開點(看一下Listing 2.)
注意:callsite的資訊並沒有在Listing 2中使用,因為對此處的profiling並不需要。
(譯註:M. Tim Jones後續利用外部工具addr2line去處理位址,另一個作法是直接在程式碼中處理,可參考這篇文章)
Listing 2. profiling functions
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);
}
|
現在你可以收集profiling data了,但你要在那邊開啟與關閉你的trace output檔案呢?到目前為止,沒有對原本應用程式作修改的需要。所以,如果沒有某種對profiling data輸出的初始化,你要如何嵌入你的應用程式,包括main function呢?gcc開發者也想過這點了,並且提供一個完美的方式來實現main function的constructor與destructor。constructor function會在main之前被呼叫;而destructor function會在應用程式離開時被呼叫。
要建立你自己的constructor與destructor,需宣告兩個functions,然後施以constructor與destuctor的function attribute。在constructor function中,一個trace file被打開,profiling data會在稍候被寫入;在desturctor中,該trace file會被關閉(Listing 3.)。
Listing 3. Profiling constructor以及destructor functions/* 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 );
}
|
如果profiling functions(在instrument.c中)被編譯並連結進目標應用程式,結果就會是一個你的應用程式所產生的call trace,並存於trace.txt中。這個trace file會在應用程式被呼叫的同一個目錄下。你會獲得一個相當大的檔案,充斥著一堆位址。為了對這些資料有感覺,接著要使用一個較少人知道的GNU工具程式,叫作addr2line。
利用addr2line將function位址轉為function名稱
addr2line(GNU binutils的一部份)是一個將執行檔中的指令位址轉為檔案名稱、function名稱,以及在代碼中所屬行數的工具程式。這個功能對於將trace的位址轉換為某些有意義的東西相當完美。
要知道這件事情如何運作,就試驗一個簡單的例子吧。(我直接在shell中操作,因為這是證明這個過程最簡單的方式,看看Listing 4.吧。)範例程式(test.c)透過cat去建立(透過重導向標準輸入到檔案)。這個檔案接著以gcc編譯,並給予一些特殊編譯選項。首先,連結器被指示(透過-Wl)要產生map檔,然後編譯器被指示要產生除錯符號(-g)。結果會產出一個名為test的執行檔。你可以使用grep去尋找在map檔中的main的位址。將這個位址與可執行檔餵給addr2line,你就可以看到function名稱(main)、原始檔案(/home/mtj/test/test.c)、以及在代碼中的行數。
addr2line程式被喚起,透過-e選項識別出執行檔image為test。接著使用-f選項,要求它給出function名稱。
Listing 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
$
|
簡化function trace資料
你現在已經有收集function位址的trace資料,也有辦法透過addr2line將一個位址對應到function名稱。然而,當給你一大陀應用程式產出的trace位址時,你要如何簡化這些資料使得它們是有意義的?這就是自定義的膠合代碼需要填補開放原始碼工具的缺口。隨著此篇文章,我提供了一個對代碼有良好註解的工具程式(Pvtrace),包括了如何建構與使用的說明(查看"資源"那一小節以獲取更多資訊)。
你現在已經有收集function位址的trace資料,也有辦法透過addr2line將一個位址對應到function名稱。然而,當給你一大陀應用程式產出的trace位址時,你要如何簡化這些資料使得它們是有意義的?這就是自定義的膠合代碼需要填補開放原始碼工具的缺口。隨著此篇文章,我提供了一個對代碼有良好註解的工具程式(Pvtrace),包括了如何建構與使用的說明(查看"資源"那一小節以獲取更多資訊)。
回想一下Figure 1,在被嵌入的應用程式執行後,一個名為trace.txt的檔案會被建立。這個可以讓我們閱讀的檔案包含了一大串的位址 -- 一行一個,每個都有一個prefix字元。如果prefix是E,該位址就是function的進入function(該function被呼叫)。如果prefix是X,該位址就是一個離開的位址(就是說,你將離開該function)。
所以啦,如果在該trace檔中,你有一個進入點的位址(A),後面跟著另一個位址(B),你就可以知道是(A)呼叫了(B),如果(A)是跟著一個離開的位址(A),那就是(A)被呼叫,然後返回了。當更長的呼叫發生時,這會變得不易知道誰呼叫了誰,所以一個簡單的解決方法就是維護一個進入點位址的stack。每一次當在trace檔中遇到一個進入點位址時,就將它放進stack中。在stack頂端的位址就表示最近被呼叫的那個function(也就是active function)。如果另一個進入點位址接著出現,就表示在stack中位址呼叫了這個剛從trace檔讀到的位址。當遇到一個離開點位址時,目前的active function就是要返回了,於是就可將在stack頂端的元素去掉一個。然後,此時就回到了前一個function的環境,這就是完成了正確的呼叫流程。
Figure 2將這個概念以資料的化簡法來示意。當呼叫流程從trace檔被解析時,一個connectivity matrix被建立,它會將哪個function呼叫哪個function的關係建立起來。matrix的列表示從哪個位址呼叫,而位於欄的位址表示被呼叫的位址。對每一次的呼叫組合來說,會有一個格子的數字增加1(呼叫次數)。當整個trace檔被讀取完畢並解析後,結果就會是這個精簡的表示法,表現出對整個應用程式呼叫經過,包括呼叫次數。
Figure 2. 解析並簡化trace資料為一個matrix的型式
現在這個精簡的function connectivity matrix已建立起來了,接著就是要花張圖啦。讓我們深入Graphviz,了解一下要如何從一個connectivity matrix去弄出一張圖吧。
(譯註:此處的connectivity matrix應使用某種型式的sparse matrix來避免鉅量的記憶體使用量。)
(譯註:此處的connectivity matrix應使用某種型式的sparse matrix來避免鉅量的記憶體使用量。)
使用Graphviz
Graphviz,或Graph Visiualization,是AT&T的一個開放原始碼圖形工具。他提供了數種圖形的選擇,不過我會聚焦在directed graph的能力,並使用Dot語言。我給你一個很快的概觀,讓你知道如何透過Dot去建立一張圖,並展示如何將profiling資料轉換為Graphviz所需的格式(請參考"資源"那一小節以下載這個工具)。
Dot的Graph格式Graphviz,或Graph Visiualization,是AT&T的一個開放原始碼圖形工具。他提供了數種圖形的選擇,不過我會聚焦在directed graph的能力,並使用Dot語言。我給你一個很快的概觀,讓你知道如何透過Dot去建立一張圖,並展示如何將profiling資料轉換為Graphviz所需的格式(請參考"資源"那一小節以下載這個工具)。
透過Dot語言,你可以定義三種物件:圖、節點、邊。為了了解這些物件的意思,讓我們建立一個例子來解釋吧。
Listing 5表示一個簡單的directed圖,它有3個節點,以Dot的表示法呈現。第1行宣告了你的圖,叫作G,以及它的類型(一個digraph)。接著的三行建立了圖的節點,叫作node1、node2、node3。當節點的名稱出現在圖的格式中時,它們就會被建立。邊則是透過->運算子將兩個節點合在一起時被建立,如6到8行所示。我也用了一個可選用的邊的屬性 -- label -- 也就是這個邊的名稱。最後,整張圖就在九行內完成了。
Listing 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檔轉成圖檔,你要使用由Graphviz所提供的Dot工具程式,Listing 6展示了這個轉換:
Listing 6. 使用Dot以產生JPG圖檔
$ dot -Tjpg test.dot -o test.jpg
$
|
在這段代碼,我指示了Dot去利用我的test.dot然後產出一張JPG圖檔,test.jpg。這張圖在Figure 3.,我使用JPG格式,不過Dot支援其他圖檔格式,包括GIF、PNG、以及postscript。
Figure 3. Dot產生的範例圖檔
Dot語言還支援其他選項,包括形狀、顏色、以及一堆屬性。不過對我想要完成的東西來說,一點選項就很好用了。
將這些片斷串起來
現在你已經看到整個流程的每個片斷了,一個完整的例子來說明這個流程可以將它們全部串在一起。現在,你應該已經解開並安裝好Pvtrace工具程式了。你也應該有將instrument.c複製到你要觀察的原始碼目錄中。
現在你已經看到整個流程的每個片斷了,一個完整的例子來說明這個流程可以將它們全部串在一起。現在,你應該已經解開並安裝好Pvtrace工具程式了。你也應該有將instrument.c複製到你要觀察的原始碼目錄中。
在此例中,我有一個原始檔叫作test.c,我打算將它嵌入。Listing 7展示了整個過程。在第3行,我建立(編譯並連結)應用程式。我在第四行執行test,然後用 ls 確認trace.txt有產生出來。在第八行,我喚起pvtrace,然後餵給它一個image作為它的唯一參數。這個image名稱是要給add2line用的(在pvtrace中會使用addr2line),所以addr2line就可以獲取image中的除錯資訊。在第九行,我使用了另一個 ls 去確認pvtrace有產出graph.dot。最後,在第12行,使用dot將圖形規格轉為JPG圖檔。
Listing 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: $
|
這個流程的範例圖檔在Figure 4中呈現。這個範例圖檔是從一個簡單的、使用Q learning方式強化學習的應用程式擷取出來。
Figure 4. 範例程式的追蹤結果
你也可以對更多程式使用這個方法。我給的最後一個例子是gzip工具程式。我很簡單的加進instrument.c到gzip的Makefile相依關係中,建置它,然後用gzip去產生出一個trace檔。這個圖檔太大了以至於無法呈現出太多細節,不過這張圖表現了gzip如何壓縮一個小檔的整個經過。
你也可以對更多程式使用這個方法。我給的最後一個例子是gzip工具程式。我很簡單的加進instrument.c到gzip的Makefile相依關係中,建置它,然後用gzip去產生出一個trace檔。這個圖檔太大了以至於無法呈現出太多細節,不過這張圖表現了gzip如何壓縮一個小檔的整個經過。
Figure 5. gzip的追蹤結果
結論
利用開放原始碼軟體以及一點點的膠合代碼,你可以在很短的時間內開發出有趣並且有用的專案。透過一些GNU編譯器對應用程式進行profiling的extensions、addr2line解析位址的能力、以及Graphviz產生圖表的功能,你可以獲得一個可以對應用程式的呼叫流程的圖形。能夠以圖形方式觀看呼叫流程是很有用的,可以對內部行為更加理解。這種知識對於除錯與最佳化應用程式都有很大的幫助。
利用開放原始碼軟體以及一點點的膠合代碼,你可以在很短的時間內開發出有趣並且有用的專案。透過一些GNU編譯器對應用程式進行profiling的extensions、addr2line解析位址的能力、以及Graphviz產生圖表的功能,你可以獲得一個可以對應用程式的呼叫流程的圖形。能夠以圖形方式觀看呼叫流程是很有用的,可以對內部行為更加理解。這種知識對於除錯與最佳化應用程式都有很大的幫助。
資源
- Download the instrumentation and Pvtrace source code described in this article.
- See the latest GNU Compiler Collection (GCC) documentation.
- Visit the
Addr2line
main page at gnu.org to learn more about the Addr2line utility. - Learn about the other GNU Binutils at gcc.org.
- Explore other possibilities at the Graphviz Web site.
- The manual for the Dot utility outlines the tool's other capabilities.
- Discover the Gzip utility and its capabilities.
- Read GNU/Linux Application Programming by M. Tim Jones to learn more about GNU/Linux and open source tools.
- "Mastering Linux debugging techniques" (developerWorks, August 2002) outlines techniques for debugging in the context of four problem scenarios.
- "Kernel debugging with Kprobes" (developerWorks, August 2004) shows how to use Kprobes to dynamically insert printk's to assist in Linux kernel debugging.
- Find more resources for Linux developers in the developerWorks Linux zone.
- Get involved in the developerWorks community by participating in developerWorks blogs.
- Browse for books on these and other technical topics.
- Order the SEK for Linux, a two-DVD set containing the latest IBM trial software for Linux from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
- Innovate your next Linux development project with IBM trial software, available for download directly from developerWorks.
留言
張貼留言