LLVM Clang 的 Source-based Code Coverage

Source-based Code Coverage

1. Introduction

本文档解释了如何使用clang的 source-based code coverage 特性。它被称为 “source-based” ,因为它直接操作AST(抽象语法树)和预处理器信息。这使得它能够生成非常精确的覆盖率数据。

Clang提供了另外两个代码覆盖实现:

SanitizerCoverage:一种低开销的工具,可以与各种Sanitizer一起使用。它可以提供高达边缘级的覆盖。

gcov:一个兼容gcc的覆盖实现,它在DebugInfo上运行。这可以通过 -ftest-coverage--coverage来启用。

从这里开始,code coverage将指 source-based code coverage

2.The code coverage workflow

code coverage 工作流包括三个主要步骤:

  • 在启用覆盖的情况下编译
  • 运行已插桩程序
  • 构建覆盖率报告

接下来的几节将给出一个基于下列程序的示例:

% cat <<EOF > foo.cc
#define BAR(x) ((x) || (x))
template <typename T> void foo(T x) {
  for (unsigned I = 0; I < 10; ++I) { BAR(I); }
}
int main() {
  foo<int>(0);
  foo<float>(0);
  return 0;
}
EOF

3. Compiling with coverage enabled

要编译启用覆盖的代码,向编译器传递参数 -fprofile-instr-generate-fcoverage-mapping

# Compile with coverage enabled.
$ clang++ -fprofile-instr-generate -fcoverage-mapping foo.cc -o foo

注意,带有覆盖插装的代码和不带有覆盖插装的代码可以链接在一起。未插桩的代码不会在报告中说明。

4.Running the instrumented program

下一步是运行插桩的程序。当程序退出时,它将把一个 raw profile 写入由LLVM_PROFILE_FILE环境变量指定的路径。如果该变量不存在,则将 profile 写入程序的当前目录下的 default.profrow 文件。如果LLVM_PROFILE_FILE 指定一个不存在的目录路径,则会创建缺失的目录结构。此外,以下特殊的pattern strings将被重写:

  • “%p” 扩展为进程ID。

  • “%h” 扩展为运行程序的计算机的主机名。

  • “%t” 扩展为TMPDIR环境变量的值。在Darwin上,这通常设置为临时临时目录。

  • “%Nm” 扩展为插桩的二进制的签名。当指定此模式时,运行时将创建一个包含N个 raw profile 的池,用于在线 profile 合并。运行时负责从池中选择 raw profile、锁定它并在程序退出之前更新它。如果没有指定N(即模式为“%m”),则假定N = 1。N必须在1和9之间。每个文件名模式只能有一个 merge pool specifier。

  • “%c” 什么都不扩展,但启用一种模式,在这种模式下,profile 计数器更新将持续同步到文件中。这意味着,如果插桩程序崩溃,或被信号杀死,完全覆盖信息仍然可以恢复。该模式不支持PGO的值分析,并且目前仅在Darwin上支持。对Linux的支持可能已经基本完成,但还需要测试,对Windows的支持可能需要更广泛的更改:如果您对移植该特性感兴趣,请参与进来。

# Run the program.
$ LLVM_PROFILE_FILE="foo.profraw" ./foo

注意,在Fuchsia系统上也支持连续模式,它是唯一受支持的模式,但实现不同。Darwin和Linux的实现依赖于 padding 和将文件映射到现有内存映射上的能力,现有内存映射通常只在POSIX系统上可用,不适合其他平台。

在Fuchsia上,我们依赖于在运行时使用某种间接方式重新定位计数器的能力。在每一次计数器访问中,我们为计数器地址添加一个bias。这个 bias 存储在 __llvm_profile_counter_bias 符号中,该符号由配置文件运行时提供,最初被设置为零,这意味着没有重定位。运行时可以将 profile 映射到任意位置的内存中,并将 bias 设置为原始计数器位置和新计数器位置之间的偏移量,此时,每次后续的计数器访问都将指向新位置,这允许类似于连续模式直接更新概要文件。

