kernel/gcov 内核代码覆盖率(GCOV)机制与原理详解

kernel/gcov 内核代码覆盖率(GCOV)机制与原理详解

源码路径rk3588/kernel-6.1/kernel/gcov/
内核版本:Linux 6.1(RK3588 平台)
平台:RK3588(4×Cortex-A76 + 4×Cortex-A55 big.LITTLE,ARM64)

该目录实现 Linux 内核 GCOV 代码覆盖率分析 基础设施。通过编译时插桩(-fprofile-arcs -ftest-coverage),在运行时收集每个基本块/分支的执行次数,并通过 debugfs 以标准 .gcda 格式导出到用户态,供 gcovlcovgenhtml 等工具生成覆盖率报告。

与 KCOV 的区别kernel/gcov编译期插桩的代码行/分支覆盖率(开发/测试用);KCOV运行时 syscall/代码路径覆盖(syzkaller 模糊测试用),二者独立,不可混淆。


目录


一、源码目录结构

1.1 编译依赖(Makefile)

1
2
3
4
5
ccflags-y := -DSRCTREE='"$(srctree)"' -DOBJTREE='"$(objtree)"'

obj-y := base.o fs.o
obj-$(CONFIG_CC_IS_GCC) += gcc_base.o gcc_4_7.o
obj-$(CONFIG_CC_IS_CLANG) += clang.o
配置项 说明
CONFIG_GCOV_KERNEL 启用 gcov 子系统(默认 n,依赖 DEBUG_FS
CONFIG_GCOV_PROFILE_ALL 对整个内核启用插桩(依赖 ARCH_HAS_GCOV_PROFILE_ALL
CONFIG_CC_IS_GCC 使用 GCC 后端(gcc_base.o + gcc_4_7.o
CONFIG_CC_IS_CLANG 使用 Clang/LLVM 后端(clang.o
CONFIG_FAIL_FUTEX — (无关,gcov 无 fault injection)

编译标志(顶层 Makefile):

1
2
3
CFLAGS_GCOV := -fprofile-arcs -ftest-coverage
# GCC 额外禁用 loop invariant motion,避免计数器失真
CFLAGS_GCOV += -fno-tree-loop-im

1.2 源文件

文件 行数 功能
base.c ~138 公共基础:事件启用、数值序列化、模块卸载通知
fs.c ~882 debugfs 文件系统:节点树、gcda 读写、reset
gcc_base.c ~86 GCC 入口:__gcov_init 等编译器回调
gcc_4_7.c ~438 GCC ≥4.7 数据结构与 gcda 转换
clang.c ~393 Clang/LLVM 覆盖率回调与 gcda 转换
gcov.h ~85 内部 API 与 opaque gcov_info 接口
Kconfig 配置项说明

二、整体架构

2.1 数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
编译阶段                          运行阶段                         分析阶段
───────── ───────── ─────────
-fprofile-arcs 代码执行时 用户态工具
-ftest-coverage 编译器插桩计数器递增 │
│ │ │
▼ ▼ ▼
生成 .gcno(静态) __gcov_init / llvm_gcov_init 读取 debugfs
生成计数器段(.data) 注册 gcov_info │
│ │ ▼
│ gcov_event(GCOV_ADD) cp *.gcda
│ │ │
│ debugfs 节点树 lcov / genhtml
│ /sys/kernel/debug/gcov/ │
└────────────────────────────────┴──────────────────── 覆盖率 HTML 报告

2.2 模块职责

模块 职责
gcc_base.c / clang.c 接收编译器生成的 profiling 元数据,注册到内核链表
gcc_4_7.c / clang.c 实现 gcov_info_* 接口:dup/add/reset/convert_to_gcda
base.c 全局锁、事件重放、模块 notifier、store_gcov_u32/u64
fs.c debugfs 目录树维护、用户态读写 .gcda、reset 控制

2.3 debugfs 目录结构

1
2
3
4
5
6
7
8
9
/sys/kernel/debug/gcov/
├── reset # 写入任意值 → 清零所有计数器
└── <objtree相对路径>/ # 镜像内核源码/对象树路径
├── kernel/
│ └── sched/
│ └── core.gcda # 覆盖率数据(可读)
│ └── core.gcno → ... # 符号链接到 .gcno 静态文件
└── drivers/
└── ...

三、核心数据结构

3.1 struct gcov_info(GCC 版,gcc_4_7.c)

1
2
3
4
5
6
7
8
9
10
11
12
struct gcov_info {
unsigned int version; // GCC version magic
struct gcov_info *next; // 全局链表
unsigned int stamp; // 唯一时间戳
#if (__GNUC__ >= 12)
unsigned int checksum; // 编译单元 checksum
#endif
const char *filename; // 对应 .gcda 文件名
void (*merge[GCOV_COUNTERS])(...); // 各 counter 类型 merge 函数
unsigned int n_functions;
struct gcov_fn_info **functions;
};

3.2 struct gcov_fn_info — 每函数元数据

1
2
3
4
5
6
7
8
9
10
11
12
struct gcov_fn_info {
const struct gcov_info *key; // comdat key
unsigned int ident;
unsigned int lineno_checksum;
unsigned int cfg_checksum;
struct gcov_ctr_info ctrs[]; // 柔性数组:各类型 counter
};

struct gcov_ctr_info {
unsigned int num; // counter 数量
gcov_type *values; // 运行时递增的计数值
};

3.3 struct gcov_node(fs.c)— debugfs 节点

1
2
3
4
5
6
7
8
9
10
11
12
struct gcov_node {
struct list_head list; // 父节点 children 链表
struct list_head children;
struct list_head all; // 全局节点链表
struct gcov_node *parent;
struct gcov_info **loaded_info; // 已加载对象的 profiling 数据
int num_loaded;
struct gcov_info *unloaded_info; // 模块卸载后的持久化副本
struct dentry *dentry;
struct dentry **links; // .gcno 符号链接
char name[];
};

3.4 GCOV 文件格式常量(gcov.h)

常量 说明
GCOV_DATA_MAGIC 0x67636461 (“gcda”) .gcda 文件魔数
GCOV_TAG_FUNCTION 0x01000000 函数记录标签
GCOV_TAG_COUNTER_BASE 0x01a10000 计数器记录基标签

四、编译时插桩机制

4.1 启用方式

Kconfig

1
2
3
CONFIG_GCOV_KERNEL=y
CONFIG_GCOV_PROFILE_ALL=y # 可选:全内核插桩
CONFIG_DEBUG_FS=y # 必须

按文件/目录选择性插桩scripts/Makefile.lib):

1
2
3
4
5
6
7
8
9
# 单个文件
GCOV_PROFILE_sched/core.o := y

# 整个目录
GCOV_PROFILE := y

# 排除(即使 PROFILE_ALL 开启)
GCOV_PROFILE := n
GCOV_PROFILE_futex.o := n

优先级:GCOV_PROFILE_<obj.o> > GCOV_PROFILE > CONFIG_GCOV_PROFILE_ALL

4.2 编译器行为

GCC/Clang 对插桩文件:

  1. 在每个基本块/弧(arc)插入计数器递增代码
  2. 生成 .gcno 文件(编译时静态 CFG 信息)
  3. .data 段放置 gcov_info 结构及计数器数组
  4. 生成 constructor 调用 __gcov_init()(GCC)或 llvm_gcov_init()(Clang)

4.3 注意事项

  • 插桩内核 体积显著增大、运行变慢,仅用于开发/CI
  • 未链接进内核的插桩文件会导致 链接错误,需排除
  • 模块 final link 会过滤 CFLAGS_GCOVMakefile.modfinal
  • gcov 子系统自身 不应被插桩(否则递归/失真)

五、运行时注册流程

5.1 GCC 路径(gcc_base.c)

1
2
3
4
5
6
7
8
9
10
11
void __gcov_init(struct gcov_info *info)
{
mutex_lock(&gcov_lock);
if (gcov_version == 0)
gcov_version = gcov_info_version(info); // 记录 GCC magic

gcov_info_link(info); // 加入全局链表
if (gcov_events_enabled)
gcov_event(GCOV_ADD, info); // 创建 debugfs 节点
mutex_unlock(&gcov_lock);
}

由 GCC constructor 段在每个插桩 .o 加载时自动调用。

5.2 Clang 路径(clang.c)

1
2
3
4
5
6
7
8
9
10
11
12
void llvm_gcov_init(writeout, flush)
{
info = kzalloc(...);
list_add_tail(&info->head, &clang_gcov_list);
current_info = info;
writeout(); // 回调编译器 writeout,依次调用:
// llvm_gcda_start_file()
// llvm_gcda_emit_function() + llvm_gcda_emit_arcs()
// llvm_gcda_end_file()
current_info = NULL;
gcov_event(GCOV_ADD, info);
}

Clang 采用 回调驱动、无状态 设计,在模块 init 时一次性 dump 元数据。

5.3 事件启用与重放(base.c)

1
2
3
4
5
6
7
void gcov_enable_events(void)
{
gcov_events_enabled = 1;
// 重放已注册但未创建 debugfs 节点的 info
while ((info = gcov_info_next(info)))
gcov_event(GCOV_ADD, info);
}

fs.cgcov_fs_init() 在 debugfs 就绪后调用,解决 早期 constructor 早于 debugfs 初始化 的时序问题。

5.4 全局锁

1
2
3
DEFINE_MUTEX(gcov_lock);    // 保护 gcov_info 链表(注册/遍历)
// fs.c 中:
DEFINE_MUTEX(node_lock); // 保护 debugfs 节点树

六、debugfs 导出(fs.c)

6.1 gcov_event 回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void gcov_event(enum gcov_action action, struct gcov_info *info)
{
switch (action) {
case GCOV_ADD:
node = get_node_by_name(gcov_info_filename(info));
if (node)
add_info(node, info); // 已有节点,追加 data set
else
add_node(info); // 新建目录树 + .gcda 文件
break;
case GCOV_REMOVE:
remove_info(node, info); // 模块卸载
break;
}
}

6.2 节点树构建(add_node)

filename 路径逐级创建目录节点:

1
2
3
4
filename = "home/cp/.../kernel/sched/core.gcda"
→ gcov/kernel/sched/ (目录节点)
→ gcov/kernel/sched/core.gcda (数据文件)
→ 符号链接: core.gcno → $(objtree)/kernel/sched/core.gcno

6.3 读取覆盖率数据

1
2
3
4
5
6
// open /sys/kernel/debug/gcov/.../core.gcda
gcov_seq_open()
→ get_accumulated_info(node) // dup + 合并多个 loaded_info
→ gcov_iter_new(info)
→ convert_to_gcda(buffer, info) // 转为标准 .gcda 二进制
→ seq_read() 分页输出

读操作返回 快照副本,不影响运行时计数器。

6.4 写入 / Reset

操作 路径 效果
单文件 reset .gcda 文件 清零该文件所有 counter
全局 reset gcov/reset 清零所有 counter,移除纯 unloaded 节点

6.5 符号链接

1
2
3
4
const struct gcov_link gcov_link[] = {
{ OBJ_TREE, "gcno" }, // 链接到 $(objtree) 下的 .gcno
{ 0, NULL },
};

使用户态 lcov 工具能在同一目录找到 .gcno + .gcda 配对文件。


七、GCDA 格式转换

7.1 convert_to_gcda(GCC 版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
size_t convert_to_gcda(char *buffer, struct gcov_info *info)
{
// File header
store_gcov_u32(GCOV_DATA_MAGIC); // "gcda"
store_gcov_u32(info->version);
store_gcov_u32(info->stamp);
#if GCC >= 12
store_gcov_u32(0); // checksum placeholder
#endif

for each function:
store_gcov_u32(GCOV_TAG_FUNCTION);
store_gcov_u32(length);
store_gcov_u32(ident, lineno_checksum, cfg_checksum);

for each active counter type:
store_gcov_u32(GCOV_TAG_FOR_COUNTER(type));
store_gcov_u32(num * 2);
for each counter value:
store_gcov_u64(value); // 64-bit 分两个 u32 存储
}

7.2 数值编码(base.c)

  • 所有数值以 本机字节序 32 位 unsigned 存储
  • 64 位数值:低 32 位在前,高 32 位在后
  • store_gcov_u32/u64(buffer, off, val)buffer==NULL 时仅计算大小(dry-run)

7.3 数据操作 API

函数 功能
gcov_info_reset() 所有 counter 清零
gcov_info_add(dst, src) 累加两份 profiling 数据
gcov_info_dup() 深拷贝(供 debugfs 读取快照)
gcov_info_is_compatible() 检查 stamp/checksum 是否可合并
gcov_info_free() 释放 dup 副本

八、GCC 与 Clang 双后端

8.1 设计模式

gcov.hstruct gcov_info 声明为 opaque 类型,GCC 和 Clang 各自维护不兼容的内部结构,通过统一接口对外:

1
2
3
4
const char *gcov_info_filename(struct gcov_info *info);
struct gcov_info *gcov_info_next(struct gcov_info *info);
size_t convert_to_gcda(char *buffer, struct gcov_info *info);
// ...

8.2 GCC 后端特点

方面 说明
注册时机 每个 .o 的 constructor 调用 __gcov_init
数据结构 静态分配在 .data,与 gcc/gcov-io.h 兼容
版本适配 GCOV_COUNTERS 随 GCC 版本变化(8~10 个 counter 类型)
GCC 12+ 新增 checksum 字段,GCOV_UNIT_SIZE 改为 4 字节

8.3 Clang 后端特点

方面 说明
注册时机 每模块一次 llvm_gcov_init,回调 writeout
数据结构 动态分配,list_head 链表
兼容性检查 基于 checksum + 各函数 cfg_checksum
参考 LLVM GCOVProfiling.cpp

8.4 未使用的 GCC 回调

1
2
3
void __gcov_flush(void)           { /* Unused */ }
void __gcov_merge_add(...) { /* Unused */ }
void __gcov_exit(void) { /* Unused */ }

内核不做 profiling 数据合并(无 fork/exit 场景),这些回调为空实现但需 EXPORT 以满足链接。


九、模块与持久化

9.1 模块加载/卸载

1
2
3
4
5
6
7
8
9
模块 insmod(插桩编译)
→ 各 .o constructor → __gcov_init
→ gcov_event(GCOV_ADD) → debugfs 节点

模块 rmmod
→ base.c: gcov_module_notifier(MODULE_STATE_GOING)
→ 遍历 gcov_info 链表,移除属于该 module 的 info
→ gcov_event(GCOV_REMOVE)
→ remove_info() → 可选 save_info() 持久化

9.2 gcov_persist

1
2
3
4
static int gcov_persist = 1;   // 默认开启

// 内核参数:gcov_persist=0 关闭
__setup("gcov_persist=", gcov_persist_setup);
  • gcov_persist=1:模块卸载时将 counter 数据 dup 保存node->unloaded_info
  • 重新加载兼容版本模块时,新数据 替换 旧副本
  • 不兼容版本:丢弃旧数据并警告

9.3 多版本冲突

同一 .gcda 节点可能关联多个 loaded_info(如不同版本模块同时加载):

  • 读取时 get_accumulated_info() 合并所有 loaded + unloaded 数据
  • stamp/checksum 兼容 的数据才允许合并

十、用户态使用方法

10.1 典型工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 配置并编译插桩内核
make menuconfig # CONFIG_GCOV_KERNEL=y, CONFIG_GCOV_PROFILE_ALL=y
make -j$(nproc)

# 2. 启动内核,挂载 debugfs
mount -t debugfs none /sys/kernel/debug

# 3. 运行测试用例(触发内核代码路径)

# 4. 收集覆盖率数据
mkdir -p /tmp/gcov-data
cp -r /sys/kernel/debug/gcov/* /tmp/gcov-data/

# 5. 生成报告(需要编译时生成的 .gcno 文件)
lcov --capture --directory /tmp/gcov-data --output-file coverage.info
genhtml coverage.info --output-directory /tmp/coverage-html

10.2 Reset 计数器

1
2
3
4
5
# 清零所有覆盖率计数
echo 1 > /sys/kernel/debug/gcov/reset

# 清零单个文件
echo 1 > /sys/kernel/debug/gcov/path/to/file.gcda

10.3 文件配对

文件 来源 内容
.gcno 编译时生成($(objtree) 静态 CFG、行号映射
.gcda debugfs 运行时导出 动态执行计数

两者需 版本/stamp 匹配,否则 lcov 报错。


十一、RK3588/ARM64 平台说明

11.1 架构支持

1
2
# arch/arm64/Kconfig
select ARCH_HAS_GCOV_PROFILE_ALL

RK3588 支持 CONFIG_GCOV_PROFILE_ALL 全内核插桩。

11.2 编译器选择

工具链 后端文件 说明
GCC(常见 RK3588 内核构建) gcc_base.o + gcc_4_7.o 通过 __gcov_init 注册
Clang/LLVM clang.o 通过 llvm_gcov_init 注册

Rockchip SDK 通常使用 GCC 交叉编译器(如 aarch64-linux-gnu-gcc),走 GCC 后端。

11.3 生产内核注意

  • RK3588 产品内核 默认不启用 CONFIG_GCOV_KERNELdefault n
  • 启用后内核镜像和模块体积增大,启动和运行性能下降
  • 仅建议在 CI 覆盖率测试驱动开发调试 构建中使用
  • CONFIG_KCOV(模糊测试)独立,可同时或分别启用

11.4 big.LITTLE 无关性

GCOV 计数器是 per-compilation-unit 内存变量,与运行在 A76 还是 A55 核心无关,无需额外跨核同步。

11.5 常见排除项

以下文件/目录通常需排除插桩:

类型 原因
kernel/gcov/ 自身 避免自引用
引导/early init 代码 链接或时序问题
汇编文件(.S 无法插桩
未链入 vmlinux 的 lib 文件 链接错误

十二、总结

kernel/gcov 是 Linux 内核 代码覆盖率分析 的完整基础设施:

  1. 编译插桩-fprofile-arcs -ftest-coverage 在基本块插入计数器
  2. 双编译器后端 — GCC(constructor 注册)和 Clang(callback 注册)统一接口
  3. debugfs 导出 — 标准 .gcda 格式,目录树镜像源码路径
  4. 模块感知 — 加载/卸载自动增删节点,支持持久化(gcov_persist
  5. 快照读取 — dup + convert_to_gcda,不影响运行时计数
  6. Reset 支持 — 单文件或全局清零计数器
  7. 符号链接 — 自动链接 .gcno 便于 lcov 配对

RK3588 平台完整支持 GCOV 全内核插桩,典型用途是 内核/driver CI 覆盖率测试开发阶段代码路径验证。生产环境应关闭此选项。


附录:源文件清单

文件 行数 分类
base.c ~138 公共基础与模块 notifier
fs.c ~882 debugfs 文件系统
gcc_base.c ~86 GCC 编译器回调
gcc_4_7.c ~438 GCC 数据结构/gcda 转换
clang.c ~393 Clang/LLVM 后端
gcov.h ~85 内部 API

相关构建配置

文件 功能
kernel/gcov/Kconfig GCOV_KERNEL / GCOV_PROFILE_ALL
scripts/Makefile.lib GCOV_PROFILE per-file 控制
Makefile CFLAGS_GCOV 定义
arch/arm64/Kconfig ARCH_HAS_GCOV_PROFILE_ALL

与 KCOV 对比

特性 GCOV(本目录) KCOV
机制 编译期 -fprofile-arcs 运行时 -fsanitize-coverage
粒度 行/分支/弧 PC/比较操作
导出 debugfs .gcda /sys/kernel/debug/kcov
用途 代码覆盖率报告 模糊测试(syzkaller)
配置 CONFIG_GCOV_KERNEL CONFIG_KCOV

文章互动

阅读 --

留言

0 条留言

正在加载留言…