天天看點

C++代碼覆寫率調研總結

最近公司要做C++代碼的覆寫率,以此來記錄下C++代碼覆寫率的調研結果和總結,

本文參考了:https://blog.csdn.net/yanxiangyfg/article/details/80989680

LCOV Code Coverage

gcov—a Test Coverage Program

Options for Debugging Your Program

gcov lcov genhtml for kernel and app

Linux平台代碼覆寫率測試工具GCOV簡介 系列文章

本文使用Linux下的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獲得的資訊。

坑點:

1、通過将一些代碼行合并到一個函數中,可能不會提供足夠的資訊來查找代碼使用大量計算機時間的“熱點”。同樣地,由于gcov按行(在最低的分辨率下)積累統計資料,它最适合于隻在每行上放置一個語句的程式設計風格。如果您使用擴充到循環或其他控制結構的複雜宏,那麼統計資訊就沒有那麼有用了——它們隻報告出現宏調用的行。如果您的複雜宏的行為類似于函數,那麼您可以用inline fu替換它們。

2、gcov隻在使用GCC編譯的代碼上工作。它與任何其他概要或測試覆寫機制不相容。

gcov執行過程:

C++代碼覆寫率調研總結

工作原理和流程:

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檔案,前後彙編檔案.
    
    [email protected]:~/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
               

    $ 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檔案,前後彙編檔案.

    [email protected]:~/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++代碼覆寫率調研總結
      其中

      #####

      表示未運作的行
    • 每行前面的數字表示行運作的次數
  • 上述生成的.c.gcov檔案可視化成都較低,需要借助lcov,genhtml工具直接生成html報告。
    • 根據

      .gcno .gcda

      檔案生成圖形化報告

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

    • C++代碼覆寫率調研總結
    • C++代碼覆寫率調研總結

四、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大小,就可以推斷所有的大小。

  • C++代碼覆寫率調研總結

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

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

2. gcov怎樣插樁來更新覆寫率資料的

當打開gcov編譯選項是,在彙編階段,插樁就已經完成,這裡引用寫的很好的一篇文章來說明:

https://github.com/yanxiangyfg/gcov

C++代碼覆寫率調研總結

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

繼續閱讀