这种方法的优点是不需要任何特殊的操作系统支持。缺点是由于每次计数器访问需要额外的指令(二进制大小和性能方面的开销)加上重复的计数器(即二进制本身中的一个副本和映射到内存中的另一个副本)而产生额外的开销。通过在编译期间将 -runtime-counter-relocation 选项传递给后端,也可以为其他平台启用此实现。

$ clang++ -fprofile-instr-generate -fcoverage-mapping -mllvm -runtime-counter-relocation foo.cc -o foo

5. Creating coverage reports

在使用 raw profile 生成覆盖率报告之前,必须对它们进行索引。这是使用 llvm-profdata 中的 “merge” 工具完成的(它可以合并多个原始配置文件并同时索引它们):

# Index the raw profile.
$ llvm-profdata merge -sparse foo.profraw -o foo.profdata

呈现覆盖率报告有多种不同的方法。最简单的选择是生成一个面向行的报告:

# Create a line-oriented coverage report.
$ llvm-cov show ./foo -instr-profile=foo.profdata

该报告包括一个摘要视图以及模板函数及其实例化的专用子视图。对于我们的示例程序,我们得到了foo(…)和foo(…)的不同视图。如果启用 -show-line-counts-or-regions, llvm-cov将显示子行区域计数(即使在宏扩展中):

    1|   20|#define BAR(x) ((x) || (x))
                           ^20     ^2
    2|    2|template <typename T> void foo(T x) {
    3|   22|  for (unsigned I = 0; I < 10; ++I) { BAR(I); }
                                   ^22     ^20  ^20^20
    4|    2|}
------------------
| void foo<int>(int):
|      2|    1|template <typename T> void foo(T x) {
|      3|   11|  for (unsigned I = 0; I < 10; ++I) { BAR(I); }
|                                     ^11     ^10  ^10^10
|      4|    1|}
------------------
| void foo<float>(int):
|      2|    1|template <typename T> void foo(T x) {
|      3|   11|  for (unsigned I = 0; I < 10; ++I) { BAR(I); }
|                                     ^11     ^10  ^10^10
|      4|    1|}
------------------

如果--show-branches=count--show-expansion也被启用,子视图将显示除区域计数外的详细分支覆盖率信息:

------------------
| void foo<float>(int):
|      2|    1|template <typename T> void foo(T x) {
|      3|   11|  for (unsigned I = 0; I < 10; ++I) { BAR(I); }
|                                     ^11     ^10  ^10^10
|  ------------------
|  |  |    1|     10|#define BAR(x) ((x) || (x))
|  |  |                             ^10     ^1
|  |  |  ------------------
|  |  |  |  Branch (1:17): [True: 9, False: 1]
|  |  |  |  Branch (1:24): [True: 0, False: 1]
|  |  |  ------------------
|  ------------------
|  |  Branch (3:23): [True: 10, False: 1]
|  ------------------
|      4|    1|}
------------------

要生成覆盖统计信息的文件级摘要而不是面向行的报告,请尝试:

# Create a coverage summary.
$ llvm-cov report ./foo -instr-profile=foo.profdata
Filename           Regions    Missed Regions     Cover   Functions   Missed Functions   Executed      Lines
------------------------------------------------------------------------------------------------------------
/tmp/foo.cc          13             0          100.00%       3            0             100.00%         13
------------------------------------------------------------------------------------------------------------
TOTAL                13             0          100.00%       3            0             100.00%         13                

Missed Lines     Cover     Branches    Missed Branches     Cover
--------------------------------------------------------------------
	0           100.00%       12              2     	   83.33%
--------------------------------------------------------------------
	0           100.00%       12              2            83.33%

llvm-cov工具支持指定定制的demangler,在目录结构中写出报告,并生成html报告。有关选项的完整列表,请参阅

最后几点注意事项:

  • -sparse 参数是可选的,但会导致索引配置文件显著变小。如果索引配置文件将被PGO重用,则不应使用此选项。

  • raw profiles 被索引后可以被丢弃。配置文件运行时库的高级使用允许仪插桩程序将配置文件信息直接合并到磁盘上现有的原始配置文件中。这些细节超出了讨论范围。

  • llvm-profdata工具可用于将多个原始或索引的 profile合并在一起。要组合来自程序多次运行的分析数据,可以尝试:

    $ llvm-profdata merge -sparse foo1.profraw foo2.profdata -o foo3.profdata
    

