实践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…