lcov
,gcovr
等前端图形工具才能实现统计数据图形化profiling tool
,例如gprof
)一起工作,可以估计程序中哪段代码最耗时使用象gcov或gprof这样的分析器,您可以找到一些基本的性能统计数据:
* 每一行代码执行的频率是多少
* 实际执行了哪些行代码,配合测试用例达到满意的覆盖率和预期工作
* 每段代码使用了多少计算时间,从而找到热点优化代码
* gcov创建一个sourcefile.gcov
的日志文件,此文件标识源文件sourcefile.c
每一行执行的次数,您可以与gprof
一起使用这些日志文件来帮助优化程序的性能。gprof
提供了您可以使用的时间信息以及从gcov获得的信息。
-fprofile-arcs -ftest-coverage
;要开启gcov功能,需要在源码编译参数中加入-fprofile-arcs -ftest-coverage
* -ftest-coverage
:在编译的时候产生.gcno文件,它包含了重建基本块图和相应的块的源码的行号的信息。
* -fprofile-arcs
:在运行编译过的程序的时候,会产生.gcda文件,它包含了弧跳变的次数等信息。
如下以helloworld_gcov.c
为例子,源码如下:
#include
#include
int main(int argc, char *argv[])
{
if (argc >=2) {
printf("=====argc>=2\n");
return;
}
printf("helloworld begin\n");
if (argc <2){
printf("=====argc<2\n");
return;
}
return;
}
helloworld_gcov.c
的Makefile的书写如下,在编译选项CFLAGS中加入-fprofile-arcs -ftest-coverage
选项:
#加入gcov编译选项,通过宏PRJRELEASE=gcov控制
ifeq ("$(PRJRELEASE)","gcov")
CFLAGS+= -fprofile-arcs -ftest-coverage
endif
CC=gcc
.PHONE: all
all: helloworld
helloworld: *.c
# 编译出汇编和gcno文件
@echo ${CFLAGS}
@${CC} ${CFLAGS} -S -o helloworld_gcov.s helloworld_gcov.c
@${CC} ${CFLAGS} -o helloworld_gcov helloworld_gcov.c
.PHONE: clean
clean:
@-rm helloworld_gcov helloworld_gcov.gcno helloworld_gcov.gcda helloworld_gcov.c.gcov helloworld_gcov.s
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./helloworld_gcov
产生helloworld_gcov.gcda
文件,其中包含了代码基本块和狐跳变次数统计信息gcov helloworld_gcov.c
产生的helloworld_gcov.c.gcov
中包含了代码覆盖率数据,其数据的来源为helloworld_gcov.gcda
为了对比运行./helloworld_gcov
前后的覆盖率数据文件helloworld_gcov.c.gcov
信息,直接执行如下脚本,产生前后数据对比
$ make #编译
$ gcov helloworld_gcov.c #生成原始的helloworld_gcov.c.gcov文件
$ cp helloworld_gcov.c.gcov helloworld_gcov.c.gcov-old #备份好原始的helloworld_gcov.c.gcov文件,方便后续对比
$ cp helloworld_gcov.gcno helloworld_gcov.gcno-old #备份好原始的helloworld_gcov.gcno文件,方便后续对比
$ ./helloworld_gcov #产生helloworld_gcov.gcda文件,记录的代码运行的统计数据
$ gcov helloworld_gcov.c #根据gcda文件,再次生成helloworld_gcov.c.gcov文件
#最后显示如下,可以对比先后的gcov文件,前后汇编文件.
yangfogen@ubuntu:~/work/helloworld_gcov$ ls
helloworld_gcov helloworld_gcov.c.gcov helloworld_gcov.gcda helloworld_gcov.gcno-old helloworld_gcov.s
helloworld_gcov.c helloworld_gcov.c.gcov-old helloworld_gcov.gcno helloworld_gcov-gcov.s Makefile
#####
表示未运行的行 Gcc中指定-ftest-coverage
等覆盖率测试选项后,gcc 会:
* 在输出目标文件中留出一段存储区保存统计数据
* 在源代码中每行可执行语句生成的代码之后附加一段更新覆盖率统计结果的代码,也就是前文说的插桩
* 在最终可执行文件中进入用户代码 main 函数之前调用 gcov_init
内部函数初始化统计数据区,并将gcov_exit
内部函数注册为 exit handlers
用户代码调用 exit 正常结束时,gcov_exit
函数得到调用,其继续调用 __gcov_flush
函数输出统计数据到 *.gcda 文件中
gcov_init
和gcov_exit
怎样放到编译的可执行文件中的只有把这几个问题搞明白了,才算真正搞懂gcov的原理.那么下面就来好好分析这几个问题
gcov是使用 基本块BB 和 跳转ARC 计数,结合程序流图来实现代码覆盖率统计的:
如果一段程序的第一条语句被执行过一次,这段程序中的每一个都要执行一次,称为基本块。一个BB中的所有语句的执行次数一定是相同的。一般由多个顺序执行语句后边跟一个跳转语句组成。所以一般情况下BB的最后一条语句一定是一个跳转语句,跳转的目的地是另外一个BB的第一条语句,如果跳转时有条件的,就产生了分支,该BB就有两个BB作为目的地。
从一个BB到另外一个BB的跳转叫做一个arc,要想知道程序中的每个语句和分支的执行次数,就必须知道每个BB和ARC的执行次数
如果把BB作为一个节点,这样一个函数中的所有BB就构成了一个有向图。,要想知道程序中的每个语句和分支的执行次数,就必须知道每个BB和ARC的执行次数。根据图论可以知道有向图中BB的入度和出度是相同的,所以只要知道了部分的BB或者arc大小,就可以推断所有的大小。
这里选择由arc的执行次数来推断BB的执行次数。
所以对部分 ARC插桩,只要满足可以统计出来所有的BB和ARC的执行次数即可。
当打开gcov编译选项是,在汇编阶段,插桩就已经完成,这里引用写的很好的一篇文章来说明:
https://github.com/yanxiangyfg/gcov
https://github.com/tejainece/gcov
为了解决这个问题,我们可以给待测程序增加一个 signal handler,拦截 SIGHUP、SIGINT、SIGQUIT、SIGTERM 等常见强制退出信号,并在 signal handler
中主动调用 exit 或 __gcov_flush
函数输出统计结果即可。
该方案仍然需要修改待测程序代码,不过借用动态库预加载技术和 gcc 扩展的 constructor 属性,我们可以将 signalhandler 和其注册过程都封装到一个独立的动态库中,并在预加载动态库时实现信号拦截注册。这样,就可以简单地通过如下命令行来实现异常退出时的统计结果输出了:
LD_PRELOAD=./libgcov_preload.so ./helloworld_server
#或者:
echo "/sbin/gcov_preload.so" >/etc/ld.so.preload
./helloworld_server
其中__attribute__ ((constructor))
是gcc的符号,它修饰的函数会在main函数执行之前调用,我们利用它把异常信号拦截到我们自己的函数中. 【注:具体代码请看文章后面的例子章节】
测试完毕后可直接 kill 掉 helloworld_server 进程,并获得正常的统计结果文件 *.gcda。
CONFIG_DEBUG_FS=y
CONFIG_GCOV_KERNEL=y
CONFIG_GCOV_PROFILE_ALL=y #获取内核数据覆盖率
CONFIG_GCOV_FORMAT_AUTODETECT=y #选择gcov的格式
mount -t debugfs none /sys/kernel/debug
内核相关文件介绍
#支持gcov的内核在debugfs中创建如下几个文件或文件夹
#所有gcov相关文件的父目录
/sys/kernel/debug/gcov
#全局重置文件:在写入时将所有覆盖率数据重置为零
/sys/kernel/debug/gcov/reset
#gcov工具理解的实际gcov数据文件。当写入文件时,将文件覆盖率数据重置为零
/sys/kernel/debug/gcov/path/to/compile/dir/file.gcda
#gcov工具所需的静态数据文件的符号链接。这个文件是gcc在编译时生成的, 选项:-ftest-coverage
/sys/kernel/debug/gcov/path/to/compile/dir/file.gcno
需要注意的是/sys/kernel/debug
文件夹是一个临时文件夹,不存在于磁盘当中,是在内存当中存在的,其中的文件也是系统运行是动态产生的
sudo apt install lcov
,用于使gcno和gcda文件生成info覆盖率统计文件. ~/.lcovrc
文件,并加入一行geninfo_auto_base = 1
,用于消除ERROR: could not read source file
错误lcov生成的.info文件包含一个或多个源文件所对应的覆盖率信息,一个源文件对应一条“记录”,“记录”中的详细格式如下
TN: 表示测试用例名称,即通过geninfo中的--test-name选项来命名的测试用例名称,默认为空;
SF: 表示带全路径的源代码文件名;
FN: <函数启始行号>, <函数名>; <函数有效行总数>; <函数有效行总数中被执行个数>
FNDA: <函数被执行的次数>, <函数名>; <函数有效行总数>; <函数有效行总数中被执行个数>
FNF: <函数总数>
FNH: <函数总数中被执行到的个数> BRDA: <分支所在行号>, <对应的代码块编号>, <分支编号>, <执行的次数> BRF: <分支总数> BRH: <分支总数中被执行到的个数> DA: <代码行号>, <当前行被执行到的次数> LF: < counts> 代码有效行总数 LH: 代码有效行总数中被执行到的个数 end_of_record 一条“记录”结束符
#include
#include
int main(int argc, char *argv[])
{
if (argc >=2) {
printf("=====argc>=2\n");
return;
}
printf("helloworld begin\n");
if (argc <2){
printf("=====argc<2\n");
return;
}
return;
}
简单编写的Makefile如下:
.PHONE: all
all: helloworld
CFLAGS+= -fprofile-arcs -ftest-coverage
CC=gcc
helloworld: *.c
@echo ${CFLAGS}
@${CC} ${CFLAGS} -o helloworld helloworld_gcov.c
.PHONE: clean
clean:
@-rm helloworld
单独产生同一个程序不同用例的info并合并
make
#运行两个参数用例并产生info文件和html文件
./helloworld i 2
lcov -c -d . -o helloworld2.info
genhtml -o 222 helloworld2.info
#运行无参数用例并产生info文件和html文件
rm helloworld_gcov.gcda
./helloworld
lcov -c -d . -o helloworld1.info
genhtml -o 111 helloworld1.info
#合并两个用例产生的info文件,输出同一个模块不同用例的总的统计数据
genhtml -o 333 helloworld1.info helloworld2.info
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
if (argc >=2) {
printf("=====argc>=2\n");
}
printf("helloworld begin\n");
if (argc <2){
printf("=====argc<2\n");
}
while(1){
printf("this is the server body");
sleep(5);
}
return 0;
}
helloworld_server_gcov.c
的Makefile:ifeq ("$(PRJRELEASE)","gcov")
CFLAGS+= -fprofile-arcs -ftest-coverage
endif
CC=gcc
.PHONE: all
all: helloworld
helloworld: *.c
@echo ${CFLAGS}
@${CC} ${CFLAGS} -o helloworld_server helloworld_server_gcov.c
.PHONE: clean
clean:
@-rm helloworld_server helloworld_server_gcov.gcno helloworld_server_gcov.gcda
gcov_preload.c
主要作用为捕获信号,调用gcov相关函数产生gcda文件。此文件编译成gcov_preload.so
#include
#include
#include
#include
#define SIMPLE_WAY
void sighandler(int signo)
{
#ifdef SIMPLE_WAY
exit(signo);
#else
extern void __gcov_flush();
__gcov_flush(); /* flush out gcov stats data */
raise(signo); /* raise the signal again to crash process */
#endif
}
/**
* 用来预加载的动态库gcov_preload.so的代码如下,其中__attribute__ ((constructor))是gcc的符号,
* 它修饰的函数会在main函数执行之前调用,我们利用它把异常信号拦截到我们自己的函数中,然后调用__gcov_flush()输出错误信息
* 设置预加载库 LD_PRELOAD=./gcov_preload.so
*/
__attribute__ ((constructor))
void ctor()
{
int sigs[] = {
SIGILL, SIGFPE, SIGABRT, SIGBUS,
SIGSEGV, SIGHUP, SIGINT, SIGQUIT,
SIGTERM
};
int i;
struct sigaction sa;
sa.sa_handler = sighandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESETHAND;
for(i = 0; i < sizeof(sigs)/sizeof(sigs[0]); ++i) {
if (sigaction(sigs[i], &sa, NULL) == -1) {
perror("Could not set signal handler");
}
}
}
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文件。
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需要自动确定基础目录来收集代码覆盖率数据.
使用lcov [srcfile]的命令生成.info文件的时候,提示如下错误, 无法生成info文件:
xxxxxxxxxxxx.gcno:version '402*', prefer '408*'
Segmentation fault
在lcov工具中使用–gcov-tool选项选择需要的gcov版本,如lcov --gcov-tool /usr/bin/gcov-4.2
2018 …… ,卷起裤管跑,撸起袖子干!
yanxiangyfg的专栏 : “忠于实践,记录点滴”