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文件获取
-
- 如果没有自定义TEST_CONFIG_GCOV_REPORT_BASE,gcda文件都生成在 /data/gcda/xxx
- 利用 https://yuque.antfin-inc.com/yoznxz/nswtdy/ycsh4g 介绍的方式将gcda文件夹copy出来,放置到out/xxx/modules/。
-
-
- --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://blog.csdn.net/zhouzhaoxiong1227/article/details/50352944
- 测试报告示例
四、gcov工作原理
- 编译流程
- 工作原理
-
- gcov是使用 基本块BB和 跳转ARC计数,结合程序流图来实现代码覆盖率统计的
-
- 基本块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文件来统计目标模块的源码被执行的覆盖率
若有收获,就点个赞吧