6. Exporting coverage data

可以使用 llvm-cov export 子命令将覆盖率数据导出为JSON。在llvm-cov源代码中有一个全面的参考,它在较高的级别上定义了导出数据的结构。

7. Interpreting reports

在覆盖率总结中有五个统计数据:

  • 函数覆盖率是至少执行过一次的函数的百分比。如果执行了函数的任何实例化,则认为该函数已被执行。

  • 实例化覆盖率是至少执行过一次的函数实例化的百分比。模板函数和来自头文件的静态内联函数是两种可以有多个实例化的函数。默认情况下,该统计信息在报告中是隐藏的,但是可以通过 -show- instantiationsummary 选项启用。

  • 行覆盖率是至少执行过一次的代码行所占的百分比。只有函数体中的可执行行被认为是代码行。

  • 区域覆盖率是至少执行过一次的代码区域的百分比。代码区域可以跨越多行(例如在没有控制流的大型函数体中)。然而,也有可能一行包含多个代码区域(例如在" return x || y && z ")。

  • 分支覆盖率是“真”和“假”分支的百分比,这些分支至少被使用过一次。每个分支都与源代码中的个别条件相关联,每个条件的值可能为“真”或“假”。这些条件可能包含由布尔逻辑运算符链接的更大的布尔表达式。例如,“x = (y == 2) || (z < 10)”是一个布尔表达式,由两个单独的条件组成,每个条件的计算结果为真或假,产生四个分支结果。

在这五种统计数据中,函数覆盖率通常是粒度最小的,而分支覆盖率是粒度最大的。函数的100%分支覆盖率意味着函数的100%区域覆盖率。项目范围内每个统计数据的总数列在摘要中。

8. Format compatibility guarantees 格式兼容性保证

  • raw profile格式没有向后或向前兼容性保证。raw profiles可能依赖于用于生成它们的特定编译器版本。不建议长时间存储原始概要文件。

  • 工具必须保持与索引配置文件格式的向后兼容性。这些格式是不向前兼容的:即,使用格式版本X的工具将无法理解格式版本(X+k)。

  • 工具还必须保持与发射到插桩二进制文件的覆盖映射格式的向后兼容性。这些格式与转发不兼容。

  • JSON覆盖导出格式有(主要、次要、补丁)版本三种格式。只有主要版本增量指示向后不兼容的更改。小版本增量用于增加功能,补丁版本增量用于修复错误。

9. Impact of llvm optimizations on coverage reports

llvm优化(如内联或CFG简化)应该不会影响覆盖率报告的质量。这是因为从源区域到概要计数器的映射是不可变的,并且是在llvm优化器启动之前生成的。优化器不能证明删除配置文件计数器检测是安全的(因为它不是安全的:它会影响程序发出的配置文件),所以不去管它。

注意,这个覆盖特性不依赖于在优化过程中可能降级的信息,例如调试信息行表。

10. Using the profiling runtime without static initializers

默认情况下,编译器运行时使用一个静态初始化式来确定 profile 输出路径并注册一个写入函数。要收集 profile 而不使用静态初始化器,请手动执行以下操作:

  • 从每个测试共享库和可执行文件中导出 int __llvm_profile_runtime 符号。当链接器找到这个符号的定义时,它知道跳过加载包含分析运行时的静态初始化式的对象。

前向声明 void __llvm_profile_initialize_file(void) 并从每个可执行文件中调用它一次。这个函数解析 LLVM_PROFILE_FILE,设置输出路径,并截断该路径上的所有现有文件。要在不截断现有文件的情况下获得相同的行为,可以将文件名模式字符串传递给 void __llvm_profile_set_filename(char *)。这些调用可以放置在任何地方,只要它们在所有 __llvm_profile_write_file 调用之前。

  • 向前声明 int __llvm_profile_write_file(void) 并调用它来写出一个配置文件。该函数成功时返回0,否则返回非零值。多次调用此函数将概要数据追加到现有磁盘上的raw profile

在c++文件中,将它们声明为extern “C”。

11. Using the profiling runtime without a filesystem

