linux中代码覆盖率实现原理(一)
前言
我们都知道gcc有统计代码覆盖率的工具gcov,其使用方法基本就是打开–coverage编译选项,然后在程序编译时会生成含有代码Basic Block分析结果的gcno文件
程序运行后,会生成含有Basic Block的运行统计数据的gcda文件。然后使用lcov工具以及genhtml工具即可生成代码覆盖率的html报告了
其实clang编译器也支持与gcc的gcov兼容的代码覆盖率方案,其在原理上稍有不同,具体原理可以参考文章iOS 覆盖率检测原理与增量代码测试覆盖率工具实现
在使用gcov代码覆盖率时,一般都是统计一个可以在linux系统上运行的用户态的程序代码,那么linux的内核的代码覆盖率怎么统计呢?
之前有不少人尝试过使用clang编译linux内核代码,虽然我没试过,但我看到linux内核代码中是有与clang兼容gcc-gcov相关的代码的,所以今天就来介绍这个使用clang编译时linux的内核的代码覆盖率统计相关的代码的分析吧
在第一篇文章先看kernel/gcov/clang.c,其中有与gcda相关的基本的数据结构,以及定制clang源码中与gcda数据导出相关的函数、为了方便而实现的一些函数
数据结构
基本的数据结构主要是有gcov_info结构体,gcov_fn_info结构体,以及全局链表clang_gcov_list
可以简单的把gcov_info理解为是保存有一个源码文件的代码覆盖率信息的数据,gcov_fn_info是这个文件中一个函数的的覆盖率信息的数据
clang_gcov_list则是所有文件的覆盖率信息的链表
struct gcov_info {
struct list_head head;
const char *filename; //源码文件的文件名
unsigned int version;
u32 checksum;
struct list_head functions; //gcov_fn_info的链表
};
struct gcov_fn_info {
struct list_head head;
u32 ident;
u32 checksum;
u8 use_extra_checksum;
u32 cfg_checksum;
u32 num_counters; //计数器的个数
u64 *counters; //指向存有计数器值的地址
const char *function_name; //函数名
};
static LIST_HEAD(clang_gcov_list) //gcov_info的链表
函数调用
clang源码中实现gcov data数据生成时的流程与gcc稍有不同,在编译时编译器会插入__llvm_gcov_init、__llvm_gcov_writeout和__llvm_gcov_flush函数,
__llvm_gcov_init函数会调用llvm_gcov_init函数并将__llvm_gcov_writeout、__llvm_gcov_flush函数地址作为参数传入。而__llvm_gcov_flush会依次调用
llvm_gcda_start_file、llvm_gcda_emit_function、llvm_gcda_emit_arcs函数将数据导出到gcda文件。因此如果想要对gcda数据生成的目的地进行定制化的话,
需要实现的就是如下几个函数
llvm_gcda_start_file()
llvm_gcda_emit_function()
llvm_gcda_emit_arcs()
llvm_gcda_summary_info()
llvm_gcda_end_file()
clang.c中对于这几个函数的实现的目的就是把所有目标文件的gcov_info存在clang_gcov_list这个全局链表里
void llvm_gcov_init(llvm_gcov_callback writeout, llvm_gcov_callback flush)
{
struct gcov_info *info = kzalloc(sizeof(*info), GFP_KERNEL);
if (!info)
return;
INIT_LIST_HEAD(&info->head);
INIT_LIST_HEAD(&info->functions); //初始化gcov_info中的链表节点
mutex_lock(&gcov_lock);
list_add_tail(&info->head, &clang_gcov_list); //插入到全局链表
current_info = info;
writeout(); //覆盖率分发,将当前目标文件的覆盖率数据存入current_info
current_info = NULL;
if (gcov_events_enabled)
gcov_event(GCOV_ADD, info); //事件注册,详细看下篇文章
mutex_unlock(&gcov_lock);
}
llvm_gcda_emit_function 与 llvm_gcda_emit_arcs的作用,基本就是将数据存放到当前gcov_info中的最新的(即functions的最后一个节点)gcov_fn_info中
代码也比较简单,基本就是新建结构体更新数据,存放到链表中
void llvm_gcda_emit_function(u32 ident, const char *function_name,
u32 func_checksum, u8 use_extra_checksum, u32 cfg_checksum)
{
struct gcov_fn_info *info = kzalloc(sizeof(*info), GFP_KERNEL);
if (!info)
return;
INIT_LIST_HEAD(&info->head);
info->ident = ident;
info->checksum = func_checksum;
info->use_extra_checksum = use_extra_checksum;
info->cfg_checksum = cfg_checksum;
if (function_name)
info->function_name = kstrdup(function_name, GFP_KERNEL);
list_add_tail(&info->head, ¤t_info->functions); //将gcov_fn_info插入到当前gcov_info的functions链表里
}
void llvm_gcda_emit_arcs(u32 num_counters, u64 *counters)
{
struct gcov_fn_info *info = list_last_entry(¤t_info->functions,
struct gcov_fn_info, head);
info->num_counters = num_counters;
info->counters = counters;
}
convert_to_gcda函数就是将gcov_info中保存的数据按照gcda的格式存放到buffer里
从上可以看出,clang.c中根据clang编译器兼容gcov的原理重新实现了数据导出部分的函数,将数据存放在了clang_gcov_list这一全局gcov_info链表里
那么,怎么才能真正像用户态程序一样获得gcda文件呢? 请看下一章