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文件中定义这两个回调如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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是插桩函数,每个代码块开始调用时,都会首先调用此插桩函数。
直接运行代码,控制台输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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方法中添加一些代码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - (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++; } }
运行项目,输出效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 此模式需要配置成:
1 -fsanitize-coverage=inline-8bit-counters
此模式与trace-pc-guard类似,只是其在代码块开始时不会进行回调,而是简单的增加内置计数器的计数。同样,在此模式下,用户需要实现如下自定义函数:
1 2 3 4 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模式类似,需要配置成:
1 -fsanitize-coverage=inline-bool-flag
在此模式下,在代码块开始时会将一个内置的布尔值置为true,而不是增加计数器的计数。需要实现如下函数来捕获此变量:
1 2 3 4 void __sanitizer_cov_bool_flag_init(bool *start, bool *end) { }
4. Tracing PCs 此模式在代码块的开始出会回调__sanitizer_cov_trace_pc() 函数,也是插桩回调,此模式可配置为:
1 -fsanitize-coverage=trace-pc
对应实现自定义的插桩函数如下:
1 2 3 4 5 6 7 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文件中的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #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中进行了调用,配置编译选项如下:
1 -fsanitize-coverage=trace-pc,indirect-calls
对应实现间接调用的插桩回调如下:
1 2 3 void __sanitizer_cov_trace_pc_indir(void *callee) { printf("__sanitizer_cov_trace_pc_indirect:%p\n",callee); }
运行代码,控制台输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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等级即可,编译设置如下:
1 -fsanitize-coverage=trace-pc,func
专注技术,热爱生活,交流技术,也做朋友。
—— 珲少 QQ:316045346
同时,如果本篇文章让你觉得有用,欢迎分享给更多朋友,请标明出处。