前言

我们都知道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, &current_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(&current_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文件呢? 请看下一章