一、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 过程概况

gcov代码覆盖率测试-原理和实践总结 - 图1

主要工作流

  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为例子, 源码如下:

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main(int argc, char *argv[])
  4. {
  5. if (argc >=2) {
  6. printf("=====argc>=2\n");
  7. return;
  8. }
  9. printf("helloworld begin\n");
  10. if (argc <2){
  11. printf("=====argc<2\n");
  12. return;
  13. }
  14. return;
  15. }

helloworld_gcov.c的 Makefile 的书写如下,在编译选项 CFLAGS 中加入-fprofile-arcs -ftest-coverage选项:

  1. #加入gcov编译选项,通过宏PRJRELEASE=gcov控制
  2. ifeq ("$(PRJRELEASE)","gcov")
  3. CFLAGS+= -fprofile-arcs -ftest-coverage
  4. endif
  5. CC=gcc
  6. .PHONE: all
  7. all: helloworld
  8. helloworld: *.c
  9. # 编译出汇编和gcno文件
  10. @echo ${CFLAGS}
  11. @${CC} ${CFLAGS} -S -o helloworld_gcov.s helloworld_gcov.c
  12. @${CC} ${CFLAGS} -o helloworld_gcov helloworld_gcov.c
  13. .PHONE: clean
  14. clean:
  15. @-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信息, 直接执行如下脚本, 产生前后数据对比
  1. $ make #编译
  2. $ gcov helloworld_gcov.c #生成原始的helloworld_gcov.c.gcov文件
  3. $ cp helloworld_gcov.c.gcov helloworld_gcov.c.gcov-old #备份好原始的helloworld_gcov.c.gcov文件,方便后续对比
  4. $ cp helloworld_gcov.gcno helloworld_gcov.gcno-old #备份好原始的helloworld_gcov.gcno文件,方便后续对比
  5. $ ./helloworld_gcov #产生helloworld_gcov.gcda文件,记录的代码运行的统计数据
  6. $ gcov helloworld_gcov.c #根据gcda文件,再次生成helloworld_gcov.c.gcov文件
  7. #最后显示如下,可以对比先后的gcov文件,前后汇编文件.
  8. yangfogen@ubuntu:~/work/helloworld_gcov$ ls
  9. helloworld_gcov helloworld_gcov.c.gcov helloworld_gcov.gcda helloworld_gcov.gcno-old helloworld_gcov.s
  10. helloworld_gcov.c helloworld_gcov.c.gcov-old helloworld_gcov.gcno helloworld_gcov-gcov.s Makefile

gcov代码覆盖率测试-原理和实践总结 - 图2

其中#####表示未运行的行,每行前面的数字表示行运行的次数

  • 上述生成的. c.gcov 文件可视化成都较低,需要借助 lcov,genhtml 工具直接生成 html 报告。
    gcov代码覆盖率测试-原理和实践总结 - 图3
  • 根据.gcno .gcda文件生成图形化报告
    1. $ lcov -c -d . -o helloworld_gcov.info
    2. $ genhtml -o 111 helloworld_gcov.info

    gcov代码覆盖率测试-原理和实践总结 - 图4

四、gcov 检测代码覆盖率的原理

原理概述

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

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

说了这么多, 其实还是很模糊, 这里有几个要点需要深入

  • 怎么计算统计数据的?
  • gcov 怎样插桩来更新覆盖率数据的
  • gcov_initgcov_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 大小,就可以推断所有的大小。

gcov代码覆盖率测试-原理和实践总结 - 图5

这里选择由 arc 的执行次数来推断 BB 的执行次数。

所以对部分 ARC 插桩,只要满足可以统计出来所有的 BB 和 ARC 的执行次数即可。

2. gcov 怎样插桩来更新覆盖率数据的

当打开 gcov 编译选项是,在汇编阶段,插桩就已经完成,这里引用写的很好的一篇文章来说明:

https://github.com/yanxiangyfg/gcov

gcov代码覆盖率测试-原理和实践总结 - 图6

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 和其注册过程都封装到一个独立的动态库中,并在预加载动态库时实现信号拦截注册。这样,就可以简单地通过如下命令行来实现异常退出时的统计结果输出了:

  1. LD_PRELOAD=./libgcov_preload.so ./helloworld_server
  2. echo "/sbin/gcov_preload.so" >/etc/ld.so.preload
  3. ./helloworld_server

