gcov簡介gcov是什么gcov能做什么
使用象gcov或gprof這樣的分析器,您可以找到一些基本的性能統計數據:
注意事項gcov過程概況
gcov過程
主要工作流
編譯前,在編譯器中加入編譯器參數--arcs -ftest-;源碼經過編譯預處理,然后編譯成匯編文件,在生成匯編文件的同時完成插樁。插樁是在生成匯編文件的階段完成的,因此插樁是匯編時候的插樁代碼覆蓋率統計工具,每個樁點插入3~4條匯編語句,直接插入生成的*.s文件中,最后匯編文件匯編生成目標文件,生成可執行文件;并且生成關聯BB和ARC的.gcno文件;執行可執行文件,在運行過程中之前插入樁點負責收集程序的執行信息。所謂樁點,其實就是一個變量,內存中的一個格子,對應的代碼執行一次,則其值增加一次;生成.gcda文件,其中有BB和ARC的執行統計次數等,由此經過加工可得到覆蓋率。使用gcov的3個階段編譯階段
要開啟gcov功能,需要在源碼編譯參數中加入--arcs -ftest-
如下以.c為例子,源碼如下:
#include
#include
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;
}
.c的的書寫如下,在編譯選項中加入--arcs -ftest-選項:
#加入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
gcov收集代碼運行信息生成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
$ lcov -c -d . -o helloworld_gcov.info
$ genhtml -o 111 helloworld_gcov.info
gcov檢測代碼覆蓋率的原理原理概述
Gcc中指定-ftest- 等覆蓋率測試選項后,gcc 會:
說了這么多,其實還是很模糊,這里有幾個要點需要深入:
只有把這幾個問題搞明白了,才算真正搞懂gcov的原理.那么下面就來好好分析這幾個問題
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的執行次數即可。
gcov怎樣插樁來更新覆蓋率數據的
當打開gcov編譯選項是,在匯編階段,插樁就已經完成,這里引用寫的很好的一篇文章來說明:
gcno和gcda文件格式
服務程序覆蓋率統計
為了解決這個問題,我們可以給待測程序增加一個 ,攔截 、、、 等常見強制退出信號,并在 中主動調用 exit 或 函數輸出統計結果即可。
該方案仍然需要修改待測程序代碼,不過借用動態庫預加載技術和 gcc 擴展的 屬性,我們可以將 和其注冊過程都封裝到一個獨立的動態庫中,并在預加載動態庫時實現信號攔截注冊。這樣,就可以簡單地通過如下命令行來實現異常退出時的統計結果輸出了:
LD_PRELOAD=./libgcov_preload.so ./helloworld_server
#或者:
echo "/sbin/gcov_preload.so" >/etc/ld.so.preload
./helloworld_server
測試完畢后可直接 kill 掉 進程,并獲得正常的統計結果文件 *.gcda。
內核和模塊的gcov代碼覆蓋率測試
CONFIG_DEBUG_FS=y
CONFIG_GCOV_KERNEL=y
CONFIG_GCOV_PROFILE_ALL=y #獲取內核數據覆蓋率
CONFIG_GCOV_FORMAT_AUTODETECT=y #選擇gcov的格式
#支持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//debug 文件夾是一個臨時文件夾,不存在于磁盤當中,是在內存當中存在的,其中的文件也是系統運行是動態產生的
lcov工具使用info文件格式信息
lcov生成的.info文件包含一個或多個源文件所對應的覆蓋率信息代碼覆蓋率統計工具,一個源文件對應一條“記錄”,“記錄”中的詳細格式如下
TN: name> 表示測試用例名稱,即通過geninfo中的--test-name選項來命名的測試用例名稱,默認為空;
SF: name> 表示帶全路徑的源代碼文件名;
FN: <函數啟始行號>, <函數名>; <函數有效行總數>; <函數有效行總數中被執行個數>
FNDA: <函數被執行的次數>, <函數名>; <函數有效行總數>; <函數有效行總數中被執行個數>
FNF: <函數總數>
FNH: <函數總數中被執行到的個數>
BRDA: <分支所在行號>, <對應的代碼塊編號>, <分支編號>, <執行的次數>
BRF: <分支總數>
BRH: <分支總數中被執行到的個數>
DA: <代碼行號>, <當前行被執行到的次數>
LF: < counts> 代碼有效行總數
LH: 代碼有效行總數中被執行到的個數
end_of_record 一條“記錄”結束符
例子
1. 合并不同用例的代碼覆蓋率
#include
#include

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;
}
簡單編寫的如下:
.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文件的方法
ov.c的代碼:
#include
#include
#include
#include

#include
#include
#include
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;
}
編譯ov.c的:
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
.c主要作用為捕獲信號,調用gcov相關函數產生gcda文件。此文件編譯成.so
#include
#include
#include
#include
#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");
}
}
}
其中 (())是gcc的符號,它修飾的函數會在main函數執行之前調用,我們利用它把異常信號攔截到我們自己的函數中。
編譯.c
gcc -shared -fpic gcov_preload.c -o libgcov_preload.so
編譯出.so后拷貝到ov.c同目錄下,然后在編譯ov.c,最后運行,執行CTRL+c正常結束且產生了gcda文件。
FAQ
- 問題1
ERROR: could not read source file /home/user/project/sub-dir1/subdir2/subdir1/subdir2/file.c
解決方法
在home目錄下創建一個~/.文件,并加入一行 = 1
出現此問題的原因是: 當編譯工具鏈和源碼不在同一個目錄下時,會出現ERROR: could not read file錯誤,這個 = 1選項指定需要自動確定基礎目錄來收集代碼覆蓋率數據.
- 問題2
使用lcov []的命令生成.info文件的時候,提示如下錯誤, 無法生成info文件:
xxxxxxxxxxxx.gcno:version '402*', prefer '408*'
Segmentation fault
解決方法
在lcov工具中使用–gcov-tool選項選擇需要的gcov版本,如lcov --gcov-tool /usr/bin/gcov-4.2