分析运行时还支持缺乏文件系统的独立环境。运行时作为静态归档发布,其结构使对托管环境的依赖可选,具体取决于客户机应用程序使用的特性。

第一步是导出 __llvm_profile_runtime,如上所述,以禁用默认的静态初始化器。不要调用上面描述的 *_file() api,使用以下方法直接将配置文件保存到你控制的缓冲区中:

  • 向前声明 uint64_t __llvm_profile_get_size_for_buffer(void) 并调用它来确定配置文件的大小。您需要分配这个大小的缓冲区。

  • 前向声明 int __llvm_profile_write_buffer(char *Buffer) 并调用它来将当前计数器复制到Buffer中,预期缓冲区已经被分配,并且足够大,可以用于配置文件。

  • 可选地,向前声明 void __llvm_profile_reset_counters(void),并在进入要分析的特定部分之前调用它来重置计数器。只有在配置文件中应该排除某些设置时,这才有用。

在c++文件中,将它们声明为extern “C”。

12. Collecting coverage reports for the llvm project

要为llvm(及其任何子项目)准备覆盖率报告,请在cmake配置中添加 -DLLVM_BUILD_INSTRUMENTED_COVERAGE=Onraw profiles 将被写入$BUILD_DIR/profiles/。要准备html报告,请运行 llvm/utils/prepare-code-coverage-artifact.py

要为raw profiles指定备用目录,请使用 -DLLVM_PROFILE_DATA_DIR 。使用 `-DLLVM_PROFILE_MERGE_POOL_SIZE`` 命令修改策略合并池的大小。

13. Drawbacks and limitations

在2.26版本之前,GNU binutils BFD连接器不能以--gc-sections模式链接使用 -fcoverage-mapping 编译的程序。可能的解决方案包括禁用 --gc-sections、升级到更新版本的BFD,或使用Gold连接器。

代码覆盖不能精确地处理异常出现时控制流或堆栈展开中不可预测的更改。考虑以下函数:

int f() {
  may_throw();
  return 0;
}

如果调用 may_throw() 将异常传播到 f 中,代码覆盖工具可能会将返回语句标记为已执行,即使它没有执行。对 longjmp() 的调用也可以产生类似的效果。

14. Clang implementation details

希望理解或改进clang代码覆盖实现的人可能会对本节感兴趣

15. Gap regions

gap regions 是有计数的源区。报表工具不能将行执行计数设置为来自gap regions的计数,除非该区域是一行只有一个区域。

gap regions用于消除覆盖报告中的非自然工件,例如红色的 “unexecuted” 高亮显示在以其他方式覆盖的行末尾,或蓝色的 “executed” 高亮显示在以其他方式未执行的行开头。

16. Branch regions

当使用 --show-branches 在基于源代码的文件级子视图中查看分支覆盖细节时,建议用户显示所有宏扩展(使用选项 show-expansion),因为宏可能包含隐藏的分支条件。覆盖率汇总报告将始终在函数或源文件的总体分支覆盖率计数中包含这些基于宏的布尔表达式。

对于常量折叠分支条件,不会跟踪分支覆盖率,因为不会为这些情况生成分支。在基于源代码的文件级子视图中,这些分支将简单地显示为 [Folded - Ignored] ,以便告知用户发生了什么。

分支覆盖直接与源代码中的分支生成条件联系在一起。用户不应该看到没有实际绑定到源代码的隐藏分支。

17. Switch statements

switch 主体的区域映射由覆盖整个主体的gap region组成(从’ switch(…){‘中的’{'开始,到最后一个case结束处结束)。这个gap region的计数为零:这导致case语句之间的“gap”区域(其中不包含可执行代码)显示为裸露的。

当访问一个switch案例时,父区域被扩展:如果父区域没有起始位置,它的起始位置将成为案例的起始位置。这用于支持没有CompondStmt主体的switch语句,其中switch主体和单个case共享一个计数。

对于具有CompondStmt主体的 switch,在每个switch 案例的开始处创建一个新的区域。

还为每个switch 情况生成分支区域,包括默认情况。如果源代码中没有显式定义的默认情况,将生成一个分支区域,以对应编译器生成的隐式默认情况。隐式分支区域与switch语句条件的行号和列号绑定,因为隐式情况不存在源代码。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值