go/java/C++覆盖率工具原理汇总学习记录

文章介绍了Go语言中的goc如何通过插桩源码来收集测试覆盖率,这种机制使得它是侵入式的。相比之下,Java的jacoco通过修改字节码进行代码覆盖率统计,可以在编译时或运行时插桩。gcov是GCC用于C/C++的代码覆盖率工具,它在编译和运行阶段分别进行插桩和数据收集。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

go–goc

goc采用的是插桩源码的形式,而不是待二进制执行时再去设置breakpoints。这就导致了当前go的测试覆盖率收集技术,一定是侵入式的,会修改目标程序源码。直接看案例

package main

import "fmt"

func main() {
  test2(3)
  fmt.Println("main")
  test2(-3)
}

func test1() {
  fmt.Println("hello")
  fmt.Println("test1")
}

func test2(a int) {
  if a > 0 {
    fmt.Println(a)
    test1()
  } else {
    fmt.Println("world")
  }
  fmt.Println("test2")
}

运行命令

 go tool cover -mode=count -var=CoverageVariableName learn/cover/exp1/main.go >> learn/cover/exp1/main_gen.go

生成的代码如下

//line learn/cover/exp2/main.go:1
package main

import "fmt"

  
func main() {CoverageVariableName.Count[0]++;
  test2(3)
  fmt.Println("main")
  test2(-3)
}

  
func test1() {CoverageVariableName.Count[1]++;
  fmt.Println("hello")
  fmt.Println("test1")
}

  
func test2(
    a int) {CoverageVariableName.Cont[2]++;
  if a > 0 {CoverageVariableName.Count[4]++;
    fmt.Println(a)
    test1 (
    CoverageVariableName.Count[5]++
    
    } else{ CoverageVariableName.Count[5]++;{
      
  fmt.Println("world")
  }}
  
  CoverageVariableName.Count[3]++;fmt.Println("test2")
}

var CorageVariableName = struct {
  Count     [6]uint32     //数组中每个元素代表相应基本块(basic block)被执行到的次数
  Pos     [3 * 6]uint32     // 代表的各个基本块在源码文件中的位置,三个为一组。比如这里的`21`代表该基本块的起始行数,`23`代表结束行数,`0x2000d`比较有趣,其前16位代表结束列数,后16位代表起始列数。通过行和列能唯一确定一个点,而通过起始点和结束点,就能精确表达某基本块在源码文件中的物理范围
  NumStmt   [6]uint16   // 代表相应基本块范围内有多少语句(statement)
} {
  Pos: [3 * 6]uint32{
    5, 9, 0x2000d, // [0]
    11, 14, 0x2000e, // [1]
    16, 17, 0xb0013, // [2]
    23, 23, 0x160002, // [3]
    17, 20, 0x3000b, // [4]
    20, 22, 0x30008, // [5]
  },
  NumStmt: [6]uint16{
    3, // 0
    2, // 1
    1, // 2
    1, // 3
    2, // 4
    1, // 5
  },
}

java-jacoco

jacoco是一个开源的代码覆盖率工具,针对java语言,其使用方法很灵活,可以嵌入到Ant、Maven中;可以作为Eclipse插件,可以使用其JavaAgent技术监控Java程序等等。

java 代码运行原理

java代码是运行在java虚拟机(JVM)上的,JVM相当于一个虚拟的计算机,符合约定的指令均可在上面执行。java编译后的class文件就是一种符合JVM的字节码指令集合,所以可以在JVM上执行。所以JVM其实指的不是运行java代码的虚拟机,它并不关心字节码是由哪种语言编译而来的,而是只要符合该虚拟机指令的文件均可在上面执行,如Kotlin、Groovy、JRuby、Jython、Scala等编译后也可以在JVM上执行。所以java代码在JVM里运行的时候,实际运行的是编译后的class文件字节码指令流,如果想要改变类的行为,分析类的信息等,只需要修改对应的字节码即可

jacoco原理

jacoco即是通过修改class文件的字节码来进行代码覆盖率统计的。即,在原有class字节码中的指定位置插入探针字节码,形成新的字节码指令流。jacoco使用的是ASM字节码框架对字节码进行修改的。jacoco的探针实际是一个布尔值,当代码执行到探针位置时,将其置为true,该探针前面的代码会被认为执行过,然后对该部分代码对应的html文件中的css样式进行染色(红色表示未覆盖,绿色表示已覆盖,黄色表示部分覆盖),形成最终的覆盖率报告

jacoco插庄模式(插入探针)

  • offline模式:编译时插桩,在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试插过桩 的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。

  • on-the-fly模式:运行时插桩,JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,启动jvm实例会调用程序里面的premain方法,通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。addTransformer 方法并没有指明要转换哪个类,转换发生在 premain 函数执行之后,main 函数执行之前(转换发生在JVM定义类之前),这时每装载一个类,transform 方法就会执行一次,看看是否需要转换。

插庄对比

在这里插入图片描述

C+±gcov

gcov是gcc内置的一个代码覆盖率工具,配合GCC共同实现对C/C++文件的语句覆盖、功能函数覆盖和分支覆盖测试,通常需要结合lcov生成可视化报告。
在这里插入图片描述

工作流

  1. 编译前,在编译器中加入编译器参数-fprofile-arcs -ftest-coverage;
  2. 源码经过编译预处理,然后编译成汇编文件,在生成汇编文件的同时完成插桩。插桩是在生成汇编文件的阶段完成的,因此插桩是汇编时候的插桩,每个桩点插入3~4条汇编语句,直接插入生成的*.s文件中,最后汇编文件汇编生成目标文件,生成可执行文件;并且生成关联BB和ARC的.gcno文件;
  3. 执行可执行文件,在运行过程中之前插入桩点负责收集程序的执行信息。所谓桩点,其实就是一个变量,内存中的一个格子,对应的代码执行一次,则其值增加一次;
  4. 生成.gcda文件,其中有BB和ARC的执行统计次数等,由此经过加工可得到覆盖率。

配置使用

gcc -fprofile-arcs -ftest-coverage -o test test.c

-ftest-coverage:在编译时产生.gcno文件,它包含了重建基本块图和相应的块的源码的行号信息*

-fprofile-arcs :在运行编译过的程序,会产生.gcda文件,包含基本块弧跳变的次数信息
Gcc在编译阶段指定 –ftest-coverage 等覆盖率测试选项后,GCC会:
1、 在输出目标文件中留出一段存储区保存统计数据;

2、 在源代码中每行可执行语句生成的代码之后附加一段更新覆盖率统计结果的代码,也就是插桩(后面详细介绍);

3、 Gcc编译,会生成*.gcno文件,它包含重建基本块图和相应块的源码的行号信息;

4、 在最终可执行文件中,进入main函数之前调用gcov_init内部函数初始化统计数据区,并将gcov_init内部函数注册为exit_handers,用户代码调用exit正常结束时,gcov_exit函数得到调用,并继续调用__gcov_flush输出统计数据到*.gcda文件

插庄

gcov是使用 基本块BB 和 跳转ARC 计数,结合程序流图来实现代码覆盖率统计的
基本块BB:如果一段程序的第一条语句被执行过一次,这段程序中的每一个都要执行一次,称为基本块。一个BB中的所有语句的执行次数一定是相同的。一般由多个顺序执行语句后边跟一个跳转语句组成。所以一般情况下BB的最后一条语句一定是一个跳转语句,跳转的目的地是另外一个BB的第一条语句,如果跳转时有条件的,就产生了分支,该BB就有两个BB作为目的地。

跳转ARC:从一个BB到另外一个BB的跳转叫做一个arc,要想知道程序中的每个语句和分支的执行次数,就必须知道每个BB和ARC的执行次数。

如果把BB作为一个节点,这样一个函数中的所有BB就构成了一个有向图。要想知道程序中的每个语句和分支的执行次数,就必须知道每个BB和ARC的执行次数。根据图论可以知道有向图中BB的入度和出度是相同的,所以只要知道了部分的BB或者ARC大小,就可以推断所有的大小。
在这里插入图片描述

由ARC的执行次数来推断BB的执行次数。所以对部分 ARC插桩,只要满足可以统计出来所有的BB和ARC的执行次数即可。

记录BB块和ARB的数据结构为:

struct bb

{

long zero_word; //是否被插入到链表中

const char *file_name; //当前被测试文件名

long *count;//指向bx2的指针

long ncounts;//桩点个数

struct bb *next;//下一个文件的BX2信息

};

1、GCC在插桩的过程中会向源文件的末尾插入一个静态数组,BX2.,数组的大小就是这个源文件中桩点的个数。BX2+0代表第0个桩点的位置,BX2+n代表第n个桩点的位置,数组的值就是桩点的执行次数。

2、每个桩点插入汇编语句:

*按照我的理解,汇编语句是inc$(BX2+n).

3、 BX2数组链表:

为了便于统计,gcc还将各个源文件中的BX2数组链接成一个链表,这个链表结构是在测试main函数之前就产生了,在调用main之前会有一个类似构造函数的函数,进行构建链表。这个函数会在退出时调用exit函数计算执行次数生成.gcda文件

<think>好的,用户问的是有没有办法在运行时获取覆盖率并处理数据。首先,我需要理解用户的需求。运行时获取覆盖率通常指的是在程序运行的过程中收集代码覆盖情况,比如哪些代码行被执行了,哪些分支被覆盖了。这可能用于动态分析、测试优化或者监控等场景。 接下来,我得考虑不同的方法。常见的覆盖率工具有gcov和lcov,但它们是针对C/C++的,而像JaCoCo适用于Java,Coverage.py用于Python。用户可能需要通用方法或者特定语言的方案,但问题里没明确说,所以得覆盖多种情况。 然后是动态插桩技术,比如使用ASM或Javassist在字节码层面插入统计代码。这需要解释如何动态修改代码,记录执行情况。可能还要提到Javajavaagent或Python的sys.settrace,这些是运行时动态获取覆盖率的常用方法。 接着是数据处理部分。用户提到要对数据做处理,可能需要聚合、可视化或者生成报告。比如用Prometheus和Grafana实时监控,或者生成HTML报告。这部分需要给出实际例子,比如用Python的Coverage.py生成报告,或者用JaCoCo生成XML/HTML。 还要考虑性能影响。动态插桩可能会影响程序运行速度,特别是高频交易系统或实时系统,需要权衡开销。此外,数据存储和传输也是问题,分布式系统可能需要将数据汇总到中央服务器。 用户可能的深层需求是实时监控或者持续集成中的覆盖率收集。比如在测试环境中实时查看覆盖率,或者在CI/CD流程中集成覆盖率检查。可能需要提到如何与CI工具集成,比如Jenkins或GitHub Actions。 最后,确保回答结构清晰,分步骤说明,并提供具体示例。例如,分步骤介绍使用工具、动态插桩、数据处理和可视化,以及注意事项。这样用户可以根据自己的情况选择合适的方法。</think>在软件开发中,运行时获取代码覆盖率并处理数据可以通过以下结构化方案实现: 一、常见实现方案 1. 编译期插桩方案 - 适用语言:C/C++/Rust等编译型语言 - 工具示例: $$Coverage = \frac{Executed\ Lines}{Total\ Executable\ Lines} \times 100\%$$ 使用gcov+lcov工具链,通过GCC的`--coverage`编译选项插入探针 执行流程: ```mermaid graph LR A[源码编译] --> B[插入探针] B --> C[运行程序] C --> D[生成.gcda文件] D --> E[lcov生成报告] ``` 2. 动态字节码插桩 - 适用语言:Java/Python等解释型语言 - Java示例: ```java // 使用JaCoCo Agent启动参数 -javaagent:jacocoagent.jar=includes=com.yourpackage.* ``` - Python示例: ```python # 使用coverage.py动态监控 import coverage cov = coverage.Coverage() cov.start() # 业务代码 cov.stop() cov.save() ``` 二、数据处理流程 1. 数据采集阶段 - 内存缓存:使用环形缓冲区存储覆盖率数据 - 定时持久化:设置阈值触发数据保存 公式表示: $$Flush\ Condition: t_{interval} \geq 30s\ \lor\ Buffer\ Size \geq 10MB$$ 2. 数据聚合分析 - 常用数据结构: ```python class CoverageData: def __init__(self): self.line_hits = defaultdict(int) self.branch_map = {} ``` - 统计维度: - 行覆盖率 - 分支覆盖率 - 方法覆盖率 三、可视化实现 1. Web Dashboard方案 - 技术栈:Prometheus + Grafana - 指标暴露示例: ```go // Go语言暴露metrics示例 var codeCoverage = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "runtime_coverage", Help: "Current code coverage percentage", }) ``` 2. 增量覆盖率检测 - 差异计算算法: $$ΔCoverage = \frac{Coverage_{current} - Coverage_{baseline}}{1 - Coverage_{baseline}} \times 100\%$$ 四、工程实践建议 1. 性能优化方向 - 采样率控制:设置动态采样策略 $$Sample\ Rate = \begin{cases} 100\% & CPU\ Usage < 50\% \\ 50\% & 50\% \leq CPU\ Usage < 80\% \\ 20\% & CPU\ Usage \geq 80\% \end{cases}$$ 2. 分布式系统处理 - 数据分片策略: $$Shard\ Key = hash(Class\ Name) \mod N_{nodes}$$ 五、典型应用场景 1. 线上流量回放 2. 混沌工程测试 3. 灰度发布验证 注意事项: 1.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值