gnu 工具链中的代码覆盖率工具--gcov

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获得的信息。

 

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

 

二、工作流程

 

2.1 编译阶段

  • 对需要监控覆盖率的一个或文件增加编译选项 -fprofile-arcs -ftest-coverage
    • 指定要做覆盖率检测的模块
在测试用例的aos.mk 中增加

ifeq ($(TEST_CONFIG_GCOV_REPORT), y)
    GLOBAL_GCOV_COMPONENTS += cunit
endif

新增了 关键字:GLOBAL_GCOV_COMPONENTS, 指定的组件在被编译时会增加 CFLAGS -fprofile-arcs -ftest-coverage

公共部分的menuconfig 定义:
<<<<<autotest/Config.in>>>>>>

新增 TEST_CONFIG_GCOV_REPORT & TEST_CONFIG_GCOV_REPORT_BASE
TEST_CONFIG_GCOV_REPORT:测试是否生成覆盖率信息
TEST_CONFIG_GCOV_REPORT_BASE: 覆盖路信息文件的基地址,对应 env 的 GCOV_PREFIX
gcov 信息的默认基地址是 /data/gcda
    • 公共部分增加的功能(非用户定义)
<<<<<testcase/aos.mk >>>>>>

ifeq ($(TEST_CONFIG_GCOV_REPORT), y)
    GLOBAL_LDFLAGS      += -fprofile-arcs
    GLOBAL_DEFINES      += GCDA_ROOT_PATH=\"$(OUTPUT_ABS_DIR)/modules\"
endif

新增了

在链接时提供 libgcov 支持。
GLOBAL_LDFLAGS      += -fprofile-arcs 

全局宏定义,定义了gcda目标文件生成的原始base地址,改地址在编译时已经决定,指向了 xxx/out/xxx/modules
(由makefile根据实际编译环境指定,做到环境自适应。).
该宏用来计算 evn GCOV_PREFIX_STRIP 的层数。剥离掉原始的gcdas文件生成地址的基地址。
GLOBAL_DEFINES      += GCDA_ROOT_PATH=\"$(OUTPUT_ABS_DIR)/modules\"
被指定的组件的c源文件会被一同copy到out对应文件下,与 文件的 o & gcno在同一个目录下,如此设计是为了gcovr
可以正确的找到c并生成 覆盖率分析报告


 

  • gcno&gcda文件的路径的生成原理
    • gcda&gcno的生成位置与对应c文件的编译位置是一致的,在源文件编译时就已经确定了。
    • 主线分之上的位置
//源文件位置
/home/fuzhi/micro_core/components/autotest/testcase/uspace/demo_test

//gcno 文件位置
/home/fuzhi/micro_core/out/autotest_user@vexpressa9-mkapp/modules/components/autotest/testcase/uspace/demo_test/demo_test.gcno

//gcda 文件生成位置
/home/fuzhi/micro_core/out/autotest_user@vexpressa9-mkapp/modules/components/autotest/testcase/uspace/demo_test/demo_test.gcda
注意:
gcda文件生成位置可以从生成的app文件中查到,用编译器直接打开autotest_user@vexpressa9-mk.app.elf,在文件里搜索字符gcda就可以找到上面的gcda文件路径
可以发现,gcda文件的生成位置是与gcno文件的生成位置相同的。两者都是在gcc编译产生 对应的*.o 时就已经决定了。
即:gcc 指定的*.o的生成路径就是 gcda&gcno的目标路径。


  • 重定义gcda的生成路径
    • gcov 通过环境变量 "GCOV_PREFIX" & "GCOV_PREFIX_STRIP"来重新指定gcno文件的生成位置。
    • 在主线分之上的定义方式
// 修改 GCOV_PREFIX ,定义gcno 文件的prefix 路径信息
putenv("GCOV_PREFIX=/data/");

//修改GCOV_PREFIX_STRIP, 决定剥离原始gcno文件生成路径的prefix路径信息

以/home/fuzhi/micro_core/out/autotest_user@vexpressa9-mkapp/modules/components/autotest/testcase/uspace/demo_test/demo_test.gcda
为例:gcda文件前的文件夹层级有11层,从home -> demo_test
如果我们想把dir信息全部剥离,只要设置GCOV_PREFIX_STRIP为11即可。
putenv("GCOV_PREFIX_STRIP=11");


配合GCOV_PREFIX定义新的路径,就可以完成新路径的定义。
注意:我门要检测的目标文件可能有很多个并且不在同一个路径下。我么一般不会剥离掉 prefix路径的所有内容,
一般会剥离一个基准的base地址,以上面的路径为例,我们会剥离以/home/fuzhi/micro_core/out/autotest_user@vexpressa9-mkapp/
如此依赖,我们在data下会获取到 一个 modules目录

 

2.2 运行阶段

 

  • 运行app,运行的统计信息信息会被保存在对应的gcda文件中。
    • 主线分之流程分析
qemu调试环境下

1. 运行 alios kernel


2. load /system/autotest_user@vexpressa9-mk.app.elf

注意:
a. gcov 功能的初始化函数 __gcov_init 不是被显示调用的,他是由
_GLOBAL__sub_I_00100_0_test_demo_test -> ____gcov_init_from_arm -> __gcov_init 间接引用的。
函数_GLOBAL__sub_I_00100_0_test_demo_test 的地址被注册到了段 .ctors(其中的函数会在进程启动时,main调用前被libc的初始化函数依次调用),
所以gcov的初始化依赖 进程入口出得libc中的实现,而主线分之中uspace app的入口是我们定义好的,没有调用libc提供的入口。
所以 gcov 后面运行会失败
临时解决方案:
static void append_start_init(void)
{
    //_init();

    unsigned int a = (unsigned int)&__ctors_start__;
    for (; a<(unsigned int)&__ctors_end__; a+=sizeof(void(*)()))
        (*(void (**)(void))a)();
}
在application_start中增加了上述函数调用,完成 ctors 初始化。

b. gcov 把统计信息写入到 gcda文件是在 gcov 函数 __gcov_exit被调用时,该函数本来也不是显示调用的
是在进程退出时被自动调用的。(肯能是在 .fini_array中,也可能是在进程退出时的注册函数中)
在我们的uapp退出过程中没有调用过__gcov_exit,所以我们要认为加入这个函数的调用


3. 获取gcda文件

  • gcda文件获取
      • --todo此处无法为每个用户提供自动的操作,因为该操作需要获取sudo权限去操作linux的loop设备。这个操作过程会收到权限,loop设备资源抢占等因素的制约。
    • 检测gcda文件和gcno文件是否匹配
一般情况下无需去check匹配,用户要保证 gcno 和 gcda 是同一次编译产生的(必要条件,否则gcovr 会报错)

hexdump -e '"%x\n"' -s8 -n4 *.gcno
hexdump -e '"%x\n"' -s8 -n4 *.gcda
两次运行得到的16机制数字串要完全相等

参考资料:https://blog.csdn.net/weixin_41910194/article/details/80759473

 

 

 

2.3 分析测试结果

  • gcovr (推荐,因为jekins可以展示,可以与cicd流程集成)
    • 产生的xml 结果可以被Cobertura读取解析,这个Cobertura是Jenkins的一个现有java统计覆盖率的插件
    • jekins支持显示gcovr产出的测试结果
    • gcovr 的安装
参考资料 :https://gcovr.com/en/stable/installation.html gcovr 的官方文档

pip install gcovr
安装后 会生成可执行文件gcovr ,可以被shell直接调用

 

    • gcovr 的基本命令
gcovr 是一个 分析工具,但是它并不是直接分析gcda&gcna, 他是通过调用gcc toolchain 的 gcov 工具先处理gcda文件,
然后再对处理后生成的gcov文件统计分析。

gcov 的分析过程需要用到c源文件,而其搜索c文件的路径是在gcno文件中定义好的,gcno文件中有关c文件的路径的定义又是
在gcc 编译时决定的,所以要求保证各个c文件的相对路径,gcov 才能正确的分析(此处解释了为什么被添加了覆盖率统计需求
的组件的c文件要被放置到 out 目录下和其对应的o&gcno放在一起的原因)
todo: 一些c文件依赖了定义在h中的宏函数,如此操作在生成report时需要该h文件的参与(用inline可以解决这个问题)
       h文件的收集还么有完成,需要按需手动添加。

