基于桥的全量方法 Hook 方案(2) - 全新升级

如果读过我的博客的人可能知道,我在 2017 年曾经研究过当时苹果出的一个新玩意 MainThread Checker,并以此为基础推导了一个基于桥的全量 Hook 方案,基于桥的全量方法Hook方案 - 探究苹果主线程检查实现 。当时简单写了下 ARM64 的方案代码,并放在了 Github 上,不过已废弃。

当时觉得自己研究的还算深入,基于汇编写(其实是复制粘贴)了一大堆的桥,可以针对性 Hook 一个或多个二进制,比如 UIKit 的逻辑,觉得挺屌的。

但是使用中发现了两个巨大的问题:

  • 性能问题。由于我是运行时的方案,没法对二进制产物进行修改(比如编译插桩),因此如果要能达到对二进制所有方法中心重定向的效果,借助了 forwarding 的流程(不是objc_msgForward)。但是这个方案懂的人肯定明白,性能巨慢。

不能上线的方案其实价值都不大。

  • Crash 问题。尽管我通过汇编的方式解决了中心重定向 Hook 方案上对一条继承链重复 Hook 会死循环的 Crash 问题(如果你不理解,可以回到我文章开头所提及的文章了解原因或者查看 Aspects 库中对应的 issue), 但是却出乎意料的引入了系统库新的 Crash,这个我会开一篇新的文章来分析。

因此,当时这个方案我就抛弃了,后续也因为我不怎么搞iOS,就没深入优化。

新的方案

新的方案的起源灵感来自于我隔壁组的同事,手淘架构新生代小天王谢俊逸的启发。他说你用汇编写桥,照理性能不会慢啊,你为啥要走一次 forwarding 的逻辑?

我回顾了下代码,发现原先我为了保留所谓的层级上下文,将类名和方法名构造成了一个唯一标示,然后将这个唯一标示和一个动态生成的函数地址相绑定。然后通过这个不存在的方法名触发 forwarding 流程,然后改写成正确的方法名,从而调用正确的被 HOOK 前的函数。

看不懂的话等我周末整理下代码开源吧。

而整个流程,也是如下两个问题的罪魁祸首。

  • 改了方法名:SEL 的修改之前是为了解决中心重定向相同继承链上的 Hook Crash 问题,但是会导致意想不到的其他 Crash 问题。

  • 性能巨慢:走 fowarding 流程绕了一大圈。

要解决上述这些问题,汇编和桥依然是不可或缺的,但是如何把所有 UIKit 的方法都中心重定向同时又能绕开继承链问题呢还能不修改 SEL 的名称呢?

经过和谢俊逸的讨论,我们发现,我们把原先保存 拼接后 SEL 的逻辑,换成直接保存 HOOK 之前的 函数IMP,然后通过汇编直接跳过去执行 IMP 不就完事了?

思路非常 Nice ! 开工

动手过程

想法有了,因为涉及到汇编,需要非常复杂的操作流程,简单抛砖引玉一下。

_template_page:
sub x12, lr, #0x8
sub x12, x12, #0x4000 // 获取对应数据页的便宜
mov lr, x13   // 获取返回原始调用处

// x8-x18 临时寄存器,里面的值在函数调用后可能被修改,这些寄存器为caller-saved,可以用
ldr x10, [x12] // originIMP

stp lr, x10, [sp, #-16]!
stp x0, x1,  [sp, #-16]!
stp x2, x3,  [sp, #-16]!
stp x4, x5,  [sp, #-16]!
stp x6, x7,  [sp, #-16]!
str x8,      [sp, #-16]!

// 我不用浮点数寄存器,所以我不保存,你们用你们要保存
// 这行是伪代码,意思意思。实际上这个代码是会Crash的。
bl _WZQMainThreadChecker

ldr x8,      [sp], #16
ldp x6, x7,  [sp], #16
ldp x4, x5,  [sp], #16
ldp x2, x3,  [sp], #16
ldp x0, x1,  [sp], #16
ldp lr, x10, [sp], #16

br x10   // 执行原函数

mov x13, lr
bl _template_page;

//// 下面是重复性的一堆代码。

主体上是这么写,但是需要考虑的太多了,今天周二,来不及整完博客了,吹吹逼睡觉。

还有很多东西实现了但是文章中没写,周末再写吧,水一篇博客。

  • 要考虑对齐问题?

  • 为什么可以这么设计桥?

  • 如何保存重要的上下文、寄存器信息等?

结尾

代码写完后我和同事放在手淘里跑了泡,美滋滋,嘻嘻。不崩,还挺顺畅,哈哈,吊打原先的实现。

当然,鉴于本人汇编仅较初级的掌握 ARM64,因此 x86_64 或者 ARM64e(不知道有没有差别) 上的方案近期慢慢等我搞出来吧。

ARM64 上的代码等我周末慢慢整理下开源。