基本塊圖(Basic Block Graph),基本塊的最后一條語句一般都要跳轉,否則后面一條語句也會被計算為基本塊的一部分。 如果跳轉語句是有條件的,就產生了一個分支(arc),該基本塊就有兩個基本塊作為目的地。如果把每個基本塊當作一個節點,那么一個函數中的所有基本塊就構成了一個有向圖,稱之為基本塊圖(Basic Block Graph)。且只要知道圖中部分BB或arc的執行次數就可以推算出所有的BB和所有的arc的執行次數;
打樁,意思是在有效的基本塊之間增加計數器,計算該基本塊被運行的次數;打樁的位置都是在基本塊圖的有效邊上;
行覆蓋率(line coverage),源代碼有效行數與被執行的代碼行的比率;
分支覆蓋率(branch coverage),有判定語句的地方都會出現2個分支,整個程序經過的分支與所有分支的比率是分支覆蓋率。注意,與條件覆蓋率(condition coverage)有細微差別,條件覆蓋率在判定語句的組合上有更細的劃分。
2. gcc/g++ 編譯選項
gcc需要靜態注入目標程序編譯選項,在編譯鏈接的時候加入2個選項(-ftest-coverage -fprofile-arcs ),編譯結束之后會生成 *.gcno 文件,而經過靜態注入的目標程序在“正常結束”后,會在運行目錄下產生*.gcda數據文件,通過gcov工具就可產生覆蓋率數據結果。
-ftest-coverage
Produce a notes file that the gcov code-coverage utility (see gcov—a Test Coverage Program) can use to show program coverage. Each source file’s note file is called auxname.gcno. Refer to the -fprofile-arcs option above for a description of auxname and instructions on how to generate test coverage data. Coverage data matches the source files more closely if you do not optimize.
讓編譯器生成與源代碼同名的.gcno文件(note file),這種文件含有重建基本塊依賴圖和將源代碼關聯至基本塊的必要信息;
-fprofile-arcs
Add code so that program flow arcs are instrumented. During execution the program records how many times each branch and call is executed and how many times it is taken or returns. When the compiled program exits it saves this data to a file called auxname.gcda for each source file. The data may be used for profile-directed optimizations (-fbranch-probabilities), or for test coverage analysis (-ftest-coverage). Each object file’s auxname is generated from the name of the output file, if explicitly specified and it is not the final executable, otherwise it is the basename of the source file. In both cases any suffix is removed (e.g. foo.gcda for input file dir/foo.c, ordir/foo.gcda for output file specified as -o dir/foo.o). See Cross-profiling.
讓編譯器靜態注入對每個源代碼行關聯的計數器進行操作的代碼,并在鏈接階段鏈入經態度libgcov.a,其中包含在程序正常結束時生成*.gcda文件的邏輯;
下面通過源碼解析來說明到底這2個選項做了什么。通過g++ -S選項,產生匯編語言Rectangle.s 和 Rectangle_cc.s (增加–coverage選項),命令如下,
g++ -c -o Rectangle.s Rectangle.cpp -g -Wall -S
g++ -c -o Rectangle_cc.s Rectangle.cpp -g -Wall –coverage -S
vimdiff Rectangle.s 和 Rectangle_cc.s,如下圖
通過這樣匯編語言的對比,可以看出gcc通過這2個參數,把打樁的過程完成了。
更深入的內容,例如,如果想知道gcno/gcda文件的格式,可以參考 @livelylittlefish 的一篇文章,GCC Coverage代碼分析-.gcda/.gcno文件及其格式分析(http://blog.csdn.net/livelylittlefish/article/details/6448885)。
四、擴展話題
通過上面三部分的介紹,相信絕大多數覆蓋率問題都可以解決,下面2個問題是我們在實際運行過程中遇到的,也分享一下。
覆蓋率的結果只有被測試到的文件會被顯示,并非所有被編譯的代碼都被作為覆蓋率的分母
實際上,可以看到整個覆蓋率的產生的過程是4個步驟的流程,一般都通過外圍腳本,或者makefile/shell/python來把整個過程自動化。2個思路去解決這個問題,都是通過外圍的偽裝。第一個,就是修改lcov的 app.info ,中間文件,找到其他的文件與覆蓋率信息的地方,結合makefile,把所有被編譯過的源程序檢查是否存于 app.info 中,如果沒有,增加進去。第二個偽裝,是偽裝 *.gcda,沒有一些源碼覆蓋率信息的原因就是該文件沒有被調用到,沒有響應的gcda文件產生。toast(http://toast.taobao.org/)是通過第一種偽裝來實現的,更多了解需要去看下開源代碼。
2. 后臺進程的覆蓋率數據收集;
其實上述覆蓋率信息的產生,不僅可以針對單元測試,對于功能測試同樣適用。但功能測試,一般linux下c/c++都是實現了某個Daemon進程,而覆蓋率產生的條件是程序需要正常退出,即用戶代碼調用 exit 正常結束時,gcov_exit 函數才得到調用,其繼續調用 __gcov_flush 函數輸出統計數據到 *.gcda 文件中。同樣2個思路可以解決這個問題,
第一,給被測程序增加一個 signal handler,攔截 SIGHUP、SIGINT、SIGQUIT、SIGTERM 等常見強制退出信號,并在 signal handler 中主動調用 exit 或 __gcov_flush 函數輸出統計結果。但這個需要修改被測程序。這個也是我們之前的通用做法。但參加過清無同學的一個講座后,發現了下面第二種更好的方法。
第二,借用動態庫預加載技術和 gcc 擴展的 constructor 屬性,我們可以將 signalhandler 和其注冊過程都封裝到一個獨立的動態庫中,并在預加載動態庫時實現信號攔截注冊。這樣,就可以簡單地通過如下命令行來實現異常退出時的統計結果輸出了。