iOS App启动优化(四):编译期插桩 && 获取方法符号
iOS App启动优化(五):收集符号 && 生成 Order File
相关概念
编译器插桩就是在代码编译期间修改已有的代码或生成新代码。
编译期时,在每一个函数内部二进制源数据添加 hook
代码来实现全局 hook
效果。
编译期插桩会涉及关于 LLVM
的内容,但是对代码具体实现影响不大,想了解相关概念可以看看
怎么找到插桩方法
说白了我们要跟踪到 每个方法的执行,从而获取到启动时 方法执行的顺序,然后再按照这个顺序去编写order file
。
跟踪的具体实现会用到 clang
的 SanitizerCoverage
,这是什么东西??
遇到事情不要慌,先打开文档看一看 ~ clang文档
文档很重要,万一里面有 demo
呢?
代码覆盖率检测工具(SanitizerCoverage)

LLVM
具有内置的简单代码覆盖率检测工具(SanitizerCoverage
)
- 它可以在函数,块、边缘级别插入用户定义函数并提供回调
- 它可以实现了简单的可视化覆盖率报告
通过看守者跟踪 (Tracing PCs with guards)

文档是个好东西~里面就有 example
。

没明白怎么用?问题不大~
具体实现
添加设置
Target -> Build Setting -> Custom Complier Flags -> Other C Flags
添加 -fsanitize-coverage=trace-pc-guard

添加方法
我是在 viewController
里面进行的,把这两个方法复制进去
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
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);
}
执行代码看一看

__sanitizer_cov_trace_pc_guard_init
这是什么...没看懂,还是打个断点看一看吧

start
里面存的是一堆序号,stop
里面会不会也是序号呢?
看看 stop
验证一下,发现里面的并不是序号,这就尴尬了...
思考了一会发现想知道最后一个序号,应该把 stop
地址往前移动了再查看!

向前挪4个字节看看

真找到了,start
和 stop
中间是 01 ~ 0e
,十进制的 1~14
。
这会不会是函数的序号呢?给他安排个函数试试看

0e
变成了 0f
,真的增加了一个。
Block 、 C函数
看到这里可能觉得~ hook
一下都能拿到这有啥了不起。来点新花样
- 添加一个
block
- 添加一个
c函数

0xf + 2 = 0x11
,数量增加了2~验证成功
但是还不够,我万一是混编咋办呢?得添加个 swfit
函数试试。

Swift 混编处理
Target -> Build Setting -> Custom Complier Flags -> Other Swift Flags
添加
-sanitize-coverage=func
-sanitize=undefined

运行代码,输出地址

数量一下子增加到了0x1a
,我猜测生成文件同时生成了不少自带方法,那么我们屏蔽掉刚刚新建的block
、c函数
、oc函数
看看数量是否变成1a - 0x3
即可。

1a - 0x3 = 0x17
验证成功
__sanitizer_cov_trace_pc_guard
上述 guard_init
方法里面可以获取到所有方法的数量,那么肯定也有办法获取方法具体的相关信息。重点就是接下来要分析的__sanitizer_cov_trace_pc_guard
。
添加点击方法,调用一下刚刚添加的方法

运行代码,点击屏幕两次

每次点击最终输出 test
, 以此为界可以看到每次点击会进入 guard方法
8次,在[ViewController touchesBegan:withEvent:]
断点查看一下。

可以看到在调用方法的时候插入了 __sanitizer_cov_trace_pc_guard
方法。
这里其实是方法内部代码执行的第一句,在此之前的代码行是在栈平衡和准备寄存器数据。
获取方法地址
在执行了 __sanitizer_cov_trace_pc_guard
后断点,读取一下 lr
寄存器的内容

这里可以看到lr里面存储的是 [ViewController touchesBegan:withEvent:]
。
这就太神奇了,难道重新执行了一次吗?不是的,如果重新执行就会陷入死循环,很明显代码并没有。
函数嵌套时,跳转函数 bl
会保存下一条指令的地址在 X30
,也就是lr
寄存器。
funcA
调用了 funcB
,在汇编里面会被翻译成 bl + 0x????
, 该指令会首先将下一条汇编指令的地址保存在 x30
寄存器,
然后在跳转到 bl
后面传递的指定地址去执行。
bl
跳转到某个地址的原理就是修改 pc
寄存器的值来指向到要跳转的地址。
而且实际上 funcB
中也会对 x29
、x30
寄存器的值做保护,防止子函数跳转其他函数覆盖 x30
的值。
当 funcB
执行返回指令 ret
时 , 会读取 x30
的地址跳回去, 就返回到上一层函数的下一步。

所以在这里其实是执行了 __sanitizer_cov_trace_pc_guard
后返回到了原来 [ViewController touchesBegan:withEvent:]
的首行。
也就是说,我们可以在 __sanitizer_cov_trace_pc_guard
函数里面拿到原方法的地址!来看看是怎么做到的。

重点在这个 __builtin_return_address
函数,它的作用其实就是去读取 x30
中所存储的要返回时下一条指令的地址。那么这个 PC
就是我们要的方法地址!
获取方法符号
导入头文件
#import <dlfcn.h>
dlfcn.h
中有一个 dladdr()
方法,可以通过函数内部地址找到函数符号。
该方法需要用到结构体Dl_info
,里面还有一些其他信息~

获取到一个个 Dl_info
,打印出来看看。

sname
我们要的方法符号了,而且顺序正是调用函数的顺序!
到这里通过编译器插桩获取方法符号已经成功了,那马上到项目写 order file
就大功告成了?
no~too young too simple~ T.T