gcov代碼覆蓋率測試-原理和實踐總結


轉載自:https://blog.csdn.net/yanxiangyfg/article/details/80989680

講的很清晰

 

一、gcov簡介
gcov是什么
gcov是一個測試代碼覆蓋率的工具。與GCC一起使用來分析程序,以幫助創建更高效、更快的運行代碼,並發現程序的未測試部分
是一個命令行方式的控制台程序。需要結合lcov,gcovr等前端圖形工具才能實現統計數據圖形化
伴隨GCC發布,不需要單獨下載gcov工具。配合GCC共同實現對c/c++文件的語句覆蓋和分支覆蓋測試
與程序概要分析工具(profiling tool,例如gprof)一起工作,可以估計程序中哪段代碼最耗時
gcov能做什么
使用象gcov或gprof這樣的分析器,您可以找到一些基本的性能統計數據:
* 每一行代碼執行的頻率是多少
* 實際執行了哪些行代碼,配合測試用例達到滿意的覆蓋率和預期工作
* 每段代碼使用了多少計算時間,從而找到熱點優化代碼
* gcov創建一個sourcefile.gcov的日志文件,此文件標識源文件sourcefile.c每一行執行的次數,您可以與gprof一起使用這些日志文件來幫助優化程序的性能。gprof提供了您可以使用的時間信息以及從gcov獲得的信息。

注意事項
通過將一些代碼行合並到一個函數中,可能不會提供足夠的信息來查找代碼使用大量計算機時間的“熱點”。同樣地,由於gcov按行(在最低的分辨率下)積累統計數據,它最適合於只在每行上放置一個語句的編程風格。如果您使用擴展到循環或其他控制結構的復雜宏,那么統計信息就沒有那么有用了——它們只報告出現宏調用的行。如果您的復雜宏的行為類似於函數,那么您可以用inline fu替換它們。
gcov只在使用GCC編譯的代碼上工作。它與任何其他概要或測試覆蓋機制不兼容。
二、gcov過程概況


<主要工作流>
1) 編譯前,在編譯器中加入編譯器參數-fprofile-arcs -ftest-coverage;
2) 源碼經過編譯預處理,然后編譯成匯編文件,在生成匯編文件的同時完成插樁。插樁是在生成匯編文件的階段完成的,因此插樁是匯編時候的插樁,每個樁點插入3~4條匯編語句,直接插入生成的*.s文件中,最后匯編文件匯編生成目標文件,生成可執行文件;並且生成關聯BB和ARC的.gcno文件;
3) 執行可執行文件,在運行過程中之前插入樁點負責收集程序的執行信息。所謂樁點,其實就是一個變量,內存中的一個格子,對應的代碼執行一次,則其值增加一次;
4) 生成.gcda文件,其中有BB和ARC的執行統計次數等,由此經過加工可得到覆蓋率。
三、使用gcov的3個階段
1. 編譯階段
要開啟gcov功能,需要在源碼編譯參數中加入-fprofile-arcs -ftest-coverage
* -ftest-coverage:在編譯的時候產生.gcno文件,它包含了重建基本塊圖和相應的塊的源碼的行號的信息。
* -fprofile-arcs:在運行編譯過的程序的時候,會產生.gcda文件,它包含了弧跳變的次數等信息。

如下以helloworld_gcov.c為例子,源碼如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
if (argc >=2) {
printf("=====argc>=2\n");
return;
}
printf("helloworld begin\n");

if (argc <2){
printf("=====argc<2\n");
return;
}
return;
}

helloworld_gcov.c的Makefile的書寫如下,在編譯選項CFLAGS中加入-fprofile-arcs -ftest-coverage選項:

#加入gcov編譯選項,通過宏PRJRELEASE=gcov控制
ifeq ("$(PRJRELEASE)","gcov")
CFLAGS+= -fprofile-arcs -ftest-coverage
endif

CC=gcc

.PHONE: all

all: helloworld

helloworld: *.c
# 編譯出匯編和gcno文件
@echo ${CFLAGS}
@${CC} ${CFLAGS} -S -o helloworld_gcov.s helloworld_gcov.c
@${CC} ${CFLAGS} -o helloworld_gcov helloworld_gcov.c

.PHONE: clean
clean:
@-rm helloworld_gcov helloworld_gcov.gcno helloworld_gcov.gcda helloworld_gcov.c.gcov helloworld_gcov.s