其中__attribute__ ((constructor))是 gcc 的符号,它修饰的函数会在 main 函数执行之前调用,我们利用它把异常信号拦截到我们自己的函数中. 【注:具体代码请看文章后面的例子章节】

测试完毕后可直接 kill 掉 helloworld_server 进程,并获得正常的统计结果文件 *.gcda。

六、内核和模块的 gcov 代码覆盖率测试

  • 从 Linux 内核 2.6.31 开始,gcov-kernel 是 Linux 内核的一部分,可以不使用额外的补丁
  • 启用 gcov-kernel 配置选项:

    1. CONFIG_DEBUG_FS=y
    2. CONFIG_GCOV_KERNEL=y
    3. CONFIG_GCOV_PROFILE_ALL=y #获取内核数据覆盖率
    4. CONFIG_GCOV_FORMAT_AUTODETECT=y #选择gcov的格式
  • 编译,安装,启动内核,然后挂载 debugfs: mount -t debugfs none /sys/kernel/debug

  • 内核相关文件介绍
  1. #支持gcov的内核在debugfs中创建如下几个文件或文件夹
  2. #所有gcov相关文件的父目录
  3. /sys/kernel/debug/gcov
  4. #全局重置文件:在写入时将所有覆盖率数据重置为零
  5. /sys/kernel/debug/gcov/reset
  6. #gcov工具理解的实际gcov数据文件。当写入文件时,将文件覆盖率数据重置为零
  7. /sys/kernel/debug/gcov/path/to/compile/dir/file.gcda
  8. #gcov工具所需的静态数据文件的符号链接。这个文件是gcc在编译时生成的, 选项:-ftest-coverage
  9. /sys/kernel/debug/gcov/path/to/compile/dir/file.gcno

需要注意的是/sys/kernel/debug 文件夹是一个临时文件夹,不存在于磁盘当中,是在内存当中存在的,其中的文件也是系统运行是动态产生的

七、lcov 工具使用

  • 安装 lcov 工具, 以 ubuntu 为例子: sudo apt install lcov, 用于使 gcno 和 gcda 文件生成 info 覆盖率统计文件.
  • 在 home 目录下创建一个~/.lcovrc文件, 并加入一行geninfo_auto_base = 1, 用于消除ERROR: could not read source file错误

八、info 文件格式信息

lcov 生成的. info 文件包含一个或多个源文件所对应的覆盖率信息,一个源文件对应一条 “记录”,“记录” 中的详细格式如下

  1. TN: <Test name> 表示测试用例名称,即通过geninfo中的--test-name选项来命名的测试用例名称,默认为空;
  2. SF: <File name> 表示带全路径的源代码文件名;
  3. FN: <函数启始行号>, <函数名>; <函数有效行总数>; <函数有效行总数中被执行个数>
  4. FNDA: <函数被执行的次数>, <函数名>; <函数有效行总数>; <函数有效行总数中被执行个数>
  5. FNF: <函数总数>
  6. FNH: <函数总数中被执行到的个数> BRDA: <分支所在行号>, <对应的代码块编号>, <分支编号>, <执行的次数> BRF: <分支总数> BRH: <分支总数中被执行到的个数> DA: <代码行号>, <当前行被执行到的次数> LF: < counts> 代码有效行总数 LH: <counts> 代码有效行总数中被执行到的个数 end_of_record 一条“记录”结束符

九、例子

1. 合并不同用例的代码覆盖率

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main(int argc, char *argv[])
  4. {
  5. if (argc >=2) {
  6. printf("=====argc>=2\n");
  7. return;
  8. }
  9. printf("helloworld begin\n");
  10. if (argc <2){
  11. printf("=====argc<2\n");
  12. return;
  13. }
  14. return;
  15. }

简单编写的 Makefile 如下:

  1. .PHONE: all
  2. all: helloworld
  3. CFLAGS+= -fprofile-arcs -ftest-coverage
  4. CC=gcc
  5. helloworld: *.c
  6. @echo ${CFLAGS}
  7. @${CC} ${CFLAGS} -o helloworld helloworld_gcov.c
  8. .PHONE: clean
  9. clean:
  10. @-rm helloworld

单独产生同一个程序不同用例的 info 并合并

  1. make
  2. #运行两个参数用例并产生info文件和html文件
  3. ./helloworld i 2
  4. lcov -c -d . -o helloworld2.info
  5. genhtml -o 222 helloworld2.info
  6. #运行无参数用例并产生info文件和html文件
  7. rm helloworld_gcov.gcda
  8. ./helloworld
  9. lcov -c -d . -o helloworld1.info
  10. genhtml -o 111 helloworld1.info
  11. #合并两个用例产生的info文件,输出同一个模块不同用例的总的统计数据
  12. genhtml -o 333 helloworld1.info helloworld2.info

