iOS App启动优化(四):编译期插桩 && 获取方法符号

7,279 阅读5分钟

iOS App启动优化(一):检测启动时间

iOS App启动优化(二):物理内存和虚拟内存

iOS App启动优化(三):二进制重排

iOS App启动优化(四):编译期插桩 && 获取方法符号

iOS App启动优化(五):收集符号 && 生成 Order File

iOS App启动优化(六):实用党直接看这里

相关概念

编译器插桩就是在代码编译期间修改已有的代码或生成新代码。

编译期时,在每一个函数内部二进制源数据添加 hook 代码来实现全局 hook 效果。

编译期插桩会涉及关于 LLVM 的内容,但是对代码具体实现影响不大,想了解相关概念可以看看

LLVM及编译过程

编译LLVM

怎么找到插桩方法

说白了我们要跟踪到 每个方法的执行,从而获取到启动时 方法执行的顺序,然后再按照这个顺序去编写order file

跟踪的具体实现会用到 clangSanitizerCoverage,这是什么东西??

遇到事情不要慌,先打开文档看一看 ~ 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个字节看看

真找到了,startstop 中间是 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,我猜测生成文件同时生成了不少自带方法,那么我们屏蔽掉刚刚新建的blockc函数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 中也会对 x29x30 寄存器的值做保护,防止子函数跳转其他函数覆盖 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

接下来请看 iOS App启动优化(五):收集符号 && 生成 Order File