在helloworld目錄下執行make命令后,產生helloworld_gcov.s,helloworld_gcov helloworld_gcov.gcno. helloworld_gcov.gcno只要源碼不變,編譯出來永遠不改變.
運行gcov helloworld_gcov.c命令產生原始的代碼覆蓋率數據文件helloworld_gcov.c.gcov, 由於此時沒有運行./helloworld_gcov,沒有helloworld_gcov.gcda統計數據,覆蓋率為0
2. gcov收集代碼運行信息
運行./helloworld_gcov產生helloworld_gcov.gcda文件,其中包含了代碼基本塊和狐跳變次數統計信息
3. 生成gcov代碼覆蓋率報告
再次運行gcov helloworld_gcov.c產生的helloworld_gcov.c.gcov中包含了代碼覆蓋率數據,其數據的來源為helloworld_gcov.gcda
為了對比運行./helloworld_gcov前后的覆蓋率數據文件helloworld_gcov.c.gcov信息,直接執行如下腳本,產生前后數據對比

$ make #編譯
$ gcov helloworld_gcov.c #生成原始的helloworld_gcov.c.gcov文件
$ cp helloworld_gcov.c.gcov helloworld_gcov.c.gcov-old #備份好原始的helloworld_gcov.c.gcov文件,方便后續對比
$ cp helloworld_gcov.gcno helloworld_gcov.gcno-old #備份好原始的helloworld_gcov.gcno文件,方便后續對比
$ ./helloworld_gcov #產生helloworld_gcov.gcda文件,記錄的代碼運行的統計數據
$ gcov helloworld_gcov.c #根據gcda文件,再次生成helloworld_gcov.c.gcov文件


#最后顯示如下,可以對比先后的gcov文件,前后匯編文件.

yangfogen@ubuntu:~/work/helloworld_gcov$ ls
helloworld_gcov helloworld_gcov.c.gcov helloworld_gcov.gcda helloworld_gcov.gcno-old helloworld_gcov.s
helloworld_gcov.c helloworld_gcov.c.gcov-old helloworld_gcov.gcno helloworld_gcov-gcov.s Makefile


其中#####表示未運行的行
每行前面的數字表示行運行的次數
上述生成的.c.gcov文件可視化成都較低,需要借助lcov,genhtml工具直接生成html報告。

根據.gcno .gcda文件生成圖形化報告

$ lcov -c -d . -o helloworld_gcov.info
$ genhtml -o 111 helloworld_gcov.info


四、gcov檢測代碼覆蓋率的原理
原理概述
Gcc中指定-ftest-coverage 等覆蓋率測試選項后,gcc 會:
* 在輸出目標文件中留出一段存儲區保存統計數據
* 在源代碼中每行可執行語句生成的代碼之后附加一段更新覆蓋率統計結果的代碼,也就是前文說的插樁
* 在最終可執行文件中進入用戶代碼 main 函數之前調用 gcov_init 內部函數初始化統計數據區,並將gcov_exit 內部函數注冊為 exit handlers用戶代碼調用 exit 正常結束時,gcov_exit 函數得到調用,其繼續調用 __gcov_flush 函數輸出統計數據到 *.gcda 文件中

說了這么多,其實還是很模糊,這里有幾個要點需要深入
怎么計算統計數據的?
gcov怎樣插樁來更新覆蓋率數據的
gcov_init和gcov_exit怎樣放到編譯的可執行文件中的
gcno和gcda文件格式是咋樣的
只有把這幾個問題搞明白了,才算真正搞懂gcov的原理.那么下面就來好好分析這幾個問題

1. gcov數據統計原理(即:gcov怎么計算統計數據的)
gcov是使用 基本塊BB 和 跳轉ARC 計數,結合程序流圖來實現代碼覆蓋率統計的:

1.基本塊BB
如果一段程序的第一條語句被執行過一次,這段程序中的每一個都要執行一次,稱為基本塊。一個BB中的所有語句的執行次數一定是相同的。一般由多個順序執行語句后邊跟一個跳轉語句組成。所以一般情況下BB的最后一條語句一定是一個跳轉語句,跳轉的目的地是另外一個BB的第一條語句,如果跳轉時有條件的,就產生了分支,該BB就有兩個BB作為目的地。

2.跳轉ARC
從一個BB到另外一個BB的跳轉叫做一個arc,要想知道程序中的每個語句和分支的執行次數,就必須知道每個BB和ARC的執行次數

3. 程序流圖
如果把BB作為一個節點,這樣一個函數中的所有BB就構成了一個有向圖。,要想知道程序中的每個語句和分支的執行次數,就必須知道每個BB和ARC的執行次數。根據圖論可以知道有向圖中BB的入度和出度是相同的,所以只要知道了部分的BB或者arc大小,就可以推斷所有的大小。

 

這里選擇由arc的執行次數來推斷BB的執行次數。

所以對部分 ARC插樁,只要滿足可以統計出來所有的BB和ARC的執行次數即可。

2. gcov怎樣插樁來更新覆蓋率數據的
當打開gcov編譯選項是,在匯編階段,插樁就已經完成,這里引用寫的很好的一篇文章來說明:

https://github.com/yanxiangyfg/gcov

 

4. gcno和gcda文件格式
https://github.com/tejainece/gcov

五、服務程序覆蓋率統計
從 gcc coverage test 實現原理可知,若用戶進程並非調用 exit 正常退出,覆蓋率統計數據就無法輸出,也就無從生成報告了。
后台服務程序一旦啟動就很少主動退出,用 kill 殺死進程強制退出時就不會調用 exit,因此沒有覆蓋率統計結果產生。
為了解決這個問題,我們可以給待測程序增加一個 signal handler,攔截 SIGHUP、SIGINT、SIGQUIT、SIGTERM 等常見強制退出信號,並在 signal handler 中主動調用 exit 或 __gcov_flush 函數輸出統計結果即可。

該方案仍然需要修改待測程序代碼,不過借用動態庫預加載技術和 gcc 擴展的 constructor 屬性,我們可以將 signalhandler 和其注冊過程都封裝到一個獨立的動態庫中,並在預加載動態庫時實現信號攔截注冊。這樣,就可以簡單地通過如下命令行來實現異常退出時的統計結果輸出了:

LD_PRELOAD=./libgcov_preload.so ./helloworld_server

#或者:
echo "/sbin/gcov_preload.so" >/etc/ld.so.preload
./helloworld_server

其中__attribute__ ((constructor))是gcc的符號,它修飾的函數會在main函數執行之前調用,我們利用它把異常信號攔截到我們自己的函數中. 【注:具體代碼請看文章后面的例子章節】

測試完畢后可直接 kill 掉 helloworld_server 進程,並獲得正常的統計結果文件 *.gcda。

六、內核和模塊的gcov代碼覆蓋率測試
從Linux內核2.6.31開始,gcov-kernel是Linux內核的一部分,可以不使用額外的補丁
啟用gcov-kernel配置選項:

CONFIG_DEBUG_FS=y
CONFIG_GCOV_KERNEL=y
CONFIG_GCOV_PROFILE_ALL=y #獲取內核數據覆蓋率
CONFIG_GCOV_FORMAT_AUTODETECT=y #選擇gcov的格式
編譯,安裝,啟動內核,然后掛載debugfs: mount -t debugfs none /sys/kernel/debug
內核相關文件介紹


#支持gcov的內核在debugfs中創建如下幾個文件或文件夾

 

#所有gcov相關文件的父目錄

/sys/kernel/debug/gcov


#全局重置文件:在寫入時將所有覆蓋率數據重置為零

/sys/kernel/debug/gcov/reset


#gcov工具理解的實際gcov數據文件。當寫入文件時,將文件覆蓋率數據重置為零

/sys/kernel/debug/gcov/path/to/compile/dir/file.gcda


#gcov工具所需的靜態數據文件的符號鏈接。這個文件是gcc在編譯時生成的, 選項:-ftest-coverage

/sys/kernel/debug/gcov/path/to/compile/dir/file.gcno

需要注意的是/sys/kernel/debug 文件夾是一個臨時文件夾,不存在於磁盤當中,是在內存當中存在的,其中的文件也是系統運行是動態產生的

七、lcov工具使用
安裝lcov工具, 以ubuntu為例子: sudo apt install lcov,用於使gcno和gcda文件生成info覆蓋率統計文件.
關於lcov的詳細文檔請看: LCOV - the LTP GCOV extension
在home目錄下創建一個~/.lcovrc文件,並加入一行geninfo_auto_base = 1,用於消除ERROR: could not read source file錯誤
八、info文件格式信息
lcov生成的.info文件包含一個或多個源文件所對應的覆蓋率信息,一個源文件對應一條“記錄”,“記錄”中的詳細格式如下

TN: <Test name> 表示測試用例名稱,即通過geninfo中的--test-name選項來命名的測試用例名稱,默認為空;

SF: <File name> 表示帶全路徑的源代碼文件名;

FN: <函數啟始行號>, <函數名>; <函數有效行總數>; <函數有效行總數中被執行個數>

FNDA: <函數被執行的次數>, <函數名>; <函數有效行總數>; <函數有效行總數中被執行個數>

FNF: <函數總數>

FNH: <函數總數中被執行到的個數> BRDA: <分支所在行號>, <對應的代碼塊編號>, <分支編號>, <執行的次數> BRF: <分支總數> BRH: <分支總數中被執行到的個數> DA: <代碼行號>, <當前行被執行到的次數> LF: < counts> 代碼有效行總數 LH: <counts> 代碼有效行總數中被執行到的個數 end_of_record 一條“記錄”結束符

九、例子
1. 合並不同用例的代碼覆蓋率
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
if (argc >=2) {
printf("=====argc>=2\n");
return;
}
printf("helloworld begin\n");

