资源准备
简介
在项目中,对于OC
方法,可以对objc_msgSend
方法进行HOOK
。这样仅适用于OC
方法,对于C函数
、Block
、Swift
的方法/函数,都无法拦截
LLVM
内置了一个简单的代码覆盖率检测工具(SanitizerCoverage
)。它在函数级、基本块级和边缘级上插入对用户定义函数的调用,通过这种方式,可以顺利对OC
方法、C函数
、Block
、Swift
的方法/函数进行全面HOOK
。
Clang
插庄
配置
搭建测试项目,在Build Setting
–> Other C Flags
中,增加-fsanitize-coverage=trace-pc-guard
的配置
按照文档,在项目中加入示例代码:
#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
运行项目,打印以下内容:
INIT: 0x100bbd4c0 0x100bbd4f8
复制代码
-
打印来自
__sanitizer_cov_trace_pc_guard_init
函数; -
通过
for
代码中的循环,不难看出,从start
至stop
的地址中,存储的是uint32_t
类型的值; -
循环中
x
为uint32_t
指针类型,x++
表示指针运算,步长+1
会增加数据类型的长度; -
uint32_t
占4字节
,所以循环中的代码含义,每四字节记录一个++N
的值。
使用lldb
验证:
//读取start
(lldb) x 0x100bbd4c0
0x100bbd4c0: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................
0x100bbd4d0: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 ................
//读取stop
(lldb) x 0x100bbd4f8-4
0x100bbd4f4: 0e 00 00 00 c0 d2 e0 00 01 00 00 00 00 00 00 00 ................
0x100bbd504: 00 00 00 00 66 73 bb 00 01 00 00 00 00 00 00 00 ....fs..........
复制代码
-
读取最后一个值,要在
stop
地址的基础上减去4字节
; -
从
start
至stop
,读出值为01~0e
,这些值表示当前项目中方法/函数的符号个数。
__sanitizer_cov_trace_pc_guard
在__sanitizer_cov_trace_pc_guard
函数中设置断点,运行项目。来到断点,查看函数调用栈:
- 由
main
函数调用。
继续执行程序,又会进入该函数的断点:
- 由
didFinishLaunchingWithOptions
方法调用。
我们会发现一个现象,项目中每一个方法和函数的调用,都会触发__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();
}
复制代码
输出结果:
-
从运行结果来看,方法和函数全部被
HOOK
; -
被拦截的方法和函数,仅限当前项目中的符号,例如:
NSLog
等外部符号不会被HOOK
; -
二进制重排的本意,就是将代码实现的二进制中方法/函数符号,在启动时刻按照顺序排列在前面。外部符号的方法/函数实现,并不在当前项目中,所以它们的符号也不在重排的范围之内。
原理
查看汇编代码:
-
在每一个方法和函数的汇编代码中,都多了一句
bl
指令,调用的正是__sanitizer_cov_trace_pc_guard
函数; -
Clang
插庄的实现原理:只要添加Clang
插庄的标记,编译器就会在当前项目中,在所有方法、函数、Block
的代码实现的边缘,插入一句__sanitizer_cov_trace_pc_guard
函数的调用代码,达到方法/函数/Block
的100%
覆盖; -
相当于编译器在编译时期,修改了当前的二进制文件;
-
修改时机,有可能是语法分析之后,生成
IR
中间代码时进行修改(未验证)。
获取符号名称
示例代码中,使用了一个__builtin_return_address
函数:
- 函数的作用,获取当前返回地址,也就是调用者的函数地址。
得到调用者的函数地址,获取符号名称:
#include <dlfcn.h>
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"__sanitizer_cov_trace_pc_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
:函数地址
运行项目,测试打印结果:
__sanitizer_cov_trace_pc_guard
dli_fname:/private/var/containers/Bundle/Application/E4DBCC4F-B132-4462-A148-03B398B476F5/SanitizerCoverage.app/SanitizerCoverage
dli_fbase:0x104cb0000
dli_sname:-[ViewController touchesBegan:withEvent:]
dli_saddr:0x104cb5a64
复制代码
- 通过
dli_sname
可以得到函数名称。
修改测试代码,运行项目:
#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];
test();
}
void(^block)(void) = ^(void){
// NSLog(@"Block执行");
};
void test(){
// NSLog(@"test函数执行");
block();
}
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]
test
block_block_invoke
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
复制代码
- 获取到启动时刻,所有被调用的方法、函数、
Block
的函数名称。其中部分函数多次调用,出现了重复符号,还需要对其排重。
实际应用
日常开发中,我们经常会使用多线程开发。如果函数处于子线程,那__sanitizer_cov_trace_pc_guard
函数也会在子线程进行回调。
所以,当我们通过回调收集函数名称时,也要保证线程安全。
收集返回地址
以下案例,我们使用线程相对安全的原子队列进行返回地址的收集:
//定义原子队列
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传入类型,将下一个节点的地址返回给参数
2OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
- (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
-
-
测试
-
循环读取
node
,取空则停止循环 -
将返回地址写入
Dl_info
结构体 -
打印符号名称
-
循环引发的大坑
运行上述案例:
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:]
block_block_invoke
test
-[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
的函数名称; -
函数和
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);
if([name isEqualToString:@(__func__)]){
continue;
}
if(![name hasPrefix:@"+["] && ![name hasPrefix:@"-["]) {
name = [@"_" stringByAppendingString:name];
}
if([symbolNames containsObject:name]){
continue;
}
[symbolNames addObject:name];
}
symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects];
for (NSString *symbol in symbolNames) {
NSLog(@"%@", symbol);
}
}
复制代码
打印结果:
+[ViewController load]
_main
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate setWindow:]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window]
-[ViewController viewDidLoad]
_test
_block_block_invoke
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
复制代码
-
过滤掉自身
touchesBegan
的函数名称; -
获取符号名称,如果不是
+[
和-[
开头,视为函数或Block
,前面加_
; -
如果符合名称在数组中存在,跳过。否则,添加到数组;
-
将数组反转,并循环打印。
写入文件并配置
修改touchesBegan
方法,将符号列表写入.order
文件:
- (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);
if([name isEqualToString:@(__func__)]) {
continue;
}
if(![name hasPrefix:@"+["] && ![name hasPrefix:@"-["]) {
name = [@"_" stringByAppendingString:name];
}
if([symbolNames containsObject:name]){
continue;
}
[symbolNames addObject:name];
}
symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hk.order"];
NSString *symbolStr = [symbolNames componentsJoinedByString:@"\n"];
NSData *symbolData = [symbolStr dataUsingEncoding:kCFStringEncodingUTF8];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:symbolData attributes:nil];
NSLog(@"%@", symbolStr); }
复制代码
拿到.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
的函数符号
在Other C Flags
中的配置,仅对Clang
编译器生效。而Swift
使用swiftc
编译器,要想获得swift
函数符号,需要对Other Swift Flags
进行配置:
-
和
Clang
的配置参数略有出入; -
添加
-sanitize-coverage=func
、-sanitize=undefined
两项。
创建SwiftTest.swift
文件,写入测试代码:
import Foundation class SwiftTest: NSObject {
@objc class func swiftTest1() {
}
@objc class func swiftTest2(){
}
}
复制代码
在ViewController
的load
方法和Block
中分别调用:
+ (void)load {
[SwiftTest swiftTest1];
}
- (void)viewDidLoad {
[super viewDidLoad];
test();
}
void(^block)(void) = ^(void){
[SwiftTest swiftTest2];
};
void test(){
block();
}
复制代码
运行项目,点击屏幕,输出以下内容:
+[ViewController load]
_$s17SanitizerCoverage9SwiftTestC10swiftTest1yyFZTo
_$s17SanitizerCoverage9SwiftTestC10swiftTest1yyFZ
_main
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate setWindow:]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window]
-[ViewController viewDidLoad]
_test
_block_block_invoke
_$s17SanitizerCoverage9SwiftTestC10swiftTest2yyFZTo
_$s17SanitizerCoverage9SwiftTestC10swiftTest2yyFZ
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
复制代码
- 使用
OC
和Swift
混编,成功得到Swift
函数符号.