iOS优化 - Clang静态插桩实践

803 阅读4分钟

实践clang静态插桩的机制和原理

  • 新建一个工程,在setting中搜索 Other C Flags,添加-fsanitize-coverage=trace-pc-guard
  • 在ViewController.m中添加hook代码
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;  // Duplicate the guard check.

  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);
}
  • 运行工程,查看日志输出,INIT 后面打印的两个指针地址叫 start 和 stop . 那么我们打个断点,使用命令 x + 内存地址 查看内存中存储的内容, x 就是 memory read 的简写
INIT: 0x100fd4e00 0x100fd4e18
(lldb) x 0x100fd4e00
0x100fd4e00: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x100fd4e10: 05 00 00 00 06 00 00 00 88 03 34 01 01 00 00 00  ..........4.....
(lldb) x 0x100fd4e18
0x100fd4e18: 88 03 34 01 01 00 00 00 00 00 00 00 00 00 00 00  ..4.............
0x100fd4e28: bc 35 fd 00 01 00 00 00 00 00 00 00 00 00 00 00  .5..............

查看start 到 stop这个内存地址存储的信息,可以发现序号01 - 06

  • 下面再添加一个方法- (void)methodOne
INIT: 0x104678e40 0x104678e5c
(lldb) x 0x104678e40
0x104678e40: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x104678e50: 05 00 00 00 06 00 00 00 07 00 00 00 00 00 00 00  ................
(lldb) x 0x104678e5c
0x104678e5c: 00 00 00 00 88 83 80 04 01 00 00 00 00 00 00 00  ................
0x104678e6c: 00 00 00 00 bd 75 67 04 01 00 00 00 00 00 00 00  .....ug.........

发现序号变成 01-07

  • 再添加一个方法- (void)methodTwo
INIT: 0x10424ce58 0x10424ce78
(lldb) x 0x10424ce58
0x10424ce58: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x10424ce68: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb) x 0x10424ce78
0x10424ce78: 88 c3 30 04 01 00 00 00 00 00 00 00 00 00 00 00  ..0.............
0x10424ce88: bf b5 24 04 01 00 00 00 00 00 00 00 00 00 00 00  ..$.............

序号变成 01-08,那么可以得到一个结论 , 这个内存区间保存的就是工程中所有函数符号的个数 .

题外话:由此可以联想到,我们是不是可以通过插桩法得到工程中所有符号的名称?这个以后再去论述。
  • 下面在页面上添加一个UIButton,点击事件调用methodOne,发现每点击一次就会先打印一次guard:,然后在执行methodOne方法,有点类似业务中使用的埋点统计效果;
  • 在点击事件触发方法添加一个断点看下汇编信息: 通过汇编代码发现,在调用每个函数前,先去调用__sanitizer_cov_trace_pc_guard 这个插桩函数;

总结

静态插桩实际上是在编译期就在每一个函数内部二进制源数据添加 hook 代码 ( 我们添加的 __sanitizer_cov_trace_pc_guard 函数 ) 来实现全局的方法 hook 的效果 .

同样也可以使用Hopper工具查看mach-o的二进制文件,将工程生成的.app拖拽到Hopper中,界面大概如此:

或者使用MachOView也可以看到,在_TEXT段的_text中可以看到主程序代码的汇编代码:

可以自己去查看,每个函数在编译期间都会被插入桩函数;

通过二进制文件可以看出,函数内部一开始就添加了调用插桩函数,所以称之为静态插桩

附上本文中涉及的demo: ClangDemo

参考文献: juejin.cn/post/684490…