if (argc <2){
printf("=====argc<2\n");
return;
}
return;
}

簡單編寫的Makefile如下:

.PHONE: all
all: helloworld

CFLAGS+= -fprofile-arcs -ftest-coverage
CC=gcc

helloworld: *.c
@echo ${CFLAGS}
@${CC} ${CFLAGS} -o helloworld helloworld_gcov.c

.PHONE: clean
clean:
@-rm helloworld

單獨產生同一個程序不同用例的info並合並

make
#運行兩個參數用例並產生info文件和html文件
./helloworld i 2
lcov -c -d . -o helloworld2.info
genhtml -o 222 helloworld2.info

#運行無參數用例並產生info文件和html文件
rm helloworld_gcov.gcda
./helloworld
lcov -c -d . -o helloworld1.info
genhtml -o 111 helloworld1.info

#合並兩個用例產生的info文件,輸出同一個模塊不同用例的總的統計數據
genhtml -o 333 helloworld1.info helloworld2.info

2. 服務程序無exit時產生gcda文件的方法
helloworld_server_gcov.c的代碼:
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <stdlib.h>
#include <dlfcn.h>
#include <signal.h>
#include <errno.h>

int main(int argc, char *argv[])
{
if (argc >=2) {
printf("=====argc>=2\n");
}
printf("helloworld begin\n");

if (argc <2){
printf("=====argc<2\n");
}

while(1){

printf("this is the server body");
sleep(5);
}
return 0;
}

編譯helloworld_server_gcov.c的Makefile:
ifeq ("$(PRJRELEASE)","gcov")
CFLAGS+= -fprofile-arcs -ftest-coverage
endif

CC=gcc

.PHONE: all

all: helloworld

helloworld: *.c
@echo ${CFLAGS}
@${CC} ${CFLAGS} -o helloworld_server helloworld_server_gcov.c

.PHONE: clean
clean:
@-rm helloworld_server helloworld_server_gcov.gcno helloworld_server_gcov.gcda

gcov_preload.c主要作用為捕獲信號,調用gcov相關函數產生gcda文件。此文件編譯成gcov_preload.so
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <signal.h>
#define SIMPLE_WAY

void sighandler(int signo)
{
#ifdef SIMPLE_WAY
exit(signo);
#else
extern void __gcov_flush();
__gcov_flush(); /* flush out gcov stats data */
raise(signo); /* raise the signal again to crash process */
#endif
}

/**
* 用來預加載的動態庫gcov_preload.so的代碼如下,其中__attribute__ ((constructor))是gcc的符號,
* 它修飾的函數會在main函數執行之前調用,我們利用它把異常信號攔截到我們自己的函數中,然后調用__gcov_flush()輸出錯誤信息
* 設置預加載庫 LD_PRELOAD=./gcov_preload.so
*/

__attribute__ ((constructor))

void ctor()
{
int sigs[] = {
SIGILL, SIGFPE, SIGABRT, SIGBUS,
SIGSEGV, SIGHUP, SIGINT, SIGQUIT,
SIGTERM
};
int i;
struct sigaction sa;
sa.sa_handler = sighandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESETHAND;

for(i = 0; i < sizeof(sigs)/sizeof(sigs[0]); ++i) {
if (sigaction(sigs[i], &sa, NULL) == -1) {
perror("Could not set signal handler");
}
}
}

編譯gcov_preload.c
gcc -shared -fpic gcov_preload.c -o libgcov_preload.so
1
編譯出libgcov_preload.so后拷貝到helloworld_server_gcov.c同目錄下,然后在編譯helloworld_server_gcov.c,最后運行,執行CTRL+c正常結束helloworld_server且產生了gcda文件。

FAQ
問題1
ERROR: could not read source file /home/user/project/sub-dir1/subdir2/subdir1/subdir2/file.c
1
解決方法
在home目錄下創建一個~/.lcovrc文件,並加入一行geninfo_auto_base = 1

出現此問題的原因是: 當編譯工具鏈和源碼不在同一個目錄下時,會出現ERROR: could not read source file錯誤,這個geninfo_auto_base = 1選項指定geninfo需要自動確定基礎目錄來收集代碼覆蓋率數據.

問題2
使用lcov [srcfile]的命令生成.info文件的時候,提示如下錯誤, 無法生成info文件:

xxxxxxxxxxxx.gcno:version '402*', prefer '408*'
Segmentation fault

解決方法
在lcov工具中使用–gcov-tool選項選擇需要的gcov版本,如lcov --gcov-tool /usr/bin/gcov-4.2
————————————————
版權聲明:本文為CSDN博主「yanxiangyfg」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/yanxiangyfg/article/details/80989680


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM