linux平台代码覆盖率

1. gcov简介

1.1 gcov是什么

  • gcov是一个测试代码覆盖率的工具。与GCC一起使用来分析程序,以帮助创建更高效、更快的运行代码,并发现程序的未测试部分
  • 是一个命令行方式的控制台程序。需要结合lcov,gcovr等前端图形工具才能实现统计数据图形化
  • 伴随GCC发布,不需要单独下载gcov工具。配合GCC共同实现对c/c++文件的语句覆盖和分支覆盖测试
  • 与程序概要分析工具(profiling tool,例如gprof)一起工作,可以估计程序中哪段代码最耗时

 

1.2 gcov能做什么

使用象gcov或gprof这样的分析器,您可以找到一些基本的性能统计数据:

  • 每一行代码执行的频率是多少
  • 实际执行了哪些行代码,配合测试用例达到满意的覆盖率和预期工作
  • 每段代码使用了多少计算时间,从而找到热点优化代码
  • gcov创建一个sourcefile.gcov的日志文件,此文件标识源文件sourcefile.c每一行执行的次数,您可以与gprof一起使用这些日志文件来帮助优化程序的性能。gprof提供了您可以使用的时间信息以及从gcov获得的信息。

 

1.3 注意事项

  • 通过将一些代码行合并到一个函数中,可能不会提供足够的信息来查找代码使用大量计算机时间的“热点”。同样地,由于gcov按行(在最低的分辨率下)积累统计数据,它最适合于只在每行上放置一个语句的编程风格。如果您使用扩展到循环或其他控制结构的复杂宏,那么统计信息就没有那么有用了——它们只报告出现宏调用的行。如果您的复杂宏的行为类似于函数,那么您可以用inline fu替换它们。
  • gcov只在使用GCC编译的代码上工作。它与任何其他概要或测试覆盖机制不兼容。

 

2. gcov过程概况

<主要工作流>

  • 编译前,在编译器中加入编译器参数-fprofile-arcs -ftest-coverage;
  • 源码经过编译预处理,然后编译成汇编文件,在生成汇编文件的同时完成插桩。插桩是在生成汇编文件的阶段完成的,因此插桩是汇编时候的插桩,每个桩点插入3~4条汇编语句,直接插入生成的*.s文件中,最后汇编文件汇编生成目标文件,生成可执行文件;并且生成关联BB和ARC的.gcno文件;
  • 执行可执行文件,在运行过程中之前插入桩点负责收集程序的执行信息。所谓桩点,其实就是一个变量,内存中的一个格子,对应的代码执行一次,则其值增加一次;
  • 生成.gcda文件,其中有BB和ARC的执行统计次数等,由此经过加工可得到覆盖率。

 

3. 使用gcov的3个阶段

3.1 编译阶段

要开启gcov功能,需要在源码编译参数中加入-fprofile-arcs -ftest-coverage 或者--coverage

  • -ftest-coverage:在编译的时候产生.gcno文件,它包含了重建基本块图和相应的块的源码的行号的信息。
  • -fprofile-arcs:在运行编译过的程序的时候,会产生.gcda文件,它包含了弧跳变的次数等信息。

打开–g3 选项,去掉-O2以上级别的代码优化选项;否则编译器会对代码做一些优化,例如行合并,从而影响行覆盖率结果;

以 test.c为例,源码如下:

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

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

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

    return 0;
}

添加Makefile 如下:

ifeq ($(use_gcov),yes)
GCOV_FLAGS := -fprofile-arcs -ftest-coverage
endif

test: test.o
	gcc $(GCOV_FLAGS) -o $@ $^

%.o : %.c
	gcc -S -o test.s $<
	gcc -c -g -I. $(GCOV_FLAGS) -o $@ $<

clean:
	@rm -f *.o
	@rm -f test
	@rm -f *.gcno
	@rm -f *.gcov
	@rm -f *.gcda

make命令执行后,会生成test.s test.o test 三个文件;

make use_gcov=yes后,除了上面这三个文件,还多一个test.gcno文件,如果源码不变test.gcno 文件是不会变的。

 

3.2 gcov收集代码运行信息

运行test,生成一个test.gcda 文件,其中包含了代码基本块和狐跳变次数统计信息

 

3.3 生成gcov代码覆盖率报告

运行gcov test.c,会产生一个test.c.gcov 文件,其中包含了代码覆盖率数据,其数据的来源为 test.gcda

如果在运行gcov之前并没有执行test 程序,即没有产生test.gcda的时候会显示:

test.gcda:cannot open data file, assuming not executed
File 'test.c'
No executable lines
Removing 'test.c.gcov'

如果已经运行了test程序,即产生了test.gcda文件时会显示:

File 'test.c'
Lines executed:66.67% of 9
Creating 'test.c.gcov'

 

打开test.c.gcov 文件,如下:

        -:    0:Source:test.c
        -:    0:Graph:test.gcno
        -:    0:Data:test.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:#include <stdio.h>
        -:    2:#include <string.h>
        -:    3:
        1:    4:int main(int argc, char *argv[])
        -:    5:{   
        1:    6:    if (argc >=2) {
    #####:    7:        printf("=====argc>=2\n");
    #####:    8:        return 0;
        -:    9:    }
        1:   10:    printf("helloworld begin\n");
        -:   11:
        1:   12:    if (argc <2){
        1:   13:        printf("=====argc<2\n");
        1:   14:        return 0;
        -:   15:    }
        -:   16:
    #####:   17:    return 0;
        -:   18:}
  • 其中#####表示未运行的行
  • 每行前面的数字表示行运行的次数

 

4. gcov 覆盖率统计原理

4.1 基本术语

  • 基本块(Basic Block),”A basic block is a sequence of instructions with only entry and only one exit. If any one of the instructions are executed, they will all be executed, and in sequence from first to last.”  这里可以把基本块看成一行整体的代码,基本块内的代码是线性的,要不全部运行,要不都不运行;
  • 基本块图(Basic Block Graph),基本块的最后一条语句一般都要跳转,否则后面一条语句也会被计算为基本块的一部分。 如果跳转语句是有条件的,就产生了一个分支(arc),该基本块就有两个基本块作为目的地。如果把每个基本块当作一个节点,那么一个函数中的所有基本块就构成了一个有向图,称之为基本块图(Basic Block Graph)。且只要知道图中部分BB或arc的执行次数就可以推算出所有的BB和所有的arc的执行次数;
  • 打桩,意思是在有效的基本块之间增加计数器,计算该基本块被运行的次数;打桩的位置都是在基本块图的有效边上;
  • 行覆盖率(line coverage),源代码有效行数与被执行的代码行的比率;
  • 分支覆盖率(branch coverage),有判定语句的地方都会出现2个分支,整个程序经过的分支与所有分支的比率是分支覆盖率。注意,与条件覆盖率(condition coverage)有细微差别,条件覆盖率在判定语句的组合上有更细的划分。

 

4.2 原理概述

Gcc中指定-ftest-coverage 等覆盖率测试选项后,gcc 会:

  • 在输出目标文件中留出一段存储区保存统计数据
  • 在源代码中每行可执行语句生成的代码之后附加一段更新覆盖率统计结果的代码,也就是前文说的插桩
  • 在最终可执行文件中进入用户代码 main 函数之前调用 gcov_init 内部函数初始化统计数据区,并将gcov_exit 内部函数注册为 exit handlers用户代码调用 exit 正常结束时,gcov_exit 函数得到调用,其继续调用 __gcov_flush 函数输出统计数据到 *.gcda 文件中

具体可以查看:https://github.com/yanxiangyfg/gcov

 

5. gcov/lcov/gcovr

5.1 gcov

gcov是由gcc工具链提供的代码覆盖率生成工具,可以很方便的和GCC编译器配合使用,通常情况下,直接安装gcc工具链,也就同时包含了gcov命令行工具。

通过gcov指定源码文件的名称,便可以得到该源码文件的覆盖率结果:

gcov test.c

 

5.2 lcov

gcov得到的结果是文本形式的,而且不同的源码文件需要一一执行gcov命令,对于大工程是不方便的,我们希望得到更加美观和便于浏览的结果。

lcov是gcov工具的图形前端,收集多个源文件的gcov数据,生成描述覆盖率的HTML页面。生成的结果中会包含概述页面,方面浏览。

lcov有很多参数配合使用可以满足各种需求,lcov的使用方法可以通过以下这条命令查询:

lcov --help

我们一般关注以下这几个参数:

  • -c 或者 --capture 指定从编译产物中收集覆盖率信息。
  • -d DIR 或者 --directory DIR 指定编译产物的路径。
  • -e FILE PATTERN 或者 --extract FILE PATTERN 从指定的文件中根据PATTERN过滤结果。
  • -o FILENAME 或者 --output-file FILENAME 指定覆盖率输出的文件名称。

此外,特殊说明:

  • lcov默认不会打开分支覆盖率,因此我们还需要增加这个参数来打开分支覆盖率的计算: --rc lcov_branch_coverage=1
  • lcov输出的仍然是一个中间产物,我们还需要通过lcov软件包提供的另外一个命令genhtml来生成最终需要的html格式的覆盖率报告文件。 同样的,为了打开分支覆盖率的计算,我们也要为这个命令增加--rc lcov_branch_coverage=1参数

最后,我们编辑一个make_all.sh脚本执行lcov相关操作:

COVERAGE_FILE=coverage.info
REPORT_FOLDER=coverage_report
lcov --rc lcov_branch_coverage=1 -c -d . -o ${COVERAGE_FILE}_tmp
lcov --rc lcov_branch_coverage=1  -e ${COVERAGE_FILE}_tmp "*src*" -o ${COVERAGE_FILE}
genhtml --rc genhtml_branch_coverage=1 ${COVERAGE_FILE} -o ${REPORT_FOLDER}

 

5.3 gcovr

gcovr是一款针对C/C++代码覆盖率并支持以多种方式(包括列表方式、XML文件方式、HTML网页方式等)展示出来的工具,而XML文件刚好是可以被持续集成工具解析的。

gcovr有很多参数配合使用可以满足各种需求,gcovr的使用方法可以通过以下这条命令查询:

gcovr --help

我们一般关注以下这几个参数:

  • -r ROOT 或者 --root ROOT 代码根目录,默认为'.',当前的路径。
  • -b 或者 --branches 以分支覆盖率形式报告。
  • -x 或者 --xml 指定报告的形式为XML。
  • --xml-pretty 美观xml
  • -o OUTPUT 或者 --output OUTPUT 指定覆盖率输出的文件名称,默认输出到stdout
  • --html 指定报告的形式为HTML

在项目的编译根目录下使用如下命令:

gcovr -r ./src/ --object-directory ./gcov --xml-pretty -o gcovr-report.xml

-r 指定源文件路径,--object-directory指定gcno和gcda路径(一般gcno和gcda放一个目录中),-o 是输出文件

 

结果如下:

代码覆盖率和分支覆盖率能清晰的看到,其中打钩为执行到,打叉为没有执行,如果是if 判断会第一个代表true状态,第二个代表false状态。

例如第二个case,if(argc < 2),在直接运行test 的时候,现在参数个数小于2,true的时候打钩,false的时候打叉。

 

6. 后台服务程序覆盖率统计

  • 从 gcc coverage test 实现原理可知,若用户进程并非调用 exit 正常退出,覆盖率统计数据就无法输出,也就无从生成报告了。
  • 后台服务程序一旦启动就很少主动退出,用 kill 杀死进程强制退出时就不会调用 exit,因此没有覆盖率统计结果产生。

为了解决这个问题,我们可以给待测程序增加一个 signal handler,拦截 SIGHUP、SIGINT、SIGQUIT、SIGTERM 等常见强制退出信号,并在 signal handler 中主动调用 exit 或 __gcov_flush 函数输出统计结果即可。但是,该方案仍然需要修改待测程序代码。

可以借用动态库预加载技术和 gcc 扩展的 constructor 属性,我们可以将 signa lhandler 和其注册过程都封装到一个独立的动态库中,并在预加载动态库时实现信号拦截注册。

命令大概如下:

LD_PRELOAD=./libgcov_preload.so ./test

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

 

文件代码如下:

#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");
        }
    } 
}

其中__attribute__ ((constructor))是gcc的符号,它修饰的函数会在main函数执行之前调用,我们利用它把异常信号拦截到我们自己的函数中。

 

 

 

参考:

https://blog.csdn.net/yanxiangyfg/article/details/80989680

https://www.linuxidc.com/Linux/2011-05/36572.htm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

私房菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值