gcno &gcda 文件的相对位置不重要,gcovr会去逐级扫描 gcno & gcda


一个典型的应用命令
gcovr --gcov-executable arm-none-eabi-gcov -r . --html --html-details -o result-detials.html

-r 指定目录要包含的根目录,我们会指定 xxx/out/xxx/modules/ 为根目录。 此处可以是相对地址,也可是绝对地址
--gcov-executable arm-none-eabi-gcov 制定了 gcovr 使用的 gcov工具,一定要选择toolchain的gcc工具。

--html 生成 html格式的report。output 到 result-detials.html
--html-details 指定为各个 c 生成html 格式的详细报,此时会生成多个 html,其中包含c的文件名

-x 生成xml格式report,次xml可以被jekins的组件Cobertura去解析和显示。在集成到cicd环境时要使用。




 

三、结果展示

  • 示例工程
    • 该工程的文件均来自 主线分之,都是由qemu环境运行生成的
    • 📎gcda.zip
  • 参考资料

https://gcovr.com/en/stable/

 

https://blog.csdn.net/lee_lixiang2/article/details/84965064?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-10.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-10.nonecase

 

https://blog.csdn.net/zhouzhaoxiong1227/article/details/50352944

 

 

  • 测试报告示例

屏幕快照 2020-07-03 下午3.28.32.png

屏幕快照 2020-07-03 下午3.28.43.png

 

 

四、gcov工作原理

 

  • 编译流程

 

屏幕快照 2020-07-03 下午3.47.52.png

 

  • 工作原理
    • gcov是使用 基本块BB和 跳转ARC计数,结合程序流图来实现代码覆盖率统计的
    • 屏幕快照 2020-07-03 下午3.50.50.png

 

    • 基本块BB:
      • 如果一段程序的第一条语句被执行过一次,这段程序中的每一个都要执行一次,称为基本块。一个BB中的所有语句的执行次数一定是相同的。一般由多个顺序执行语句后边跟一个跳转语句组成。所以一般情况下BB的最后一条语句一定是一个跳转语句,跳转的目的地是另外一个BB的第一条语句,如果跳转时有条件的,就产生了分支,该BB就有两个BB作为目的地。
    • 跳转ARC:
      • 从一个BB到另外一个BB的跳转叫做一个arc,要想知道程序中的每个语句和分支的执行次数,就必须知道每个BB和ARC的执行次数。
    • 总结:
      • 根据ARC的分割来统计不同的基本块BB
      • 每次进入在BB块的首部,插入计数桩函数。

 

 

五、总结

 

  • 编译阶段
    • 在源文件的编译阶段使用专有的编译选项,为在生成的 o文件中插入“计数桩”函数。并同时生成gcno资源文件。
    • 目标app执行后才会产生gcda统计数据文件。这个文件与gcno 文件一同被解释才能得到最终的代码执行覆盖率统计结果
      • 注意:gcda文件的生成路径在编译时就定义好了,取决于 *.c  -o path/*.o  中的path。用户可以通过修改环境变量(GCOV_PREFIX&GCOV_PREFIX_STRIP)来重新定义gcda的输出目录。
      • gcda和gcno 一定要使用在统一编译&运行过程中产生的一对文件,否则会有时间戳不匹配错误,无法得到覆盖率报告。
  • 运行阶段
    • gcov 模块的入口和出口函数都是隐式调用的,需要系统环境支持。否则就可可能出现未被调用的问题,最终无法生成 gcda结果
  • 解释阶段
    • 推荐使用gcovr py 功能模块来解析gcda&gcno
      • gcovr 可以自扫描并批量解析gcda文件,产生一个总的测试结果
      • 产出的xml 测试结果可以被jekins的Cobertura功能组件解析,使整个过程可以接入cicd流程中
  • 使用原则
    • 为目标模块的源码文件添加编译gcov 编译选项,批量生成gcno文件
    • 执行测试用例,通过产出的gcda文件来统计目标模块的源码被执行的覆盖率

 

 

 

 

 

 

若有收获,就点个赞吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值