# 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")查看结果





覆盖率结果图中,`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 - 启用代码优化")

这种情况下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")