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 格式导出到用户态,供 gcov、lcov、genhtml 等工具生成覆盖率报告。
与 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 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; struct gcov_info *next ; unsigned int stamp; #if (__GNUC__ >= 12) unsigned int checksum; #endif const char *filename; void (*merge[GCOV_COUNTERS])(...); 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 ; unsigned int ident; unsigned int lineno_checksum; unsigned int cfg_checksum; struct gcov_ctr_info ctrs []; }; struct gcov_ctr_info { unsigned int num; 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 ; struct list_head children ; struct list_head all ; struct gcov_node *parent ; struct gcov_info **loaded_info ; int num_loaded; struct gcov_info *unloaded_info ; struct dentry *dentry ; struct dentry **links ; 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 GCOV_PROFILE := n GCOV_PROFILE_futex.o := n
优先级:GCOV_PROFILE_<obj.o> > GCOV_PROFILE > CONFIG_GCOV_PROFILE_ALL
4.2 编译器行为 GCC/Clang 对插桩文件:
在每个基本块/弧(arc)插入计数器递增代码
生成 .gcno 文件(编译时静态 CFG 信息)
在 .data 段放置 gcov_info 结构及计数器数组
生成 constructor 调用 __gcov_init()(GCC)或 llvm_gcov_init()(Clang)
4.3 注意事项
插桩内核 体积显著增大、运行变慢 ,仅用于开发/CI
未链接进内核的插桩文件会导致 链接错误 ,需排除
模块 final link 会过滤 CFLAGS_GCOV(Makefile.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); gcov_info_link(info); if (gcov_events_enabled) gcov_event(GCOV_ADD, info); 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(); 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 ; while ((info = gcov_info_next(info))) gcov_event(GCOV_ADD, info); }
由 fs.c 的 gcov_fs_init() 在 debugfs 就绪后调用,解决 早期 constructor 早于 debugfs 初始化 的时序问题。
5.4 全局锁 1 2 3 DEFINE_MUTEX(gcov_lock); DEFINE_MUTEX(node_lock);
六、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); else add_node(info); 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 gcov_seq_open() → get_accumulated_info(node) → gcov_iter_new(info) → convert_to_gcda(buffer, info) → 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" }, { 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) { store_gcov_u32(GCOV_DATA_MAGIC); store_gcov_u32(info->version); store_gcov_u32(info->stamp); #if GCC >= 12 store_gcov_u32(0 ); #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); }
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.h 将 struct 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 ) { }void __gcov_merge_add(...) { }void __gcov_exit(void ) { }
内核不做 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 ; __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 make menuconfig make -j$(nproc ) mount -t debugfs none /sys/kernel/debug mkdir -p /tmp/gcov-datacp -r /sys/kernel/debug/gcov/* /tmp/gcov-data/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/resetecho 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_KERNEL(default 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 内核 代码覆盖率分析 的完整基础设施:
编译插桩 — -fprofile-arcs -ftest-coverage 在基本块插入计数器
双编译器后端 — GCC(constructor 注册)和 Clang(callback 注册)统一接口
debugfs 导出 — 标准 .gcda 格式,目录树镜像源码路径
模块感知 — 加载/卸载自动增删节点,支持持久化(gcov_persist)
快照读取 — dup + convert_to_gcda,不影响运行时计数
Reset 支持 — 单文件或全局清零计数器
符号链接 — 自动链接 .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
正在加载留言…