Clang代码覆盖率检测(插桩技术)

简介: Clang的全称是C Language Family Frontend for LLVM,即基于LLVM的C系列语言的前端编译器。iOS应用的前端编译,即是采用Clang完成的。本篇文章,我们主要介绍Clang内置的一个简单的代码覆盖率检测功能,对于iOS开发来说,此功能更多用于Objective-C的方法插桩,为二进制重排提供支持,优化应用启动速度。但代码覆盖率检测功能并不仅仅只能应用与二进制重排,其本质是对于函数级、基本块级或代码边缘级插入回调,我们可以基于这一原理更灵活的实现所需要的功能。

Clang代码覆盖率检测(插桩技术)

Clang的全称是C Language Family Frontend for LLVM,即基于LLVM的C系列语言的前端编译器。iOS应用的前端编译,即是采用Clang完成的。本篇文章,我们主要介绍Clang内置的一个简单的代码覆盖率检测功能,对于iOS开发来说,此功能更多用于Objective-C的方法插桩,为二进制重排提供支持,优化应用启动速度。但代码覆盖率检测功能并不仅仅只能应用与二进制重排,其本质是对于函数级、基本块级或代码边缘级插入回调,我们可以基于这一原理更灵活的实现所需要的功能。

1. Tracing PCs with guards

开启Clang代码覆盖率检查功能,需要配置-fsanitize-coverage编译参数,你可以创建一个iOS模板工程做测试,在Build Settings->Apple Clang - Custom Complier Flags->Other C Flags下面配置。如图:

trace-pc-guard模式下,所有代码块首部都会被插入如下回调函数:

void \_\_sanitizer\_cov\_trace\_pc_guard(uint32_t *guard)

此回调函数是需要开发者自定义的,除此之外,还需要实现对应初始化的回调函数:

void \_\_sanitizer\_cov\_trace\_pc\_guard\_init(uint32_t start, uint32_t stop)

在示例工程的main.m文件中定义这两个回调如下:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    printf("%s \n",info.dli_sname);
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    for (uint32_t *x = start; x < stop; x++) {
        *x = ++N;
    }
    printf("INIT Count: %llu \n", N);
}

其中,\_\_sanitizer\_cov\_trace\_pc\_guard\_init为初始化回调,通过其中参数可以获取到符号个数,\_\_sanitizer\_cov\_trace\_pc_guard是插桩函数,每个代码块开始调用时,都会首先调用此插桩函数。

直接运行代码,控制台输出如下:

INIT Count: 14 
main 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
-[SceneDelegate window] 
-[SceneDelegate setWindow:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate scene:willConnectToSession:options:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[ViewController viewDidLoad] 
-[SceneDelegate sceneWillEnterForeground:] 
-[SceneDelegate sceneDidBecomeActive:] 

可以看到,输出的结果就是按照项目中方法的调用顺序排序的。你可能看到有许多重复的符号,这是由于trace-pc-guard设定的,其会对源码中任意的代码块开始执行时进行插桩函数回调,包括if判断,while循环以及Block调用等,例如你可以尝试在ViewController.m文件的viewDidLoad方法中添加一些代码,如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    printf("开始Block==================\n");
    void(^block)(void) = ^{

    };
    block();
    printf("开始循环==================\n");
    int n = 3;
    while (n > 0) {
        n--;
    }
    printf("开始分支判断==================\n");
    if (n < 10) {
        n++;
    }
}

运行项目,输出效果如下:

INIT Count: 18 
main 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
-[SceneDelegate window] 
-[SceneDelegate setWindow:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate scene:willConnectToSession:options:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[ViewController viewDidLoad] 
开始Block==================
__29-[ViewController viewDidLoad]_block_invoke 
开始循环==================
-[ViewController viewDidLoad] 
-[ViewController viewDidLoad] 
-[ViewController viewDidLoad] 
开始分支判断==================
-[ViewController viewDidLoad] 
-[SceneDelegate sceneWillEnterForeground:] 
-[SceneDelegate sceneDidBecomeActive:] 

有时候并非所有的代码块都需要插桩,例如做二进制重排时,只需要方法和函数的插桩,也有配置方式,我们后面介绍。

2. Inline 8bit-counters

此模式需要配置成:

-fsanitize-coverage=inline-8bit-counters

此模式与trace-pc-guard类似,只是其在代码块开始时不会进行回调,而是简单的增加内置计数器的计数。同样,在此模式下,用户需要实现如下自定义函数:

void __sanitizer_cov_8bit_counters_init(char *start, char *end) {
  // [start,end) is the array of 8-bit counters created for the current DSO.
  // Capture this array in order to read/modify the counters.
}

此函数对应计数器的初始化。

3. Inline bool-flag

此模式与inline-8bit-counters模式类似,需要配置成:

-fsanitize-coverage=inline-bool-flag

在此模式下,在代码块开始时会将一个内置的布尔值置为true,而不是增加计数器的计数。需要实现如下函数来捕获此变量:

void __sanitizer_cov_bool_flag_init(bool *start, bool *end) {
  // [start,end) is the array of boolean flags created for the current DSO.
  // Capture this array in order to read/modify the flags.
}

4. Tracing PCs

此模式在代码块的开始出会回调\_\_sanitizer\_cov\_trace\_pc() 函数,也是插桩回调,此模式可配置为:

-fsanitize-coverage=trace-pc

对应实现自定义的插桩函数如下:

void __sanitizer_cov_trace_pc(void*a) {
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    printf("%s %p \n",info.dli_sname, info.dli_saddr);
    printf("__sanitizer_cov_trace_pc:%p\n",a);
}

对于此模式,我们可以配置一个额外的参数来区别间接调用,例如修改ViewController.m文件中的代码如下:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    printf("开始Block==================\n");
    void(^block)(void) = ^{

    };
    block();
    printf("开始循环==================\n");
    int n = 3;
    while (n > 0) {
        n--;
    }
    printf("开始分支判断==================\n");
    if (n < 10) {
        n++;
    }
    [self log];
}