2. 服务程序无 exit 时产生 gcda 文件的方法

helloworld_server_gcov.c 的代码:

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <unistd.h>
  4. #include <stdlib.h>
  5. #include <dlfcn.h>
  6. #include <signal.h>
  7. #include <errno.h>
  8. int main(int argc, char *argv[])
  9. {
  10. if (argc >=2) {
  11. printf("=====argc>=2\n");
  12. }
  13. printf("helloworld begin\n");
  14. if (argc <2){
  15. printf("=====argc<2\n");
  16. }
  17. while(1){
  18. printf("this is the server body");
  19. sleep(5);
  20. }
  21. return 0;
  22. }

编译helloworld_server_gcov.c的 Makefile:

  1. ifeq ("$(PRJRELEASE)","gcov")
  2. CFLAGS+= -fprofile-arcs -ftest-coverage
  3. endif
  4. CC=gcc
  5. .PHONE: all
  6. all: helloworld
  7. helloworld: *.c
  8. @echo ${CFLAGS}
  9. @${CC} ${CFLAGS} -o helloworld_server helloworld_server_gcov.c
  10. .PHONE: clean
  11. clean:
  12. @-rm helloworld_server helloworld_server_gcov.gcno helloworld_server_gcov.gcda

gcov_preload.c主要作用为捕获信号,调用 gcov 相关函数产生 gcda 文件。此文件编译成gcov_preload.so

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <signal.h>
  4. #include <signal.h>
  5. #define SIMPLE_WAY
  6. void sighandler(int signo)
  7. {
  8. #ifdef SIMPLE_WAY
  9. exit(signo);
  10. #else
  11. extern void __gcov_flush();
  12. __gcov_flush(); /* flush out gcov stats data */
  13. raise(signo); /* raise the signal again to crash process */
  14. #endif
  15. }
  16. /**
  17. * 用来预加载的动态库gcov_preload.so的代码如下,其中__attribute__ ((constructor))是gcc的符号,
  18. * 它修饰的函数会在main函数执行之前调用,我们利用它把异常信号拦截到我们自己的函数中,然后调用__gcov_flush()输出错误信息
  19. * 设置预加载库 LD_PRELOAD=./gcov_preload.so
  20. */
  21. __attribute__ ((constructor))
  22. void ctor()
  23. {
  24. int sigs[] = {
  25. SIGILL, SIGFPE, SIGABRT, SIGBUS,
  26. SIGSEGV, SIGHUP, SIGINT, SIGQUIT,
  27. SIGTERM
  28. };
  29. int i;
  30. struct sigaction sa;
  31. sa.sa_handler = sighandler;
  32. sigemptyset(&sa.sa_mask);
  33. sa.sa_flags = SA_RESETHAND;
  34. for(i = 0; i < sizeof(sigs)/sizeof(sigs[0]); ++i) {
  35. if (sigaction(sigs[i], &sa, NULL) == -1) {
  36. perror("Could not set signal handler");
  37. }
  38. }
  39. }

编译 gcov_preload.c

  1. gcc -shared -fpic gcov_preload.c -o libgcov_preload.so

编译出libgcov_preload.so后拷贝到helloworld_server_gcov.c同目录下,然后在编译helloworld_server_gcov.c, 最后运行, 执行 CTRL+c 正常结束 helloworld_server 且产生了 gcda 文件。


FAQ

问题 1

  1. ERROR: could not read source file /home/user/project/sub-dir1/subdir2/subdir1/subdir2/file.c

解决方法

在home目录下创建一个~/.lcovrc文件,并加入一行geninfo_auto_base = 1

出现此问题的原因是: 当编译工具链和源码不在同一个目录下时,会出现ERROR: could not read source file错误,这个geninfo_auto_base = 1选项指定geninfo需要自动确定基础目录来收集代码覆盖率数据.

问题 2

使用 lcov [srcfile]的命令生成. info 文件的时候,提示如下错误, 无法生成 info 文件:

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

解决方法

在lcov工具中使用–gcov-tool选项选择需要的gcov版本,如lcov --gcov-tool /usr/bin/gcov-4.2


参考

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