# gcov **Repository Path**: jswyll_com/gcov ## Basic Information - **Project Name**: gcov - **Description**: C/C++代码覆盖率 - **Primary Language**: C - **License**: LGPL-3.0 - **Default Branch**: master - **Homepage**: https://jswyll.com/note/lcov/ - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 3 - **Created**: 2022-05-31 - **Last Updated**: 2024-12-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: GCC, 单元测试, 代码覆盖率, lcov ## README # GCC代码覆盖率 > 代码覆盖率是指应用程序或组件的代码被测试的程度,对于C语言来说主要包括代码行、函数和分支的覆盖率。对于大多数项目70%-80%较为合理,一些严格的场景则要求100%。 > > > 本文原文链接: ## 引言 GCC(GNU Compiler Collection)是开源免费的C/C++编译工具链,内置代码覆盖率工具`gcov`,许多C/C++语言IDE(RT-Thread Studio、Eclipse C/C++、STM32 Studio)都使用GCC来编译代码。GCC可被改写以适配到不同的平台,它们的命令相同仅前缀不同,功能和用法几乎一致,例如编译源码工具`gcc`在嵌入式ARM平台是`arm-none-eabi-gcc`,在Windows是`x86_64-w64-mingw32-gcc`,在ESP8266和ESP32分别是`xtensa-lx106-elf-gcc`和`xtensa-esp32-elf-gcc`…… ## 本文demo 本例程源码地址: 假设有两个待测试C语言源代码如下: ``: ```c static int code1_inner_function(int value) { if (value < 7) { return value + 2; } else { return value; } } int code1_function(int a, int b) { if (a > 2) { a = 2; } else { a = code1_inner_function(a); } return a + b; } ``` ``: ```c int code2_function(int a, int b) { if (a > 2) { a = 2; } else { a -= 1; } return a - b; } ``` ### 所用软件 - Windows自带的CMD命令行或VS Code - 覆盖率渲染工具[lcov](http://ltp.sourceforge.net/coverage/lcov.php "Linux Test Project - Coverage"),下载后将lcov文件夹下的`bin/`文件夹添加到环境变量。 - [Git](https://git-scm.com/ "Git")(lcov工具只支持在Linux使用,使用Git可在Windows下运行lcov) ## Windows平台测试 ### 编译工具 Windows平台GCC编译工具[mingw-w64](https://sourceforge.net/projects/mingw-w64/ "MinGW-w64 - for 32 and 64 bit Windows"),安装后的将mingw64文件夹下的`bin/`文件夹添加到环境变量。 本demo的编译文件`Makefile`如下: ```makefile TOP_PATH = $(shell pwd) C_SRC_DIRS := src/ test/ C_INC_DIRS = inc/ OUTPUT_PATH := build/ CROSS_COMPILE = CC := $(CROSS_COMPILE)gcc CFLAGS := -std=gnu99 -Wall -Os C_INCS := $(foreach dir,$(C_INC_DIRS),-I'$(dir)') C_SRCS := $(foreach dir,$(C_SRC_DIRS),$(wildcard $(dir)*.c)) OBJS := $(patsubst %.c,$(OUTPUT_PATH)%.o,$(C_SRCS)) $(OUTPUT_PATH)src/%.o: src/%.c @echo compile test $<... @$(CC) $< $(CFLAGS) --coverage $(C_INCS) -c -o $@ $(OUTPUT_PATH)%.o: %.c @echo compile $<... @$(CC) $< $(CFLAGS) $(C_INCS) -c -o $@ all: $(OBJS) @echo linking... @$(CC) $(OBJS) -o $(OUTPUT_PATH)main --coverage .PHONY: clean check clean: del $(subst /,\,$(OBJS)) # 在编译目录创建源文件的文件夹 check: $(foreach dir,$(C_SRC_DIRS), mkdir $(subst /,\,$(OUTPUT_PATH)$(dir))) ``` 该编译脚本表示编译`src/`和`test/`文件夹下的.c文件并输出`build/main.exe`,`src/`文件夹下的文件是待测试覆盖率的。 ### 测试步骤 1. 使用`VS Code`终端或命令行进入本文demo Windows项目的主目录,输入`make && build/main.exe`执行编译和运行程序 2. 在主目录右键,选择`Git Bash Here`,输入`lcov -c -d build/ --rc lcov_branch_coverage=1 -o build/code_coverage.info && genhtml --branch-coverage build/code_coverage.info -o lcov/`并回车,然后打开`项目主目录/lcov/index.html`可查看代码覆盖率结果 #### 测试代码1 ```c #include #include "code1.h" #include "code2.h" int main() { printf("code1_function: %d\n", code1_function(2, 3)); return 0; } ``` 点击[这里](https://www.jswyll.com/markdown/embed/lcov/win/lcov_testcase1/ "测试结果1")查看结果 #### 测试代码2 ```c #include #include "code1.h" #include "code2.h" int main() { printf("code1_function: %d\n", code1_function(2, 3)); printf("code1_function: %d\n", code1_function(8, 3)); return 0; } ``` 点击[这里](https://www.jswyll.com/markdown/embed/lcov/win/lcov_testcase2/ "测试结果2")查看结果 #### 测试代码3 ```c #include #include "code1.h" #include "code2.h" int main() { printf("code1_function: %d\n", code1_function(2, 3)); printf("code1_function: %d\n", code1_function(8, 3)); printf("code2_function: %d\n", code2_function(2, 3)); printf("code2_function: %d\n", code2_function(8, 3)); return 0; } ``` 点击[这里](https://www.jswyll.com/markdown/embed/lcov/win/lcov_testcase3/ "测试结果3")查看结果 ![输出结果](figures/lcov_case3_0.png "输出结果") ![代码覆盖率 - 文件夹](figures/lcov_case3_1.png "代码覆盖率 - 文件夹") ![代码覆盖率 - 文件](figures/lcov_case3_2.png "代码覆盖率 - 文件") ![代码覆盖率 - code1.c](figures/lcov_case3_3.png "代码覆盖率 - code1.c") ![代码覆盖率 - code2.c](figures/lcov_case3_4.png "代码覆盖率 - code2.c") 覆盖率结果图中,`Line data`列表示该代码行被执行的次数,0表示未被执行,没有数字的为不可执行的代码行。 ### 结果分析 观察`code1.c`的覆盖率,可见`code1_inner_function`的else分支的执行次数总是0! 这是因为源码中`code1_function`的else分支已经限定了`a <= 2`(BUG)。使用某些静态检查工具可检测出该else分支为无效代码(dead code)。 如果在`Makefile`编译选项`CFLAGS`中设置了代码优化(-Os),`code1_inner_function`很可能会被优化为: ```c static int code1_inner_function(int value) { return value + 2; } ``` 点击[这里查看测试条件3下启用代码优化的结果](https://www.jswyll.com/markdown/embed/lcov/win/lcov_testcase3_Os/ "测试条件3 - 启用代码优化") ![启用代码优化后的代码覆盖率 - code1.c](figures/lcov_case3_3_Os.png "启用代码优化后的代码覆盖率 - code1.c") 这种情况下if的判断语句和else分支被视为无效(不可执行)代码行。 ### 关闭测试 注释或删除`Makefile`的如下代码覆盖率文件规则(去掉该规则后剩余的.c文件将遵循`$(OUTPUT_PATH)%.o: %.c`规则),然后执行`make clean`命令清除编译,再重新使用`make`重新编译。 ```makefile $(OUTPUT_PATH)src/%.o: src/%.c @echo compile test $<... @$(CC) $< $(CFLAGS) --coverage $(C_INCS) -c -o $@ ``` ## 代码覆盖率统计原理 不同编译工具版本的统计方式不一,在此以`code1.c`举例一种思路 1. 编译处理; 添加`--coverage`参数后,GCC编译源码时 - 统计可执行代码行数。分析源码中每一行是否包含可执行的代码(宏定义、声明、变量定义、空行、大括号`{`等不作统计),编译时生成`code1.gcno`(gcc coverage notes object)文件; - 对代码插桩。所谓插桩,是指在不改变代码逻辑和输出的前提下,添加语句以统计代码行、函数、分支被执行的次数。 ```c int __gcov_lines_count[FILE_CODE1_LINES_NUM]; int __gcov_functions_count[FILE_CODE1_FUNCTIONS_NUM]; static int code1_inner_function(int value) // 行1;函数1 { __gcov_functions_count[0]++; __gcov_lines_count[0]++; if (__gcov_lines_count[1]++, value < 7) // 行2 { __gcov_lines_count[2]++; return value + 2; // 行3 } else { __gcov_lines_count[3]++; return value; // 行4 } } int code1_function(int a, int b) // 行5;函数2 { __gcov_functions_count[1]++, __gcov_lines_count[4]++; if (__gcov_lines_count[5]++, a > 2) // 行6 { a = 2, __gcov_lines_count[6]++; // 行7 } else { a = code1_inner_function(a); // 行8 __gcov_lines_count[7]++; } __gcov_lines_count[8]++; return a + b; // 行9 } ``` 2. 运行程序,得到代码行和函数变量的计数值,生成`code1.gcda`(gcc coverage data)文件; 3. 使用`gcov`或`lcov`工具,工具读取`code1.gcda`文件并结合`code1.gcno`和`code1.c`统计并渲染出代码h行和函数的覆盖率。对于分支覆盖率,可以在此阶段根据行覆盖率和源代码语法分析出来。 ## 嵌入式平台测试 在Windows平台中,`.gcda`文件是程序运行时由gcov调用标准文件接口(stdlib)生成到电脑的,嵌入式系统可能没有文件接口,需要重写相关接口来获得`.gcda`文件并存到电脑以生成覆盖率 参考资料: - [Code Coverage for Embedded Target with Eclipse, gcc and gcov](https://mcuoneclipse.com/2014/12/26/code-coverage-for-embedded-target-with-eclipse-gcc-and-gcov/ "Code Coverage for Embedded Target with Eclipse, gcc and gcov") - [embedded-gcov](https://github.com/nasa-jpl/embedded-gcov "embedded-gcov") ### gcov工作流程 将`gcov_public.c`、`gcov_gcc.c`和对应的头文件添加到工程。`gcov_gcc.c`是gcov覆盖率相关的,不用修改。 重点关注`gcov_public`,它重写了gcov的部分函数。gcov的工作流程为: 1. 调用gcov初始化函数。编译器为添加了`--coverage`参数编译的每个源文件生成了初始化相关变量函数,并把该函数的地址定义为`.init_array.xxx`。 编译生成的map文件如下: ```plaintext PROVIDE (__init_array_start = .) *(SORT_BY_NAME(.init_array.*)) .init_array.00100 xxx1 0x4 xxx1.o .init_array.00100 xxx2 0x4 xxx2.o ... PROVIDE (__init_array_end = .) ``` 因此需要有一个函数来调用上面的初始化函数向量表: ```c #define GCOV_INIT_ARRAY_START __init_array_start #define GCOV_INIT_ARRAY_END __init_array_end void __gcov_call_constructors(void) { /* linker defined symbols, array of function pointers */ extern uint32_t GCOV_INIT_ARRAY_START, GCOV_INIT_ARRAY_END; uint32_t beg = (uint32_t)&GCOV_INIT_ARRAY_START; uint32_t end = (uint32_t)&GCOV_INIT_ARRAY_END; while (beg < end) { void (**p)(void); p = (void (**)(void))beg; /* get function pointer */ (*p)(); /* call constructor */ beg += sizeof(p); /* next pointer */ } } ``` 初始化函数内部调用了`__gcov_init`函数 ```c void __gcov_init(struct gcov_info *info) { ... } ``` 2. 调用每个被测试的文件的函数 3. 调用gcov汇总函数`__gcov_exit`,输出每个文件的覆盖率结果(.gcda) ```c void __gcov_exit(void) { GcovInfo *listptr = gcov_headGcov; ... while (listptr) { /* 获取文件路径(包含文件名) */ gcov_info_filename(listptr->info); /* 获取文件内容 */ gcov_convert_to_gcda(buffer, listptr->info); #ifdef GCOV_OPT_OUTPUT_SERIAL_HEXDUMP /* 输出方式之一 - 打印到串口 */ for (uint32_t i = 0; i < bytesNeeded; i++) { gcov_printf("%02X ", ((unsigned char *)buffer)[i]); } #endif /* GCOV_OPT_OUTPUT_SERIAL_HEXDUMP */ listptr = listptr->next; } } ``` ### 移植 1. 根据所使用的编译器的链接脚本`.lds`修改初始化函数向量表的起始和终止标志 例如RT-Thread Studio的链接脚本为 ```plaintext SECTIONS { .text : { ... PROVIDE(__ctors_start__ = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array)) PROVIDE(__ctors_end__ = .); } > ROM } ``` 则将对应的宏定义修改为: ```c #define GCOV_INIT_ARRAY_START __ctors_start__ #define GCOV_INIT_ARRAY_END __ctors_end__ ``` 2. 修改`gcov_public.h`打印函数定义。例如RT-Thread则修改为 ```c #ifndef __GCOV_PUBLIC_H__ #define __GCOV_PUBLIC_H__ #include #define gcov_printf rt_kprintf ... #endif ``` 3. 修改编译文件`Makefile`或相关配置文件,使编译器编译将被测试源码文件以覆盖率方式编译 例如RT-Thread Studio中,被测试.c文件在放在`src/`中,可在项目主目录新建一个文件`makefile.defs`: ```makefile src/%.o: ../src/%.c @echo compile test $<... arm-none-eabi-gcc --coverage -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O0 -ffunction-sections -fdata-sections -Wall -g -gdwarf-2 -DSOC_FAMILY_STM32 -DSOC_SERIES_STM32F4 -DUSE_HAL_DRIVER -DSTM32F407xx -I"E:\Code\test\gcov-stm32f407ve\drivers" -I"E:\Code\test\gcov-stm32f407ve\src" -I"E:\Code\test\gcov-stm32f407ve\drivers\include" -I"E:\Code\test\gcov-stm32f407ve\drivers\include\config" -I"E:\Code\test\gcov-stm32f407ve\libraries\CMSIS\Device\ST\STM32F4xx\Include" -I"E:\Code\test\gcov-stm32f407ve\libraries\CMSIS\Include" -I"E:\Code\test\gcov-stm32f407ve\libraries\CMSIS\RTOS\Template" -I"E:\Code\test\gcov-stm32f407ve\libraries\STM32F4xx_HAL_Driver\Inc" -I"E:\Code\test\gcov-stm32f407ve\libraries\STM32F4xx_HAL_Driver\Inc\Legacy" -I"E:\Code\test\gcov-stm32f407ve" -I"E:\Code\test\gcov-stm32f407ve\applications" -I"E:\Code\test\gcov-stm32f407ve" -I"E:\Code\test\gcov-stm32f407ve\cubemx\Inc" -I"E:\Code\test\gcov-stm32f407ve\cubemx" -I"E:\Code\test\gcov-stm32f407ve\rt-thread\components\drivers\include" -I"E:\Code\test\gcov-stm32f407ve\rt-thread\components\finsh" -I"E:\Code\test\gcov-stm32f407ve\rt-thread\components\libc\compilers\common" -I"E:\Code\test\gcov-stm32f407ve\rt-thread\include" -I"E:\Code\test\gcov-stm32f407ve\rt-thread\libcpu\arm\common" -I"E:\Code\test\gcov-stm32f407ve\rt-thread\libcpu\arm\cortex-m4" -include"E:\Code\test\gcov-stm32f407ve\rtconfig_preinc.h" -std=gnu11 -MMD -MP -MF"$(@:%.o=%.d)" -MT"$(@)" -c -o "$@" "$<" ``` 内容取自`Debug/src/subdir.mk`,然后增加`--coverage`参数,该规则会覆盖`Debug/src/subdir.mk`中原来的规则,该文件会被`Debug/Makefile`文件包含。 ### 使用 1. 将被测试文件`code1.c`和`code2.c`放在`src/`文件夹下(其它文件不放在这个文件夹下)。 2. 编写测试函数 ```c int test_all(void) { __gcov_call_constructors(); printf("code1_function: %d\n", code1_function(2, 3)); printf("code1_function: %d\n", code1_function(8, 3)); printf("code2_function: %d\n", code2_function(2, 3)); printf("code2_function: %d\n", code2_function(8, 3)); __gcov_exit(); return 0; } ``` 3. 调用测试函数 ```c int main(void) { test_all(); ... } ``` 4. 下载程序并运行,得到打印结果 5. 在打印结果写入电脑。例,编写一个python脚本 首先把`__gcov_exit`函数打印的输出格式改为 ```c void __gcov_exit(void) { ... #ifdef GCOV_OPT_OUTPUT_SERIAL_HEXDUMP /* 输出方式之一 - 打印到串口 */ for (uint32_t i = 0; i < bytesNeeded; i++) { gcov_printf("\\x02X", ((unsigned char *)buffer)[i]); } #endif /* GCOV_OPT_OUTPUT_SERIAL_HEXDUMP */ ... } ``` 则打印内容为 ```plaintext __gcov_init called for E:\test\gcov-stm32f407ve\Debug/src/code1.gcda __gcov_init called for E:\test\gcov-stm32f407ve\Debug/src/code2.gcda code1_function: 7 code1_function: 5 code2_function: -2 code2_function: -1 gcov_exit Emitting 56 bytes for E:\test\gcov-stm32f407ve\Debug/src/code2.gcda \x61\x64\x63\x67\x72\x39\x30\x34\x04\x64\xD4\x0A\x00\x00\x00\x01\x03\x00\x00\x00\x01\x00\x00\x00\x90\xF9\x7B\xB3\x16\x95\x21\xEB\x00\x00\xA1\x01\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00 Emitting 108 bytes for E:\test\gcov-stm32f407ve\Debug/src/code1.gcda \x61\x64\x63\x67\x72\x39\x30\x34\xFF\x63\xD4\x0A\x00\x00\x00\x01\x03\x00\x00\x00\x02\x00\x00\x00\xB9\x4D\xDC\x5C\x38\xA9\xFA\xBC\x00\x00\xA1\x01\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00\x01\x00\x00\x00\xA9\x9D\xA3\x93\x1C\xE2\x7C\xC2\x00\x00\xA1\x01\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 Gcov End ``` 分别将文件名和内容填入到如下脚本并运行即可生成文件 ```python with open(r'E:\test\gcov-stm32f407ve\Debug/src/code2.gcda', 'wb') as file: file.write(b'\x61\x64\x63\x67\x72\x39\x30\x34\x04\x64\xD4\x0A\x00\x00\x00\x01\x03\x00\x00\x00\x01\x00\x00\x00\x90\xF9\x7B\xB3\x16\x95\x21\xEB\x00\x00\xA1\x01\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00') ``` 6. 输出gcov 打开Windows资源管理器 - 项目主目录,右键`Git Bash Here`,输入`lcov -c -d . --rc lcov_branch_coverage=1 -o Debug/code_coverage.info --gcov-tool arm-none-eabi-gcov && genhtml --branch-coverage Debug/code_coverage.info -o Debug/lcov`,则lcov覆盖率输出在`项目主目录/Debug/lcov`,双击`index.html`可查看结果。点击[这里](https://www.jswyll.com/markdown/embed/lcov/embed/gcov-stm32f407ve/Debug/lcov/ "stm32f407ve gcov")查看结果 ## MDK代码覆盖率 MDK覆盖率可参考: - [MDK - Code Coverage](https://www2.keil.com/mdk5/debug/coverage "MDK - Code Coverage") - [Keil - µVision User's Guide: COVERAGE](https://www.keil.com/support/man/docs/uv4/uv4_cm_coverage.htm "Keil - µVision User's Guide: COVERAGE") - [Keil - µVision User's Guide: COVERAGE Overview Report](https://www.keil.com/support/man/docs/uv4/uv4_db_cov_report.htm "Keil - µVision User's Guide: COVERAGE Overview Report")