配置Clang插桩
LLVM
内置了一个简单的代码覆盖率检测工具(SanitizerCoverage)。它在函数级
、基本块级
和边缘级上
插入对用户定义函数的调用,通过这种方式可以顺利对OC方法
、C函数
、Block
、Swift的方法/函数
进行全面HOOK
。
Clang13的文档 关于 Tracing PCs (跟踪CPU执行到的代码),通过Clang插桩
我们可以跟踪到所有函数的执行,包括APP启动时刻所调用的
。
Clang 插桩
- 搭建测试项目,在
Build Setting -> Other C Flags
中,增加-fsanitize-coverage=trace-pc-guard
的配置
- 编译工程有如下报错
说明__sanitizer_cov_trace_pc_guard_init
与__sanitizer_cov_trace_pc_guard
方法需要我们实现,Clang13的官方文档
内容如下
- 按照文档,在项目中加入如下代码
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
@end
__sanitizer_cov_trace_pc_guard_init函数
参数一 start
是一个指针,指向无符号int类型
4个字节,相当于一个数组的起始位置,即符号的起始位置
(是从高位往低位读)
参数二 stop
由于数据的地址是往下读的
(即从高往低读,所以此时获取的地址并不是stop真正的地址,而是标记的最后的地址,读取stop时
由于stop占4个字节,stop真实地址 = stop打印的地址-0x4
)
// 运行项目,打印以下内容:
INIT: 0x10e838a0c 0x10e838aa0
- 打印来自
__sanitizer_cov_trace_pc_guard_init函数
- 通过
for循环
代码,发现从start
至stop
的地址中,存储的是uint32_t
类型的值 - 循环中
x
为uint32_t
指针类型,x++
表示指针运算,步长+1会增加数据类型的长度 -
uint32_t
占4字节,所以循环中的代码含义,每四字节记录一个++N
的值
lldb验证
// 读取start
(lldb) x 0x10e838a0c
0x10e838a0c: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................
0x10e838a1c: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 ................
// 读取stop
(lldb) x 0x10e838aa0-4
0x10e838a9c: 25 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 %...............
0x10e838aac: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 读取最后一个值,要在stop地址的基础上减去
4字节
- 从
start
至stop
,读出值为25
(注意是16进制)表示当前项目中方法/函数/Block
的符号个数。
__sanitizer_cov_trace_pc_guard函数
参数guard
是一个哨兵,告诉我们是第几个方法被调用的
- 在
__sanitizer_cov_trace_pc_guard
函数中设置断点,运行项目
查看函数调用栈,由main函数
调用
- 继续调试,进入该函数的断点,由
didFinishLaunchingWithOptions函数
调用
我们发现项目中每一个方法和函数
的调用,都会触发__sanitizer_cov_trace_pc_guard
的断点,并且由当前执行的方法/函数调用
测试__sanitizer_cov_trace_pc_guard
方法
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"__sanitizer_cov_trace_pc_guard");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan方法执行");
test();
}
void(^block)(void) = ^(void) {
NSLog(@"Block执行");
};
void test() {
NSLog(@"test函数执行");
block();
}
// 控制台打印
2021-10-20 20:57:20.938427+0800 TraceDemo[16078:2734157] __sanitizer_cov_trace_pc_guard
2021-10-20 20:57:20.940051+0800 TraceDemo[16078:2734157] touchesBegan方法执行
2021-10-20 20:57:20.940299+0800 TraceDemo[16078:2734157] __sanitizer_cov_trace_pc_guard
2021-10-20 20:57:20.940499+0800 TraceDemo[16078:2734157] test函数执行
2021-10-20 20:57:20.940675+0800 TraceDemo[16078:2734157] __sanitizer_cov_trace_pc_guard
2021-10-20 20:57:20.940861+0800 TraceDemo[16078:2734157] Block执行
- 从运行结果来看,
方法和函数全部被HOOK
- 被拦截的
方法和函数
,仅限当前项目
中的符号。例如:NSLog等外部符号不会被HOOK
- 二进制重排的本意,就是将代码实现的
二进制中方法/函数符号
在启动时刻按照顺序排列在前面
。外部符号的方法/函数实现,并不在当前项目中,所以它们的符号也不在重排的范围之内。
测试set方法能否HOOK到?
@interface ViewController ()
@property(nonatomic, assign) int age;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.age = 10;
}
image.png
set方法
成功HOOK
Clang插桩的原理
__sanitizer_cov_trace_pc_guard
方法添加断点,点击屏幕触发touchesBegan
方法进行调试
在每一个方法/函数/Block
内容执行前,都调用了__sanitizer_cov_trace_pc_guard
函数
Clang插桩的实现原理:
-
只要添加了Clang插桩的标记
,编译器就会在当前项目中所有方法
、函数
、Block
的代码实现边缘,插入一句__sanitizer_cov_trace_pc_guard
函数的调用代码,达到方法/函数/Block的100%覆盖
- 相当于编译器在编译时期,
修改了当前的二进制文件
- 修改时机:有可能是语法分析之后
生成IR中间代码
时进行修改(未验证)
获取符号名称
我们现在已经能HOOK到所有的方法/函数/Block
,现在要怎么获取它们的符号,写入order文件?
- 查看
__builtin_return_address
方法
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
//获取上一个函数的地址,通过这个地址就能拿到函数的符号名称
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
- 得到调用者的函数地址
获取符号名称
#include <dlfcn.h>
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
NSLog(@"%s", info.dli_fname);
NSLog(@"%p", info.dli_fbase);
NSLog(@"%s", info.dli_sname);
NSLog(@"%p", info.dli_saddr);
}
使用dladdr函数
传入函数地址
获取基本信息
,存入Dl_info
结构体
- Dl_info结构体的定义
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
-
dli_fname
:当前MachO路径(文件的名字) -
dli_fbase
:当前MachO起始地址(文件的地址) -
dli_sname
:函数名称 -
dli_saddr
:函数地址
运行项目查看打印结果,发现可以通过dli_sname
得到函数名称
/Users/wn/Library/Developer/CoreSimulator/Devices/C53887CF-B3AC-4677-B6FD-DD090CC6D346/data/Containers/Bundle/Application/E211EC24-FFD6-4745-8DFE-345A1DDDC07C/TraceDemo.app/TraceDemo
0x10fb8a000
-[ViewController touchesBegan:withEvent:]
0x10fb8dd20
- 修改测试代码运行项目
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>
@interface ViewController ()
@end
@implementation ViewController
+ (void)load {
// NSLog(@"load函数");
}
- (void)viewDidLoad {
[super viewDidLoad];
}
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;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
NSLog(@"%s", info.dli_sname);
}
@end
// 控制台打印
+[ViewController load]
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:]
获取到启动时刻
所有被调用的方法、函数、Block
的函数名称。其中部分函数
多次调用,出现了重复符号
,还需要对其排重。
通过原子队列保存符号
修改代码,测试能否获取到子线程的符号
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelectorInBackground:@selector(testSleep) withObject:nil];
}
- (void)testSleep {
sleep(3);
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
NSLog(@"%@", [NSThread currentThread]);
Dl_info info;
dladdr(PC, &info);
NSLog(@"%s", info.dli_sname);
}
// 控制台打印
-[SceneDelegate sceneDidBecomeActive:]
<NSThread: 0x6000038a7540>{number = 8, name = (null)}
通过日志可以确定能够获取子线程的符号,同时说明__sanitizer_cov_trace_pc_guard
的回调是多线程的。所以当我们通过回调收集函数名称时
也要保证线程安全。
- 以下案例我们使用
线程相对安全
的原子队列
进行返回地址的收集
#import <libkern/OSAtomic.h>
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义结构体
typedef struct {
void *pc;
void *next;
} SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
//创建结构体
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC, NULL};
//结构体入栈
//offsetof:参数1传入类型,将下一个节点的地址返回给参数
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
// 生成order文件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
//取空则停止循环
if(node == NULL){
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSLog(@"%s", info.dli_sname);
}
}
原子队列保存符号步骤
- 定义: 定义
原子队列
、结构体
,pc存储当前返回地址,next存储下一个节点地址 - 收集:
创建结构体,对pc赋值,next设置为NULL
结构体入栈
offsetof:宏,参数1传入类型,将下一个节点的地址返回给参数2 - 生成order文件
循环读取node,取空则停止循环
将返回地址写入Dl_info结构体
打印符号名称
运行工程,点击屏幕触发touchesBegan
方法产生死循环
// 控制台打印
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
......
解决循环引发的天坑
上面运行工程产生了死循环,下面进行调试
- 在
touchesBegan
方法中设置断点,运行项目查看汇编代码
,发现touchesBegan
方法中插入了三次__sanitizer_cov_trace_pc_guard
函数的调用
这就是循环
引发的天坑,SanitizerCoverage
不但拦截方法
、函数
、Block
,还会对循环
进行HOOK。
案例中while循环被HOOK
,循环的执行会进入回调函数
。回调函数中存入队列的还是touchesBegan
的函数地址,这会导致队列中永远存在一个到两个touchesBegan
,next永远获取不完。
解决办法:
Build Setting
-> Other C Flags
中,将配置修改为-fsanitize-coverage=func,trace-pc-guard
对其增加func参数
- 再次运行项目,点击屏幕,控制台打印如下
-[ViewController touchesBegan:withEvent:]
-[SceneDelegate sceneDidBecomeActive:]
-[SceneDelegate sceneWillEnterForeground:]
-[ViewController viewDidLoad]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate window]
-[AppDelegate application:didFinishLaunchingWithOptions:]
main
+[ViewController load]
修改配置项仅拦截方法的调用
,成功解决循环引发的天坑。
取反&去重
还有几个问题需要解决?
- 过滤掉自身
touchesBegan
的函数名称 - 不是OC的
函数
和Block
等符号,需要在符号名称之前增加_
- 相同的函数符号,需要
进行去重
- 队列原则
先进后出
,所以我们需要的符号顺序需要反转
修改touchesBegan
方法,解决上述问题
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 定义数组
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if(node == NULL){
break;
}
Dl_info info;
dladdr(node->pc, &info);
// 转字符串
NSString *name = @(info.dli_sname);
// 不是OC函数名称添加_,获取符号名称,如果不是+[和-[开头,视为函数或Block,前面加_
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbleNames addObject:symbolName];
}
// 反向遍历数组
symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects];
NSLog(@"%@",symbleNames);
}
// 运行工程,控制台打印
TraceDemo[47155:640473] (
"+[ViewController load]",
"_main",
"-[AppDelegate application:didFinishLaunchingWithOptions:]",
"-[SceneDelegate window]",
"-[SceneDelegate setWindow:]",
"-[SceneDelegate window]",
"-[SceneDelegate window]",
"-[SceneDelegate scene:willConnectToSession:options:]",
"-[SceneDelegate window]",
"-[SceneDelegate window]",
"-[SceneDelegate window]",
"-[ViewController viewDidLoad]",
"-[ViewController setAge:]",
"-[SceneDelegate sceneWillEnterForeground:]",
"-[SceneDelegate sceneDidBecomeActive:]",
"-[ViewController touchesBegan:withEvent:]"
)
相同符号去重
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 定义数组
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if(node == NULL){
break;
}
Dl_info info;
dladdr(node->pc, &info);
// 转字符串
NSString *name = @(info.dli_sname);
// 给OC函数名称添加_
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbleNames addObject:symbolName];
}
// 反向遍历数组
NSEnumerator * em = [symbleNames reverseObjectEnumerator];
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
NSString * name;
while (name = [em nextObject]) {
// 如果符号名称不在数组中,添加到数组
if (![funcs containsObject:name]) {//去重:数组没有name
[funcs addObject:name];
}
}
//去掉当前函数名称touchesBegan
[funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
NSLog(@"%@",funcs);
}
// 运行工程,控制台打印
TraceDemo[47196:643780] (
"+[ViewController load]",
"_main",
"-[AppDelegate application:didFinishLaunchingWithOptions:]",
"-[SceneDelegate window]",
"-[SceneDelegate setWindow:]",
"-[SceneDelegate scene:willConnectToSession:options:]",
"-[ViewController viewDidLoad]",
"-[ViewController setAge:]",
"-[SceneDelegate sceneWillEnterForeground:]",
"-[SceneDelegate sceneDidBecomeActive:]"
)
生成order文件
- 修改
touchesBegan
方法,将符号列表写入.order文件
// 添加load方法与block
+(void)load {
block();
}
void(^block)(void) = ^(void){
NSLog(@"block函数执行!");
};
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 定义数组
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if(node == NULL){
break;
}
Dl_info info;
dladdr(node->pc, &info);
// 转字符串
NSString *name = @(info.dli_sname);
// 给OC函数名称添加_
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbleNames addObject:symbolName];
}
// 反向遍历数组
NSEnumerator * em = [symbleNames reverseObjectEnumerator];
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
NSString * name;
while (name = [em nextObject]) {
// 如果符号名称不在数组中,添加到数组
if (![funcs containsObject:name]) {//去重:数组没有name
[funcs addObject:name];
}
}
//去掉当前函数名称touchesBegan
[funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
//写入文件
//1.编程字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"%@",funcStr);
}
// 运行工程,控制台打印
+[ViewController load]
_block_block_invoke
_main
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate scene:willConnectToSession:options:]
-[ViewController viewDidLoad]
-[ViewController setAge:]
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
- 拿到
.order文件
选择Add Additional Simulators...
- 选中
案例App
点击Downlad Container...
,如下图
- 选择
存放路径
下载.xcappdata文件
,右键显示包内容
,在AppData/tmp
目录下找到.order文件
- 将
.order文件
拷贝到工程根目录,在Build Setting
->Order File
进行配置
- 在
Build Settings
->Write Link Map File
设置为YES
-
编译项目
打开LinkMap文件
查看,发现配置生效二进制重排成功
Swift符号覆盖
- 创建
SwiftTest.swift文件
代码如下
import UIKit
class SwiftTest: NSObject {
@objc class public func swiftTest(){
print("Swift Test ...")
}
}
- 在
ViewController
的load方法
中分别调用Block
和swiftTest
方法
+(void)load
{
[SwiftTest swiftTest];
block();
}
void(^block)(void) = ^(void){
NSLog(@"block函数执行!");
};
- 在
Other C Flags
中的配置仅对Clang编译器生效
。而Swift
使用swiftc编译器
,要想获得swift函数符号
,需要对Other Swift Flags
添加-sanitize-coverage=func
、-sanitize=undefined
两项
- 运行项目,点击屏幕,查看控制台输出内容
+[ViewController load]
_$s9TraceDemo9SwiftTestC05swiftD0yyFZTo
_$s9TraceDemo9SwiftTestC05swiftD0yyFZ
_$ss5print_9separator10terminatoryypd_S2StFfA0_
_$ss5print_9separator10terminatoryypd_S2StFfA1_
_block_block_invoke
_main
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate scene:willConnectToSession:options:]
-[ViewController viewDidLoad]
-[ViewController setAge:]
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
OC和Swift的混编工程中,成功得到Swift函数符号
网友评论