- (void)log {
    
}

@end

新定义了一个log函数,并在ViewDidLoad中进行了调用,配置编译选项如下:

-fsanitize-coverage=trace-pc,indirect-calls

对应实现间接调用的插桩回调如下:

void __sanitizer_cov_trace_pc_indir(void *callee) {
    printf("__sanitizer_cov_trace_pc_indirect:%p\n",callee);
}

运行代码,控制台输出如下:

main 0x105f5dee0 
__sanitizer_cov_trace_pc:0x1
-[AppDelegate application:didFinishLaunchingWithOptions:] 0x105f5dae0 
__sanitizer_cov_trace_pc:0x6000019241f0
-[SceneDelegate window] 0x105f5e200 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[SceneDelegate setWindow:] 0x105f5e240 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[SceneDelegate window] 0x105f5e200 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[SceneDelegate window] 0x105f5e200 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[SceneDelegate scene:willConnectToSession:options:] 0x105f5df80 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[SceneDelegate window] 0x105f5e200 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[SceneDelegate window] 0x105f5e200 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[SceneDelegate window] 0x105f5e200 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[ViewController viewDidLoad] 0x105f5d940 
__sanitizer_cov_trace_pc:0x7fec54f08490
-[ViewController viewDidLoad] 0x105ffa2c8 
__sanitizer_cov_trace_pc_indirect:0x7fff20183600
开始Block==================
-[ViewController viewDidLoad] 0x105ffa2c8 
__sanitizer_cov_trace_pc_indirect:0x105f5da80
__29-[ViewController viewDidLoad]_block_invoke 0x105f5da80 
__sanitizer_cov_trace_pc:0x105f60040
开始循环==================
-[ViewController viewDidLoad] 0x105f5d940 
__sanitizer_cov_trace_pc:0x7fff864ab328
-[ViewController viewDidLoad] 0x105f5d940 
__sanitizer_cov_trace_pc:0x7fff864ab328
-[ViewController viewDidLoad] 0x105f5d940 
__sanitizer_cov_trace_pc:0x7fff864ab328
开始分支判断==================
-[ViewController viewDidLoad] 0x105f5d940 
__sanitizer_cov_trace_pc:0x7fff864ab328
-[ViewController viewDidLoad] 0x105ffa2c8 
__sanitizer_cov_trace_pc_indirect:0x7fff201833c0
-[ViewController log] 0x105f5dab0 
__sanitizer_cov_trace_pc:0x7fec54f08490
-[SceneDelegate sceneWillEnterForeground:] 0x105f5e140 
__sanitizer_cov_trace_pc:0x600001b1ffc0
-[SceneDelegate sceneDidBecomeActive:] 0x105f5e080 
__sanitizer_cov_trace_pc:0x600001b1ffc0

5. 不同级别的检测

前面我们介绍的编译模式,会对函数,Block和逻辑代码块进行检测,有时候我们不需要这个细粒度的检测,例如在二进制重排时,我们仅仅想检测方法和函数,只想对方法函数进行插桩,此时就可以配置检测级别参数,支持的级别参数有三种:

1. edge:默认的级别,细粒度最高的级别,函数,Block和代码块都会被插桩。

2. bb:基础的块级代码会被插桩。

3. func:仅仅函数块会被插桩。

通常我们在做二进制重排时,更关注的是函数的调用顺序,使用func等级即可,编译设置如下:

-fsanitize-coverage=trace-pc,func
目录
相关文章
|
3月前
|
Rust 数据可视化 C++
WASM性能分析-插桩方案
本文结合了代码插桩和性能火焰图的技术,以 WebAssembly 为例介绍了性能分析的方法和相关实现。
|
1月前
|
XML 监控 负载均衡
Jacoco的覆盖率原理
JaCoCo(Java Code Coverage)是一种广泛使用的代码覆盖率工具,通过在字节码中插入探针(Probe)来收集覆盖率信息。
98 5
Jacoco的覆盖率原理
|
3月前
|
网络协议 NoSQL Anolis
开局一张图,debug全靠瞪|内核问题定位与静态分析实战
本文描述了一个在AnolisOS 8.8操作系统上遇到的内核崩溃问题的调试过程。
|
监控 NoSQL Linux
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解(一)
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解
795 0
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解(一)
|
NoSQL Shell C语言
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解(二)
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解
455 0
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解(二)
|
存储 NoSQL IDE
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解(三)
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解
366 0
【五、深入浅出GDB调试器】如何修复程序bug或优化代码:gdb调试器的来龙去脉与debug全方位实战详解(三)
|
存储 Java 数据挖掘
AGP 实现方法插桩探究
AGP 实现方法插桩探究
208 0
AGP 实现方法插桩探究
|
监控 Java Android开发
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
372 0
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
|
Android开发
静态分析Smali添加日志
如何动手实现Smali添加日志
613 0
静态分析Smali添加日志