嵌入式linux應用程序調試方法 四 內存工具 17 4.1 MEMWATCH 17 4.2 YAMD 22 4.3 Electric Fence 24 五 C/C++代碼覆蓋、性能profiling工具 24 5.1 用gcov來測試代碼覆蓋率 25 5.2 使用gprof來優化你的C/C++程序 35 四 內存工具 您確定不想陷入相似在幾千次調用以後發生分配溢出這樣的情形。 許多小組花了許許多多時間來跟蹤稀奇古怪的內存錯誤問題。應用程序在有的開發工做站上能運行,但在新的產品工做站上,這個應用程序在調用 malloc() 兩百萬次以後就不能運行了。真正的問題是在大約一百萬次調用以後發生了溢出。新系統之全部存在這個問題,是由於被保留的 malloc() 區域的佈局有所不一樣,從而這些零散內存被放置在了不一樣的地方,在發生溢出時破壞了一些不一樣的內容。 咱們用多種不一樣技術來解決這個問題,其中一種是使用調試器,另外一種是在源代碼中添加跟蹤功能。在我職業生涯的大概也是這個時候,我便開始關注內存調試工具,但願能更快更有效地解決這些類型的問題。在開始一個新項目時,我最早作的事情之一就是運行 MEMWATCH 和 YAMD,看看它們是否是會指出內存管理方面的問題。 內存泄漏是應用程序中常見的問題,不過您可使用本文所講述的工具來解決這些問題。 內存調試工具 C 語言做爲 Linux 系統上標準的編程語言給予了咱們對動態內存分配很大的控制權。然而,這種自由可能會致使嚴重的內存管理問題,而這些問題可能致使程序崩潰或隨時間的推移致使性能降級。 內存泄漏(即 malloc() 內存在對應的 free() 調用執行後永不被釋放)和緩衝區溢出(例如對之前分配到某數組的內存進行寫操做)是一些常見的問題,它們可能很難檢測到。這一部分將討論幾個調試工具,它們極大地簡化了檢測和找出內存問題的過程。 4.1 MEMWATCH MEMWATCH 由 Johan Lindh 編寫,是一個開放源代碼 C 語言內存錯誤檢測工具,您能夠本身下載它(請參閱本文後面部分的參考資料)。只要在代碼中添加一個頭文件並在 gcc 語句中定義了 MEMWATCH 以後,您就能夠跟蹤程序中的內存泄漏和錯誤了。MEMWATCH 支持 ANSI C,它提供結果日誌紀錄,能檢測雙重釋放(double-free)、錯誤釋放(erroneous free)、沒有釋放的內存(unfreed memory)、溢出和下溢等等。 快速上手: 在MEMWATCH的源代碼包中包含有三個程序文件:memwatch.c, memwatch.h, test.c; 能夠利用這三個程序,按照你的編譯環境對makefile修改後,就但是簡單使用memwatch來檢測test.c這個文件中的內存問題; 固然你能夠根據本身的須要修改test.c文件,不過須要注意的是: 1)若是你的程序是永遠不會主動退出的話,建議深刻看看memwatch,由於memwatch默認狀況下,在程序退出的時,將有關內存檢測的結果寫入到某個日誌文件中;因此針對你永遠不會退出的程序,你須要本身動手調用MEMWATCH的結果函數。 2)若是你要使用memwatch來檢測內存問題的話,必須將memwatch.h頭文件包含在你須要檢測文件中;而且把memwatch.c加入編譯。在編譯時記得加-DMEMWATCH -DMW_STDIO選項;在主文件中最好包含如下信息, //在linux下須要包含signal.h這個頭文件 #ifndef SIGSEGV #error "SIGNAL.H does not define SIGSEGV; running this program WILL cause a core dump/crash!" #endif //防止你在編譯時沒有加 -DMEMWATCH #ifndef MEMWATCH #error "You really, really don't want to run this without memwatch. Trust me." #endif //防止你在編譯時候沒有加 -DMEMWATCH_STDIO,在一些指針錯誤的時候,會彈出 //Abort/Retry/Ignore 讓你選擇處理方法。 #if !defined(MW_STDIO) && !defined(MEMWATCH_STDIO) #error "Define MW_STDIO and try again, please." #endif //多線程程序須要定義MW_PTHREADS,雖然它並不必定可以保證多線程安全,zpf #if !defined(MW_PTHREADS) && !defined(HAVE_PTHREAD_H) #error "if ur program is multithreads, please define MW_PTHREADS; " #error "otherwise, Comment out the following line." #endif 3)剛開始仍是找一個簡單的單線程程序來上手吧. 樣例講解 清單 1. 內存樣本(test1.c) #include <stdlib.h> #include <stdio.h> #include "memwatch.h" int main(void) { char *ptr1; char *ptr2; ptr1 = malloc(512); ptr2 = malloc(512); ptr2 = ptr1; free(ptr2); free(ptr1); } 清單 1 中的代碼將分配兩個 512 字節的內存塊,而後指向第一個內存塊的指針被設定爲指向第二個內存塊。結果,第二個內存塊的地址丟失,從而產生了內存泄漏。 如今咱們編譯清單 1 的 memwatch.c。下面是一個 makefile 示例: test1 gcc -DMEMWATCH -DMW_STDIO test1.c memwatch c -o test1 當您運行 test1 程序後,它會生成一個關於泄漏的內存的報告。清單 2 展現了示例 memwatch.log 輸出文件。 清單 2. test1 memwatch.log 文件 MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh ... double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14) ... unfreed: <2> test1.c(11), 512 bytes at 0x80519e4 {FE FE FE FE FE FE FE FE FE FE FE FE ..............} Memory usage statistics (global): N)umber of allocations made: 2 L)argest memory usage : 1024 T)otal of all alloc() calls: 1024 U)nfreed bytes totals : 512 MEMWATCH 爲您顯示真正致使問題的行。若是您釋放一個已經釋放過的指針,它會告訴您。對於沒有釋放的內存也同樣。日誌結尾部分顯示統計信息,包括泄漏了多少內存,使用了多少內存,以及總共分配了多少內存。 建議: 建議在你須要測試的主文件中加入如下函數信息 //在linux下須要包含signal.h這個頭文件 #ifndef SIGSEGV #error "SIGNAL.H does not define SIGSEGV; running this program WILL cause a core dump/crash!" #endif //防止你在編譯時沒有加 -DMEMWATCH #ifndef MEMWATCH #error "You really, really don't want to run this without memwatch. Trust me." #endif //防止你在編譯時候沒有加 -DMEMWATCH_STDIO,在一些指針錯誤的時候,會彈出 //Abort/Retry/Ignore 讓你選擇處理方法。 #if !defined(MW_STDIO) && !defined(MEMWATCH_STDIO) #error "Define MW_STDIO and try again, please." #endif //多線程程序須要定義MW_PTHREADS,雖然它並不必定可以保證多線程安全,zpf #if !defined(MW_PTHREADS) && !defined(HAVE_PTHREAD_H) #error "if ur program is multithreads, please define MW_PTHREADS; " #error "otherwise, Comment out the following line." #endif 另外須要說明的是,在源代碼附帶的<<USING>>文檔中,做者說不能保證代碼是絕對多線程安全的,同時若是你若是要使用它來檢測linux下多線程程序的內存狀況時,請定義MW_PTHREADS宏(能夠經過在makefile中加 –DMW_PTHREADS),這樣memwatch將引入互斥量操做。 MEMWATCH程序使用中經過簡單的加入memwatch.h頭文件,而且將memwatch.c加入編譯,就能夠在程序正常結束的時候,輸出日誌文件,在日誌文件中給出系統中的內存問題;可是有一個問題,在咱們的終端中應用程序是永遠不會退出的,而且可能系統根本就沒有充分考慮良好的收尾工做;那麼若是使用MEMWATCH來檢測咱們的程序是否存在內存問題哪? 仔細閱讀memwatch.h文件會發現,MEMWATCH提供了兩個比較有用的函數: void mwInit( void ); void mwTerm( void ); 關於這兩個函數,做者給出的註釋爲:Normally, it is not nessecary to call any of these. MEMWATCH will automatically initialize itself on the first MEMWATCH function call, and set up a call to mwAbort() using atexit()。因此一般咱們不須要顯式調用這兩個函數。 可是問題是,咱們的程序可能不會顯式的退出,從而atexit()函數不能正常被調用;在這種狀況下,咱們能夠經過顯式的調用來完成必定的內存檢測工做; mwInit( void ) 和mwTerm( void )能夠屢次調用。可是兩個和malloc和free同樣,必須配對使用。你就能夠經過這樣方式控制MEMWATCH何時開始檢測內存,何時結束檢測內存; MEMWATCH日誌輸出 MEMWATCH會輸出日誌到程序工做目錄的memwatch.log文件中,若是這個文件不能建立的話,程序會嘗試建立memwatNN.log,NN 從01 到 99。 若是你以爲沒有必要輸出日誌到文件中,那麼能夠本身定義輸出函數,方法爲輸出函數必須爲void func(int c)類型,而後將輸出函數地址做爲參數顯式調用mwSetOutFunc()指定輸出;這樣程序就會重定向輸出到你定義的輸出函數中; 野指針 當使用沒有初始化的指針,或者指針指向的空間已被移動或者釋放時,就會形成野指針問題。 避免這個問題的最好方法是:定義指針時老是初始化爲NULL;當釋放指針後,顯式的將指針設置爲NULL; 爲了便於追蹤這種指針錯誤,MEMWATCH程序將全部的內存區域賦予特定的數字;例如:剛分配可是未初始化的內存區域所有設置爲0xFE;最近釋放後沒有被從新利用的內存所有設置爲0xFD;因此若是你的程序在沒有使用MEMWATCH時沒有問題,可是加載MEMWATCH後會崩潰的話,可能程序中就存在指針分配後,沒有初始化就使用;或者使用了已經釋放的內存。 更深刻的使用 更深刻的使用請參見,<<USING>>, <<README>>, <<memwatch.h>>. 我目前作的實驗:在一個進程中(是否多個進程可以同時使用,還由待驗證),初始化階段,顯式調用mwInit(), 而後在處理SIGINT,SIGTERM(由於程序運行過程當中,咱們會利用killall殺死咱們不會中止的程序)的函數中顯式調用mwTerm()函數; 另外我還修改了static void mwWrite( const char *format, ... )這個函數,在將有關信息輸出到日誌的過程當中,同時利用printf輸出到屏幕上(固然你也能夠經過調用mwSetOutFunc()指定本身的輸出函數)。若是是多線程的話,請定義MW_PTHREADS宏(能夠經過在makefile中加 –DMW_PTHREADS)。 單線程: RT-VD4201M和RT-VS4201S的主線程都不存在問題; 多線程: 在RT-VD4201M的多線程程序中沒有問題, 能夠檢測出那些沒有被釋放的內存; 在RT-VS4201S的主線程單獨使用時,沒有問題,能夠檢測出內存泄漏;而在RT-VS4201S多個線程中,常會出如今建立線程時程序崩潰現象,有時候程序可能會運行下去;具體緣由不明,多是做者提到的「並不能徹底確保函數多線程安全」,或者「程序存在前面提到的野指針問題」,或者其它問題。 使用MEMWATCH初步對RT-VD4201M和RT-VS4201S主程序進行內存檢測;除了一些程序初始化階段分配的動態內存外(這些內存泄漏是程序已知的,例如RTSP,MP模塊初始化分配的內存),近一個小時的運行,並無發現其餘內存泄漏問題。 多進程: 多個進程也能夠支持。 4.2 YAMD (說明:資料從網上來,該軟件沒有試用過) YAMD 軟件包由 Nate Eldredge 編寫,能夠查找 C 和 C++ 中動態的、與內存分配有關的問題。在撰寫本文時,YAMD 的最新版本爲 0.32。請下載 yamd-0.32.tar.gz(請參閱參考資料)。執行 make 命令來構建程序;而後執行 make install 命令安裝程序並設置工具。 一旦您下載了 YAMD 以後,請在 test1.c 上使用它。請刪除 #include memwatch.h 並對 makefile 進行以下小小的修改: 使用 YAMD 的 test1 gcc -g test1.c -o test1 清單 3 展現了來自 test1 上的 YAMD 的輸出。 清單 3. 使用 YAMD 的 test1 輸出 YAMD version 0.32 Executable: /usr/src/test/yamd-0.32/test1 ... INFO: Normal allocation of this block Address 0x40025e00, size 512 ... INFO: Normal allocation of this block Address 0x40028e00, size 512 ... INFO: Normal deallocation of this block Address 0x40025e00, size 512 ... ERROR: Multiple freeing At free of pointer already freed Address 0x40025e00, size 512 ... WARNING: Memory leak Address 0x40028e00, size 512 WARNING: Total memory leaks: 1 unfreed allocations totaling 512 bytes *** Finished at Tue ... 10:07:15 2002 Allocated a grand total of 1024 bytes 2 allocations Average of 512 bytes per allocation Max bytes allocated at one time: 1024 24 K alloced internally / 12 K mapped now / 8 K max Virtual program size is 1416 K End. YAMD 顯示咱們已經釋放了內存,並且存在內存泄漏。讓咱們在清單 4 中另外一個樣本程序上試試 YAMD。 清單 4. 內存代碼(test2.c) #include <stdlib.h> #include <stdio.h> int main(void) { char *ptr1; char *ptr2; char *chptr; int i = 1; ptr1 = malloc(512); ptr2 = malloc(512); chptr = (char *)malloc(512); for (i; i <= 512; i++) { chptr[i] = 'S'; } ptr2 = ptr1; free(ptr2); free(ptr1); free(chptr); } 您可使用下面的命令來啓動 YAMD: ./run-yamd /usr/src/test/test2/test2 清單 5 顯示了在樣本程序 test2 上使用 YAMD 獲得的輸出。YAMD 告訴咱們在 for 循環中有「越界(out-of-bounds)」的狀況。 清單 5. 使用 YAMD 的 test2 輸出 Running /usr/src/test/test2/test2 Temp output to /tmp/yamd-out.1243 ********* ./run-yamd: line 101: 1248 Segmentation fault (core dumped) YAMD version 0.32 Starting run: /usr/src/test/test2/test2 Executable: /usr/src/test/test2/test2 Virtual program size is 1380 K ... INFO: Normal allocation of this block Address 0x40025e00, size 512 ... INFO: Normal allocation of this block Address 0x40028e00, size 512 ... INFO: Normal allocation of this block Address 0x4002be00, size 512 ERROR: Crash ... Tried to write address 0x4002c000 Seems to be part of this block: Address 0x4002be00, size 512 ... Address in question is at offset 512 (out of bounds) Will dump core after checking heap. Done. MEMWATCH 和 YAMD 都是頗有用的調試工具,它們的使用方法有所不一樣。對於 MEMWATCH,您須要添加包含文件 memwatch.h 並打開兩個編譯時間標記。對於連接(link)語句,YAMD 只須要 -g 選項。 4.3 Electric Fence (說明:資料從網上來,該軟件沒有試用過) 多數 Linux 分發版包含一個 Electric Fence 包,不過您也能夠選擇下載它。Electric Fence 是一個由 Bruce Perens 編寫的 malloc() 調試庫。它就在您分配內存後分配受保護的內存。若是存在 fencepost 錯誤(超過數組末尾運行),程序就會產生保護錯誤,並當即結束。經過結合 Electric Fence 和 gdb,您能夠精確地跟蹤到哪一行試圖訪問受保護內存。Electric Fence 的另外一個功能就是可以檢測內存泄漏。 五 C/C++代碼覆蓋、性能profiling工具 C/C++代碼覆蓋、性能profiling工具通常基於GNU的gprof和gcov。還有一類基於模擬器的profiling工具,如IBM Purify, Valgrind。KCahcegrind是Callgrind,OProfile等的GUI前端。性能測試工具備ggcof,kprof,lcov等等。lcov是Linux Testing Project工具之一,見http://ltp.sourceforge.net/tooltable.php上的工具列表。這兒還有壓力測試、WEB Server測試等許多工具。在http://www.testingfaqs.org分類概括了多種軟件測試工具。 5.1 用gcov來測試代碼覆蓋率 gcov是gnu/gcc工具庫中的一個組件, 用來測試代碼的覆蓋率;當構建一個程序時,gcov會監視一個程序的執行,而且會標識出執行了哪一行源碼,哪一行沒有執行。更進一步,gcov能夠標識出某一行源執行的次數,這樣就能夠知道程序在哪裏花費了大多數的時間。 爲何要測試代碼覆蓋率? 我是不喜歡在代碼中有跑不到的地方,那只是在白白浪費空間,下降效率 固然了,有些時候,咱們能夠經過跑代碼覆蓋率來發現咱們有什麼異常狀況沒有進行測試,畢竟單元測試的用例,不可能一下就想的很全面的。 例如,你的程序在某個函數的入口前處檢測了指針不爲空,你進入調用函數之後又檢測了一回這個指針,而且對爲NULL的狀況進行處理,那麼兩處之中必有一處是在浪費空間,固然你的硬盤大,放的下,可是代碼寫的精緻一些,不是更好麼? 得到gcov gcov是gnu/gcc工具庫的組件,因此在創建交叉編譯工具的時候須要指定建立這個工具。 在arm-linux-的交叉編譯工具中,arm-linux-gcov好像默認是存在的;操做系統組給個人交叉編譯環境中是有這一工具的;可是uClinux的交叉編譯環境中默認好像是沒有這個工具的;具體的搭建從下面gprof的討論也許可以獲得一些信息。由於如今我尚未移植操做系統的經驗,因此沒法對這個進行證明。 關於這個論壇上面的討論是: > The gprof program, for historical reasons, is sometimes excluded > from a cross-targeted toolchain. If you have a source tree with > a Cygnus configure script at the top level, or a gcc source tree, > then look for the "native_only" variable from the top-level > configure.in, remove "gprof". Then reconfigure the build tree, > and run "make" in the gprof subdirectory. That did it! Just to be clear, this is what I did: I untar'd the binutils-2.12.1 tar ball and edited binutils-2.12.1/configure.in. There's a line containing "native_only" that several packages (like gprof, sed,...); I removed gprof from that list. I then followed the instructions here for building binutils: http://sources.redhat.com/ecos/tools/linux-arm-elf.html and arm-elf-gprof was created! Score! 使用gcov 使用gcov很簡單, 首先在編譯的時候加上-fprofile-arcs -ftest-coverage,同時連接的時候也加上這些選項;須要特別說明的時,gcov要求被測試程序在執行的時可以訪問到它編譯的那個目錄,由於要使用到編譯過程當中生成的一個文件(我測試的時是這樣的,執行時提示找不到/home/zpf/gdb_test/bubblesort.gcda這個文件,而我得bubblesort程序是在/home/zpf/gdb_tes/這個目錄裏編譯的);因此若是是嵌入式的話,就須要nfs這樣的工具支持。 示例程序源代碼 例如咱們須要測試如下bubblesort的代碼: 1: #include <stdio.h> 2: 3: void bubbleSort( int list[], int size ) 4: { 5: int i, j, temp, swap = 1; 6: 7: while (swap) { 8: 9: swap = 0; 10: 11: for ( i = (size-1) ; i >= 0 ; i— ) { 12: 13: for ( j = 1 ; j <= i ; j++ ) { 14: 15: if ( list[j-1] > list[j] ) { 16: 17: temp = list[j-1]; 18: list[j-1] = list[j]; 19: list[j] = temp; 20: swap = 1; 21: 22: } 23: 24: } 25: 26: } 27: 28: } 29: 30: } 31: 32: int main() 33: { 34: int theList[10]={10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; 35: int i; 36: 37: /* Invoke the bubble sort algorithm */ 38: bubbleSort( theList, 10 ); 39: 40: /* Print out the final list */ 41: for (i = 0 ; i < 10 ; i++) { 42: printf("%d\n", theList[i]); 43: } 44: 45: } 編譯程序 若是要使用gcov進行覆蓋測試,在編譯程序時,必須加-fprofile-arcs -ftest-coverage編譯選項;下面是咱們用來演示編譯bubbleSort.c的命令: gcc bubblesort.c -o bubblesort -ftest-coverage -fprofile-arcs 當咱們執行生成的bubblesort程序時會生成一些包含關於程序的相關數據的文件。gcov程序將會使用這些文件來報告數據而且向開發者提供相應的信息。當指定「-ftest-coverage」(注意這是一個選項而不是兩個選項)選項時會爲每個源碼生成兩個文件,他們以「.bb」與「.bbg」爲擴展名,而且用這些文件來重組每個可執行程序的程序流圖。當指定「-fprofile-arcs」 (注意這是一個選項而不是兩個選項),將會生成一個包含每個指令分支的執行計數的以「.da」爲擴展名的文件。這些文件會在執行之後與源碼文件一塊兒使用,來標識源碼的執行行爲。 運行程序 運行剛纔編譯生成的bubblesort程序就會生成咱們在前面所討論的那些附帶文件。而後咱們使用咱們但願進行檢測的源碼運行gcov程序。以下面所示: $ ./bubblesort ... $ gcov bubblesort.c 100.00% of 17 source lines executed in file bubblesort.c Creating bubblesort.c.gcov. 以上信息告訴咱們,在這個例子程序中全部的源碼行至少都執行了一次。另外還能夠經過查看生成的bubblesort.c.gcov文件來了解每一源碼行所實際運行的次數。以下面所示: -: 0:Source:bubblesort.c -: 0:Graph:bubblesort.gcno -: 0:Data:bubblesort.gcda -: 0:Runs:1 -: 0:Programs:1 -: 1:#include <stdio.h> -: 2:void bubbleSort(int list[],int size) 1: 3:{ 1: 4: int i,j,temp,swap=1; 4: 5: while(swap) -: 6: { 2: 7: swap=0; 22: 8: for(i=(size-1);i>=0;i--) -: 9: { 110: 10: for(j=1;j<=i;j++) -: 11: { 90: 12: if(list[j-1]>list[j]) -: 13: { 45: 14: temp=list[j-1]; 45: 15: list[j-1]=list[j]; 45: 16: list[j]=temp; 45: 17: swap=1; -: 18: } -: 19: } -: 20: } -: 21: } 1: 22:} -: 23:int main() 1: 24:{ 1: 25: int theList[10]={10,9,8,7,6,5,4,3,2,1}; -: 26: int i; -: 27: /*Invoke the buble sort algorithm*/ 1: 28: bubbleSort(theList,10); -: 29: -: 30: /*print out the final list*/ 11: 31: for(i=0;i<10;i++) -: 32: { 10: 33: printf("%d\n",theList[i]); -: 34: } 1: 35: return 0; -: 36:} 第一列顯示了源碼中每一行源碼所執行的次數。在一些狀況下,執行次數並無提供,由於他們是並不會影響代碼的簡單C源碼元素。 這些計數能夠提供一些關於程序執行的信息。例如,第12行執行了90次,而14-17行的代碼只是執行了45次。這告訴咱們當這個函數調用了90次,真正成功的僅是45次。換句話說,大部分的測試時間浪費在兩個元素的交換上。這是因爲測試數據的順序所形成的。 從這裏咱們能夠看到代碼段中最常執行的部分就是排序算法的內循環部分。這是由於因爲退出測試第10行要比第12行執行的次數多一些。 遺憾的是:我使用arm交叉編譯環境按照上面的步驟編譯程序,下載到目標板上運行時程序能夠正常執行;可是沒有日誌文件產生;提示爲「profiling:/home/zpf/gdb_test/bubblesort.gcda:Cannot open」;「/home/zpf/gdb_test/」是我在linux服務器上編譯bubblesort的目錄;從這裏看,要想利用gcov進行覆蓋測試的話,必須在你編譯的那個目錄執行,看來nfs是嵌入式調試的根本??? 查看分支效率 使用-b選項能夠查看程序的分支效率。這個選項會輸出程序中每個分支的頻度與相應的摘要。例如,咱們使用-b選項來執行gcov命令: $ gcov -b bubblesort.c 100.00% of 17 source lines executed in file bubblesort.c 100.00% of 12 branches executed in file bubblesort.c 100.00% of 12 branches taken at least once in file bubblesort.c 100.00% of 2 calls executed in file bubblesort.c Creating bubblesort.c.gcov. 所生成的bubblesort.c.gcov文件以下所示。 -: 0:Source:bubblesort.c -: 0:Graph:bubblesort.gcno -: 0:Data:bubblesort.gcda -: 0:Runs:1 -: 0:Programs:1 -: 1:#include <stdio.h> -: 2:void bubbleSort(int list[],int size) function bubbleSort called 1 returned 100% blocks executed 100% 1: 3:{ 1: 4: int i,j,temp,swap=1; 4: 5: while(swap) branch 0 taken 67% branch 1 taken 33% (fallthrough) -: 6: { 2: 7: swap=0; 22: 8: for(i=(size-1);i>=0;i--) branch 0 taken 91% branch 1 taken 9% (fallthrough) -: 9: { 110: 10: for(j=1;j<=i;j++) branch 0 taken 82% branch 1 taken 18% (fallthrough) -: 11: { 90: 12: if(list[j-1]>list[j]) branch 0 taken 50% (fallthrough) branch 1 taken 50% -: 13: { 45: 14: temp=list[j-1]; 45: 15: list[j-1]=list[j]; 45: 16: list[j]=temp; 45: 17: swap=1; -: 18: } -: 19: } -: 20: } -: 21: } 1: 22:} -: 23:int main() function main called 1 returned 100% blocks executed 100% 1: 24:{ 1: 25: int theList[10]={10,9,8,7,6,5,4,3,2,1}; -: 26: int i; -: 27: /*Invoke the buble sort algorithm*/ 1: 28: bubbleSort(theList,10); call 0 returned 100% -: 29: -: 30: /*print out the final list*/ 11: 31: for(i=0;i<10;i++) branch 0 taken = 91% branch 1 taken = 100% branch 2 taken = 100% -: 32: { 10: 33: printf("%d\n",theList[i]); call 0 returned 100% -: 34: } 1: 35: return 0; -: 36:} 與前一個的文件相似,可是這一次每個分支點都用他們的頻度進行了標示。 分支點依賴於目標結構指令集(看來要好好理解分支的含義,還須要對彙編有必定的瞭解)。第12行是一個簡單的if語句,因此有一個分支點。在這裏咱們能夠注意到這是50%,這經過咱們前面觀察程序的執行次數能夠看出。其餘的分支點有一些難於分析。例如, 第7行是一個while語句,有兩個分支點。在X86彙編中,這一行編譯成咱們下面所看到的樣子: 1: cmpl $0, -20(%ebp) 2: jne .L4 3: jmp .L1 從這裏咱們可看出,swap變量與0進行比較。若是他不等於0,就會跳轉到第2行,.L4。不然要跳轉到第3行,.L1。第2行所示的分支機率爲67%,這是由於這一行執行3次,可是jne只執行了兩次。當第2行的jne並無執行時,咱們直接跳轉到第3行。這隻執行一次,可是一旦執行,程序就結束了。因此分支1要花費100%的時間。 第33行的printf()調用;其下面有一行call 0 returned 100%; 關於這個在手冊上是如此解釋的:若是一個函數調用至少被執行一次的話,gcov給出的百分比表示「函數返回次數 除以 函數調用次數」, 一般這個數值是100%,可是對於象「exit」或者「longjmp」這類並非每次調用都返回的函數來講,這個百分比就小於100%。(For a call, if it was executed at least once, then a percentage indicating the number of times the call returned divided by the number of times the call was executed will be printed. This will usually be 100%, but may be less for functions call "exit" or "longjmp", and thus may not return every time they are called.) 疑惑的地方:這裏的分支是如何劃分的,一個for怎麼有三個分支,而且每一個分支都執行到了??是否是「i=0;i<10;i++」認爲是3個分支? 關於分支百分比,手冊上是這樣解釋的:若是某個分支從沒被執行過,則給出「never executed」;不然gcov給出的百分比說明這個分支「taken的次數」除以「executed的次數」(鬱悶這裏take如何理解那???)(For a branch, if it was executed at least once, then a percentage indicating the number of times the branch was taken divided by the number of times the branch was executed will be printed. Otherwise, the message ``never executed'' is printed.)。 因此分支機率在理解程序流時是至關有用的,可是要參考彙編代碼,須要理解分支點在哪裏。 看起來有點暈 不完整程序測試 當gcov計數一個測試並非100%的程序時,並無執行的行是標記爲####,而不是執行次數。下面顯示的是一個由gcov建立的文件來顯示少於100%的測試。 1: #include <stdio.h> 2: 3: int main() 4: 1 { 5: 1 int a=1, b=2; 6: 7: 1 if (a == 1) { 8: 1 printf("a = 1\n"); 9: } else { 10: ###### printf("a != 1\n"); 11: } 12: 13: 1 if (b == 1) { 14: ###### printf("b = 1\n"); 15: } else { 16: 1 printf("b != 1\n"); 17: } 18: 19: 1 return 0; 20: } 當這個程序運行時,gcov也會向標準輸出輸出相應的信息。他會顯示可能執行的源碼行的行數以及實際運行的百分比。 $ gcov incomptest.c 77.78% of 9 source lines executed in file incomptest.c Creating incomptest.c.gcov. $ 若是咱們的例子程序有多個函數,咱們能夠經過使用-f選項來查看每個函數的執行狀況。以下面的咱們以bubbleSort程序所進行的演示: $ gcov -f bubblesort.c 100.00% of 11 source lines executed in function bubbleSort 100.00% of 6 source lines executed in function main 100.00% of 17 source lines executed in file bubblesort.c Creating bubblesort.c.gcov. $ gcov可用的選項 gcov程序調用的格式爲: gcov [options] sourcefile 其可用的選項以下: 選項 目的 -v,-version 打印版本信息 -h,-help 打印幫助信息 -b,-branch-probabilities 向輸出文件輸出分支頻度 -c,-branch-counts 打印分支計數而不是分支頻度 -n,-no-output 不建立gcov輸出文件 -l,-long-file-names 建立長文件名 -f,-function-summaries 打印每個函數的概要 -o,-object-directory .bb,.bbg,.da文件存放的目錄 從上面這個表中,咱們能夠看到一個單個字符選項,以及一個長選項。當從命令行中使用gcov命令時短選項是比較有用的,可是當gcov是Makefile的一個部分時,應使用長選項,由於這更易於理解。 當了解gcov程序的版本信息時,可使用-v選項。由於gcov是與一個指定的編譯器工具鏈聯繫在一塊兒的(其實是由gcc工具鏈而構建的),gcc版本與gcov的版本是相同的。gcov程序的簡介以及選項幫助能夠用-h選項來進行顯示。 在你的目錄下多了幾個文件,後綴是.gcda,gcno,呵呵,恭喜你,你的gcov的版本仍是比較新的,在之前舊一點的版本中,有.bb,.bbg這種後綴(我pc上安裝的是2.4.18-14的內核,生成了以這個爲擴展名的文件),不過如今都沒了,其實gcov跑的數據統計什麼的都保存在這些文件中,這就是爲何,你能夠屢次跑程序,而gcov會本身統計的神奇本領所在。 另外在man gcov中還提到,gprof可使用gcov生成的日誌文件進一步分析性能,原文以下:gcov creates a logfile called sourcefile.gcov which indicates how many times each line of a source file sourcefile.c has executed. You can use these logfiles along with gprof to aid in fine-tuning the performance of your programs. gprof gives timing information you can use along with the information you get from gcov. gcov [-b] [-c] [-v] [-n] [-l] [-f] [-o directory] sourcefile -b Write branch frequencies to the output file, and write branch summary info to the standard output. This option allows you to see how often each branch in your program was taken. //b(ranch),分支測試 -c Write branch frequencies as the number of branches taken, rather than the percentage of branches taken. -v Display the gcov version number (on the standard error stream). -n Do not create the gcov output file. -l Create long file names for included source files. For example, if the header file `x.h' contains code, and was included in the file `a.c', then running gcov on the file `a.c' will produce an output file called `a.c.x.h.gcov' instead of `x.h.gcov'. This can be useful if `x.h' is included in multiple source files. -f Output summaries for each function in addition to the file level summary. -o The directory where the object files live. Gcov will search for `.bb', `.bbg', and `.da' files in this directory. //新版的是這麼說的 -o directory│file --object-directory directory --object-file file Specify either the directory containing the gcov data files, or the object path name. The .gcno, and .gcda data files are searched for using this option. If a directory is specified, the data files are in that directory and named after the source file name, without its extension. If a file is specified here, the data files are named after that file, without its extension. If this option is not sup- plied, it defaults to the current directory. 其餘的還有新版的-u, -u --unconditional-branches When branch counts are given, include those of unconditional branches. Unconditional branches are normally not interesting. -p --preserve-paths Preserve complete path information in the names of generated .gcov files. Without this option, just the filename component is used. With this option, all directories are used, with ’/’ characters translated to ’#’ characters, ’.’ directory components removed and ’..’ components renamed to ’^’. This is useful if sourcefiles are in several different directories. It also affects the -l option. man一下就能看到,我也很少說了 Using gcov with GCC Optimization If you plan to use gcov to help optimize your code, you must first com- pile your program with two special GCC options: -fprofile-arcs -ftest-coverage. Aside from that, you can use any other GCC options; but if you want to prove that every single line in your program was executed, you should not compile with optimization at the same time. On some machines the optimizer can eliminate some simple code lines by combining them with other lines. For example, code like this: if (a != b) c = 1; else c = 0; can be compiled into one instruction on some machines. In this case, there is no way for gcov to calculate separate execution counts for each line because there isn't separate code for each line. Hence the gcov output looks like this if you compiled the program with optimiza- tion: 100 if (a != b) 100 c = 1; 100 else 100 c = 0; The output shows that this block of code, combined by optimization, executed 100 times. In one sense this result is correct, because there was only one instruction representing all four of these lines. How- ever, the output does not indicate how many times the result was 0 and how many times the result was 1. 5.2 使用gprof來優化你的C/C++程序 做者:arnouten(Q)bzzt.net linuxfocus (2005-05-18 15:03:24) 中文編譯: 小 汪 謹記:在值得優化的地方優化!沒有必要花幾個小時來優化一段實際上只運行0.04秒的程序。 gprof是GNU profiler工具。能夠顯示程序運行的「flat profile」,包括每一個函數的調用次數,每一個函數消耗的處理器時間。也能夠顯示「調用圖」,包括函數的調用關係,每一個函數調用花費了多少時間。還能夠顯示「註釋的源代碼」,是程序源代碼的一個複本,標記有程序中每行代碼的執行次數。 gprof 使用了一種異常簡單可是很是有效的方法來優化C/C++ 程序,並且能很容易的識別出值得優化的代碼。一個簡單的案例分析將會顯示,GProf如何經過識別並優化兩個關鍵的數據結構,將實際應用中的程序從3分鐘的運行時優化到5秒的。 得到gprof 在gprof這個工具以前,當前首先要得到這個工具,gporf是gnu/gcc工具集中的一個工具,可是按照論壇上的說法,一般在生成工具集時,這個工具默認是不產生的,須要修改配置文件才能得到。 關於這個論壇上面的討論是: > The gprof program, for historical reasons, is sometimes excluded > from a cross-targeted toolchain. If you have a source tree with > a Cygnus configure script at the top level, or a gcc source tree, > then look for the "native_only" variable from the top-level > configure.in, remove "gprof". Then reconfigure the build tree, > and run "make" in the gprof subdirectory. That did it! Just to be clear, this is what I did: I untar'd the binutils-2.12.1 tar ball and edited binutils-2.12.1/configure.in. There's a line containing "native_only" that several packages (like gprof, sed,...); I removed gprof from that list. I then followed the instructions here for building binutils: http://sources.redhat.com/ecos/tools/linux-arm-elf.html and arm-elf-gprof was created! Score! 使用gprof 程序概要分析的概念很是簡單:經過記錄各個函數的調用和結束時間,咱們能夠計算出程序的最大運行時的程序段。這種方法聽起來彷佛要花費不少氣力——幸運的是,咱們其實離真理並不遠!咱們只須要在用 gcc 編譯時加上一個額外的參數('-pg'),運行這個(編譯好的)程序(來蒐集程序概要分析的有關數據),而後運行「gprof」以更方便的分析這些結果。 案例分析: Pathalizer 我使用了一個現實中使用的程序來做爲例子,是 pathalizer的一部分: 即event2dot,一個將路徑「事件」描述文件轉化爲圖形化「dot」文件的工具(executable which translates a pathalizer 'events' file to a graphviz 'dot' file)。 簡單的說,它從一個文件裏面讀取各類事件,而後將它們分別保存爲圖像(以頁爲節點,且將頁與頁之間的轉變做爲邊),而後將這些圖像整合爲一張大的圖形,並保存爲圖形化的'dot'格式文件。 給程序計時 先讓咱們給咱們未經優化的程序計一下時,看看它們的運行要多少時間。在個人計算機上使用event2dot並用源碼裏的例子做爲輸入(大概55000的數據),大體要三分多鐘: real 3m36.316s user 0m55.590s sys 0m1.070s 程序分析 要使用gprof 做概要分析,在編譯的時候要加上'-pg' 選項,咱們就是以下從新編譯源碼以下: g++ -pg dotgen.cpp readfile.cpp main.cpp graph.cpp config.cpp -o event2dot 編譯時編譯器會自動在目標代碼中插入用於性能測試的代碼片段,這些代碼在程序在運行時採集並記錄函數的調用關係和調用次數,以及採集並記錄函數自身執行時間和子函數的調用時間,程序運行結束後,會在程序退出的路徑下生成一個gmon.out文件。這個文件就是記錄並保存下來的監控數據。能夠經過命令行方式的gprof或圖形化的Kprof來解讀這些數據並對程序的性能進行分析。另外,若是想查看庫函數的profiling,須要在編譯是再加入「-lc_p」編譯參數代替「-lc」編譯參數,這樣程序會連接libc_p.a庫,才能夠產生庫函數的profiling信息。若是想執行一行一行的profiling,還須要加入「-g」編譯參數。 如今咱們能夠再次運行event2dot,並使用咱們前面使用的測試數據。此次咱們運行的時候,event2dot運行的分析數據會被蒐集並保存在'gmon.out'文件中,咱們能夠經過運行'gprof event2dot | less'來查看結果。 gprof 會顯示出以下的函數比較重要: % cumulative self self total time seconds seconds calls s/call s/call name 43.32 46.03 46.03 339952989 0.00 0.00 CompareNodes(Node *,Node *) 25.06 72.66 26.63 55000 0.00 0.00 getNode(char *,NodeListNode *&) 16.80 90.51 17.85 339433374 0.00 0.00 CompareEdges(Edge *,AnnotatedEdge *) 12.70 104.01 13.50 51987 0.00 0.00 addAnnotatedEdge(AnnotatedGraph *,Edge *) 1.98 106.11 2.10 51987 0.00 0.00 addEdge(Graph *,Node *,Node *) 0.07 106.18 0.07 1 0.07 0.07 FindTreshold(AnnotatedEdge *,int) 0.06 106.24 0.06 1 0.06 28.79 getGraphFromFile(char *,NodeListNode *&,Config *) 0.02 106.26 0.02 1 0.02 77.40 summarize(GraphListNode *,Config *) 0.00 106.26 0.00 55000 0.00 0.00 FixName(char *) 能夠看出,第一個函數比較重要: 程序裏面絕大部分的運行時都被它給佔據了。 優化 上面結果能夠看出,這個程序大部分的時間都花在了CompareNodes函數上,用 grep 查看一下則發現CompareNodes 只是被CompareEdges調用了一次而已, 而CompareEdges則只被addAnnotatedEdge調用——它們都出如今了上面的清單中。這兒就是咱們應該作點優化的地方了吧! 咱們注意到addAnnotatedEdge遍歷了一個鏈表。雖然鏈表是易於實現,可是卻實在不是最好的數據類型。咱們決定將鏈表 g->edges 用二叉樹來代替: 這將會使得查找更快。 結果 如今咱們看一下優化後的運行結果: real 2m19.314s user 0m36.370s sys 0m0.940s 第二遍 再次運行 gprof 來分析: % cumulative self self total time seconds seconds calls s/call s/call name 87.01 25.25 25.25 55000 0.00 0.00 getNode(char *,NodeListNode *&) 10.65 28.34 3.09 51987 0.00 0.00 addEdge(Graph *,Node *,Node *) 看起來之前佔用大量運行時的函數如今已經再也不是佔用運行時的大頭了!咱們試一下再優化一下呢:用節點哈希表來取代節點樹。 此次簡直是個巨大的進步: real 0m3.269s user 0m0.830s sys 0m0.090s gprof的輸出信息 gprof的命令格式以下所示: gprof OPTIONS EXECUTABLE-FILE gmon.out BB-DATA [YET-MORE-PROFILE-DATA-FILES...] [> OUTFILE] gprof產生的信息含義以下所示: % time the percentage of the total running time of the program used by this function. 函數使用時間佔整個程序運行時間的百分比。 cumulative seconds a running sum of the number of seconds accounted for by this function and those listed above it. 列表中包括該函數在內以上全部函數累計運行秒數。 self seconds the number of seconds accounted for by this function alone. This is the major sort for this listing. 函數自己所執行的秒數。 calls the number of times this function was invoked, if this function is profiled, else blank. 函數被調用的次數 Self ms/call the average number of milliseconds spent in this function per call, if this function is profiled, else blank. 每一次調用花費在函數的時間microseconds。 Total ms/call the average number of milliseconds spent in this function and its descendents per call, if this function is profiled, else blank. 每一次調用,花費在函數及其衍生函數的平均時間microseconds。 name the name of the function. This is the minor sort for this listing. The index shows the location of the function in the gprof listing. If the index is in parenthesis it shows where it would appear in the gprof listing if it were to be printed. 函數名 其餘 C/C++ 程序分析器 還有其餘不少分析器可使用gprof 的數據, 例如KProf (截屏) 和 cgprof。雖然圖形界面的看起來更舒服,但我我的認爲命令行的gprof 使用更方便。 對其餘語言的程序進行分析 咱們這裏介紹了用gprof 來對C/C++ 的程序進行分析,對其餘語言其實同樣能夠作到: 對 Perl,咱們能夠用Devel::DProf 模塊。你的程序應該以perl -d:DProf mycode.pl來開始,並使用dprofpp來查看並分析結果。若是你能夠用gcj 來編譯你的Java 程序,你也可使用gprof,然而目前還只支持單線程的Java 代碼。 結論 就像咱們已經看到的,咱們可使用程序概要分析快速的找到一個程序裏面值得優化的地方。在值得優化的地方優化,咱們能夠將一個程序的運行時從 3分36秒 減小到少於 5秒,就像從上面的例子看到的同樣。 References Pathalizer: http://pathalizer.sf.net KProf: http://kprof.sf.net cgprof: http://mvertes.free.fr Devel::DProf http://www.perldoc.com/perl5.8.0/lib/Devel/DProf.html gcj: http://gcc.gnu.org/java : pathalizer example files: download for article371 (http://www.fanqiang.com) 原文連接:http://main.linuxfocus.org/ChineseGB/March2005/article371.shtml 參考文獻: [1] 《掌握 Linux 調試技術》 Steve Best(sbest@us.ibm.com)JFS 核心小組成員,IBM 2002 年 8 月; 本文來源於IBM網站。 [2] nfs.sourceforge.net 網站上的HOWTO,和 FAQ文檔; [3] 《building embedded linux systems》 [4] 《嵌入式linux系統開發詳解--基於EP93XX系列ARM》 [5] 博客文章http://blog.sina.com.cn/u/1244756857