前言
在iOS-APP启动优化(一)中,我们在文章中提到二进制重排的优化方式,对它的概念做了详细的介绍,同时我们也做了一些简单的应用,即通过编辑order file
来实现二进制的重排,但是问题是我们如何将启动阶段的所有调用符号全覆盖到呐?通过我们这样一行行的手写,一定不是最佳实现方式,而且也不能做到真正的全覆盖。本片文章,我们就通过Clang 插桩 的方式来编写一个完整的,全覆盖的order file
。
Clang 插桩
Clang 插桩?这又是个什么牛逼的东西呐? 简单理解就是一个更加高级的Hook方式,比如我们通过Method swizling方式来实现运行时的hook,而Clang 插桩则是在编译器层面对二进制文件的操作,它会在所有的方法(包括OC,C/C++,Swift)边缘插入一些函数,当方法被调用时,插入的函数也会执行。我们通过实现这些函数,来达到更高级的Hook。 具体来看下官方文档:clang-llvm-SanitizerCoverage-tracing-pcs
LLVM has a simple code coverage instrumentation built in (SanitizerCoverage). It inserts calls to user-defined functions on function-, basic-block-, and edge- levels. Default implementations of those callbacks are provided and implement simple coverage reporting and visualization, however if you need just coverage visualization you may want to use SourceBasedCodeCoverage instead.
使用-fsanitize-coverage=trace-pc-guard
编译器将在每个边缘插入以下代码:
__sanitizer_cov_trace_pc_guard(&guard_variable)
编译器还将插入对模块构造函数的调用:
// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
在每个间接调用中都会插入一个附加...=trace-pc,indirect-calls
标志。__sanitizer_cov_trace_pc_indirect(void *callee)
方法由开发者来实现。
这里我们直接使用文档中的示例代码,添加到我们项目中的任意的一个.m
文件中,同时配置编译器添加-fsanitize-coverage=trace-pc-guard
1.配置编译器添加-fsanitize-coverage=trace-pc-guard
Target -> Build Settings -> Custom Compiler Flags:
此时
Command + B
会报错:这里跟文档说明是一致的,当配置完编译器后,需要开发者实现这2个函数。
- 实现2个插桩函数(这里我们的实现写在
ViewController.m
)
@implementation ViewController
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop){
// Counter for the guards.
static uint64_t N;
// Initialize only once.
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
// Guards should start from 1.
*x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
这段代码直接参考了文档中的示例代码,并做了一些简化。
__sanitizer_cov_trace_pc_guard_init ()
该方法主要是统计了被插桩的符号数量,启动时会执行一次。当没添加一个方法时,数量就会加1。通过start
和stop
读取数据:
INIT: 0x1047b2100 0x1047b21dc
(lldb) x/4gx 0x1047b2100
0x1047b2100: 0x0000000200000001 0x0000000400000003
0x1047b2110: 0x0000000600000005 0x0000000800000007
(lldb) x/4gx 0x1047b21dc-4
0x1047b21d8: 0x0000000000000037 0x0000000104aa4380
0x1047b21e8: 0x0000000000000000 0x00000001047af187
(这里的stop指针-4
,是由于从start
到stop
是一个完整的数据段,而stop
指向的是数据端的末端,通过减4来读取数据段中最后一个元素)。这里是0x37
即十进制55
,当前统计到有55个符号数量。
如果这个方法只能获取到符号数量的话,对我们想要完善order File
来说意义就不大了。
__sanitizer_cov_trace_pc_guard()
: 当有方法调用时,会被该函数Hook;
比如这里我们通过添加一个touchesBegan:withEvent:
来验证一下:
@implementation ViewController
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
char PcDescr[1024];
// __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self func1];
}
//C函数
void test1(){
}
//OC方法
- (void)func1
{
block();
}
//Block
void (^block)(void) = ^{
test1();
};
运行代码,触发touchesBegan:withEvent:
,查看printf()
信息:
guard: 0x10442a14c 1a PC \220\244\337\377
guard: 0x10442a154 1c PC �
guard: 0x10442a158 1d PC �
guard: 0x10442a150 1b PC \SB��
这里来了4次,对应的也就是:
a. touchesBegan:withEvent:
b. func1
c. block()
d. test1()
这也就是说,当触发调用,就会触发__sanitizer_cov_trace_pc_guard
,而且oc方法,C函数,Block 都能被它hook住。通过点点看下test1()
的汇编指令:
这也印证了文档里面所说的,当配置了
-fsanitize-coverage=trace-pc-guard
后,Clang
会在所有的方法边缘插入(生成IR
文件前)__sanitizer_cov_trace_pc_guard
。
那在 __sanitizer_cov_trace_pc_guard()
函数中,能否拿到我们想要的东西呐?
这里,我们要看一下函数内部的一个重要的方法:
void *PC = __builtin_return_address(0);
通过断点,来看下这个方法PC
的输出,执行touchesBegan:withEvent:
:进入断点,输出PC
,同时看下目前的堆栈信息:
这也就是说,
__builtin_return_address(0)
返回了函数的调用地址, 比如
frame #3: 0x0000000102c392ec TestApp`-[ViewController touchesBegan:withEvent:](self=0x0000000104602870, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000000281cd4140) at ViewController.m:48:5
那么,是否可以通过PC
指针来拿到对应的符号信息呐?这里要介绍一个库:<dlfcn.h>
, 这里面有个核心的方法:
* Structure filled in by dladdr().
*/
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;
extern int dladdr(const void *, Dl_info *);
通过PC
指针来获取符号相关信息,这里返回给了一个Dl_info
结构体。下面修改函数,来获取符号信息:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
/*
const char *dli_fname;
void *dli_fbase;
const char *dli_sname;
void *dli_saddr;
*/
Dl_info info;
dladdr(PC, &info);
printf("dli_fname:%s\ndli_fbase:%s\ndli_sname:%s\ndli_saddr:%s\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
// char PcDescr[1024];
// __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
// printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
执行结果:
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:-[ViewController touchesBegan:withEvent:]
dli_saddr:\377C�\321\375{�\251\375��\221(
guard: 0x102bee154 1a PC 4\264�\227\201�<�\320�\200�
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:-[ViewController func1]
dli_saddr:\377\303
guard: 0x102bee15c 1c PC
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:block_block_invoke
dli_saddr:\377\303
guard: 0x102bee160 1d PC
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:test1
dli_saddr:\375{\277\251\375�
guard: 0x102bee158 1b PC �\223\276��
这里很明显,其实对我们有价值的信息是dli_sname
。
INIT: 0x102abe0e0 0x102abe1b8
dli_sname:main
dli_sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate setWindow:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate scene:willConnectToSession:options:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[ViewController viewDidLoad]
dli_sname:-[ViewController func1]
dli_sname:block_block_invoke
dli_sname:test1
dli_sname:-[ViewController loadOtherObject]
dli_sname:-[TestObject toDoSomething]
dli_sname:-[SceneDelegate sceneWillEnterForeground:]
dli_sname:-[SceneDelegate sceneDidBecomeActive:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
到此,我们通过Clang插桩就能获取到启动时的所有符号信息了,剩下的就是将符号写入到Order File
中去,写入的方法就非常多了,可以通过写文件的方式写入,也可以直接控制台输出复制粘贴都可以。需要注意的是,在写入前要对符号做一些简单的处理:
- 方法可能重复调用,这就意味着存在重复的符号,需要去重;
-
order file
写入格式要求,C函数需要在方法前添加下划线_
;
接下来就举例演示一下(这并不是唯一方式)
编写完整Order File
这里先避开一个坑点:
while循环
,for循环
的执行会触发__sanitizer_cov_trace_pc_guard()
的Hook,
这是我们不希望的,这里修改下Custom Compiler Flags
的配置:
将-fsanitize-coverage=trace-pc-guard
修改为-fsanitize-coverage=func,trace-pc-guard
,来屏蔽掉while
或for
的干扰。
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>
#include <libkern/OSAtomic.h>
//符号结构体
typedef struct{
void *pc;
void *next;
}SymbolNode;
//原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return; //该判断会屏蔽掉load方法
void *PC = __builtin_return_address(0);
//创建结构体
SymbolNode *node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC,NULL};
//加入AtomicQueue
OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}
//-fsanitize-coverage=trace-pc-guard
//-fsanitize-coverage=func,trace-pc-guard
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray *nodes = [NSMutableArray array];
while (YES) {
SymbolNode *node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
if(node == NULL) break;
Dl_info info = {0};
dladdr(node->pc, &info);
NSString *sName = @(info.dli_sname);
BOOL isOC = ([sName hasPrefix:@"+["] || [sName hasPrefix:@"-["]) ? YES : NO;
if(isOC){
if(![nodes containsObject:sName]){
[nodes addObject:sName];
}
continue;
}
sName = [@"_" stringByAppendingString:sName];
if(![nodes containsObject:sName]){
[nodes addObject:sName];
}
}
NSMutableArray *newNodes = (NSMutableArray *)[[nodes reverseObjectEnumerator]allObjects];
NSString *nodesStr = [newNodes componentsJoinedByString:@"\n"];
NSLog(@"nodesStr == \n%@",nodesStr);
NSData *strData = [nodesStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager]createFileAtPath:[NSString stringWithFormat:@"%@app.order",NSTemporaryDirectory()] contents:strData attributes:nil];
}
总结
一切的未知,都只是我们认知范围内的未知,其问题本身都是有答案的;而我们要做的就是在有限的时间里,找到它,让我们认知中的未知,变成已知。