iOS Crash 治理:淘宝VisionKitCore 问题修复
本文通过逆向系统,阅读汇编指令,逐步找到源码,定位到了 iOS 16.0.<iOS 16.2 WKWebView 的系统bug 。同时苹果已经在新版本修复了 Bug,对于巨大的存量用户,仍旧会造成日均 Crash pv 1200+ uv 1000+, 最终通过 Hook 系统行为,规避此 Bug。在手机淘宝双 11 版本中已经彻底修复,Crash 跌 0。
背景
手机淘宝的 Crash 率(Crash+Abort)维持在了 x% 左右一两年的时间了,今年组织又提出了更高的要求,努力把 Crash 再降一降,
我也参与到了其中,我在其中负责几个疑难杂症,有幸定位解决了一些操作系统的 Bug。本文将Crash在 VisionKitCore 的系统 Bug
调研过程以及解决方案记录一下。
Crash 信息
堆栈特征:
Noteable Address 特征:
额外信息:(观察到都是图文详情)
PS 有水印不方便透出, 额外信息为 改造 KSCrash 附带的当前页面信息。
版本特征:
crash 占比:有堆栈 Crash 第三名
以上简单信息已经可以佐证,首先这大概率是一个操作系统 Bug,
并且由于前期念纪大佬治理了较多业务堆栈问题,这个疑难杂症已经登上了 Crash(有堆栈)的排行榜 Top 3 了,必须要投入解决了。
排查定位
先在苹果论坛搜索了下这个 Crash 堆栈,发现果然有人反馈过这个 Crash。
发现去年苹果论坛有人反馈是因为在webview 长按复制图片的逻辑中触发了这个 bug,有位用户反馈了,禁用掉这个 WKWebview 长按手势就可以规避掉这个 Crash(其实不行)。基于以上信息进行测试,并且从 平台找到一个用户访问的图文详情尝试寻找堆栈。
1 | WKWebView *webview = [[WKWebView alloc] initWithFrame:self.view.bounds]; |
论坛用户描述的是:禁用长按就不会 crash,但是我测试下来,禁用长按只会让 wkwebview 不创建选择框,但是还是会走创建图片的逻辑,同时手机淘宝的 WebView 容器禁用掉了默认的长按选择框,只实现了一个保存图片的功能,因此这个帖子的解决办法并不能解决手机淘宝的bug。
刚好今年系统性学习了下 Arm 64 汇编,刚好锻炼下新掌握的知识,从底层找下
Bug、简要堆栈。
1 |
|
▐ 分析关键函数汇编指令
函数调用栈为:
1 |
|
基础知识:
针对本文,只需要了解到,
x0..x7 是函数调用时传递参数使用到的通用寄存器,分别为第 1 个 到 第 7 个标量参数
v0-v8 是128位浮点计数器,d0-d7 只取 低 8 字节浮点数,用于传递第 1 个 到 第 7 个浮点数参数
x29 为 fp 寄存器,指向栈底
x30 为 lr 寄存器,记录函数调用返回地址
符号化,必须要选择与出现问题的操作系统一样的版本幸好万瑜 老师手里有一台 iOS 16.1.1 的手机。
libSystem_platform _platform_memmove 分析
1 |
|
通过分析,可以看到 __platform_memmove,的代码是一个较为常见的 memove 或者 memcopy的实现,有一些首尾重叠校验,最终 Crash 的时候 发现 X1 寄存器的内存地址指向了一块数据,这快数据出现了异常。继续往看。
- _CGDataProviderCreateWithCopyOfData
这里发现 _CGDataProviderCreateWithCopyOfData 地址跳转的是 _create_protected_copy,(⊙o⊙)…
神奇的是 Crash 堆栈里面并没有这个函数调用栈。并且_create_protected_copy 也没有找到任何关于 _platform_memmove 的 b、br、bl 调用,难道是这堆栈有点问题?
1 | CoreGraphics`create_protected_copy: |
- Arm64 Crash 堆栈解析
最后经过请教了大佬同事,补充了一个知识盲区,x86_64的调用约定里面强制要求函数调用时需要将 pc 的下一行地址(返回地址)入栈,因此只需要遍历栈即可获取正确的函数调用栈。
但 Arm 64 体系结构中使用 LR
寄存器存放函数返回地址,如果当前函数也需要调用其他函数,就需要再 prolog 里面保存 lr 寄存器的地址。这也是大家经常在函数调用栈开始看到的模版代码:
1 |
|
但是由于并不是所有函数都使用栈,这类函数叫 FrameLess 函数。比如 memset. memove memcpy 这类函数通常的逻辑都是通过一个来源地址,每次拷贝一部分数据到寄存器,然后再从寄存器复制到目标地址中,并且地址长度增长到某个长度截止。
同时 Arm64 中还有一类不返回跳转指令,比如 b/br 一般用于桩指令。
在一些尾递归场景中为了省去不必要的返回(当函数发现我调用下一个函数没必要回来)也会直接使用 b 指令来进行优化。其实最常见的就是 msg_send 既用到了尾调用优化,又是 frameless 函数。
当进程 Crash 时,KSCrash 会对函数调用堆栈进行回溯如果函数是
FrameLess函数,规则会有一定细节处理具体来说就是:
- 崩溃当前函数,直接用 pc 地址,获取最后一个函数栈帧,获取起始范围,
- 遍历 上一个函数栈,通过 ldp fp, lr, x29 取出来 lr 计算函数栈
- 递归执行2,当lr执行到0的时候,证明到了 线程启动函数,终止。
代码见 KSStackCursor
但会有个场景 frameless function + b + frameless function crash,导致堆栈看起来丢失。以本文为例,在这个里面丢失了两行堆栈原因是因为:
memmove 是一个尾调用优化,因此再尾调用优化的自身就丢失了,这确实是正常的
platformmemove 是一个 frameless 函数,因此它没有保存栈的逻辑,取出来的栈上的lr其实是_create_protected_copy 的函数栈,因为自己都是无栈的,所以丢失了lr。碰见这种函数可以从 lr 地址里面去看函数地址。
所以本文其实真正的调用堆栈是:
1 | 0 libsystem_platform.dylib 0x00000001fb27a930 _platform_memmove :96 (in libsystem_platform.dylib) |
接着看:
1 |
|
伪代码如下:
1 |
|
伪代码逻辑:
1 | cvpixbuffer = [VKCRemoveBackgroundResult pifbuffer] |
由以上逻辑可以看到系统在 WKWebview 里面长按的逻辑是这样实现的:
WKWebview 跨进程访问了 从BitMap 里面截取了一个图片,并且传递给 VisionKitCore,然后 VisionKit 直接从这个区域获取了 buffer 然后创建了一张图片做一些行为。但是具体为什么 Crash 这时候已经很难排查,因为这个 bitmap 的对象其实是很早创建的,只是在这里消费的时候挂掉了,有可能是因为提前释放,有可能是野指针,有可能是越界了~~ 因此尝试从其他地方找一些蛛丝马迹。
对比下各版本操作系统
既然线上观察到 iOS 16.2 以上就不会出现 Crash了,那可能真的是系统 Bug,并且偷偷摸摸解决了。于是寻找几台高版本的手机进行实验。
**▐ iOS 16.2 **
长按 webview 后,
__vk_cgImageRemoveBackgroundWithDownsizing_block_invoke函数传递过来的 x1 是 nil,而且针对 VKCRemoveBackgroundResult 所有符号打符号断点,发现长按webview时,不会命中任何逻辑。彻底和 iOS 16.1.1 的设备逻辑不一致了。
**▐ iOS 17 **
到了iOS 17 后又不一样了,VisionKitCore-[VKCRemoveBackgroundResult _createCGImageFromBGRAPixelBuffer:cropRect:]:改成了直接调用 visionkit 里面的 vk_cgImageFromPixelBuffer 创建。
1 | VisionKitCore`-[VKCRemoveBackgroundResult _createCGImageFromBGRAPixelBuffer:cropRect:]: |
**▐ iOS 16.1.1 **
blockInvoke 的时候也就是说 x1 一定是有值的,因此会走调用逻辑。
看看这个图片到底有什么用?
看上去绘制了一个低分辨率的缩略图,不知道有啥用。
继续看 :
看起来是回调到了 webkit,那webkit 是开源的,继续看——
找到对应设备存在的Webkit版本号:
看上去做图像识别的,但是还不确定,继续搜谁调用了它,Github目前能直接搜索符号
基本确认是做图像物体识别的,并且有额外判断逻辑,没有 image 就 return。
解决方案
基于前面的原因得到一些初步的结论:这个功能是 iOS 16 新增的Feature,也就是图像识别,在iOS 16中,系统相册也可以长按抠图,同时系统直接给 WKWebview 里面的所有图片都增加了这个功能。
iOS 16.0..<16.2 期间的所有版本都是有隐含 Bug 的。并不是开发者造成的
_memmove. platformmemory 是非常底层常用的 API,不可能是这的问题。
大概率是 WKWebview 使用方式导致的,或者是 VisionKit 抠图能力有 Bug。但是由于多次异步加 XPC 调度已经很难确认。
▐ 第一种解决方案
我突然想到,既然是默认的行为,那是不是去掉这个行为就好了,同时在前面的的调用栈发现,当-[VKCRemoveBackgroundResult createCGImage]创建图片识别时,系统也有判空逻辑,不会出现 Crash 那我不让它返回就好了。
于是我写个 demo 测试下Hook 掉这个行为, 用了下之前去家里的小猫照片。
1 |
|
可以发现,在 hook 后,长按图片不再有抠图功能。
综上猜测,觉得这个方案可行,于是咨询了下详情和容器,他们并未对 WKWebView 的默认行为做额外处理,并不太会影响手机淘宝的业务。于是准备上线。
不过在上线前突然发现, 淘宝里扫一扫和拍立淘有 visionkit 的使用,觉得有风险,又陷入了困境。
▐ Diff 发现
突然想到既然代码是开源,并且只在 iOS 16.0..<iOS16.2 之间的版本有,是不是可以看下系统怎么偷偷摸摸修了bug。果不其然发现了蛛丝马迹,系统在多处
copy 图片的逻辑中都涉及一个图片长度尺寸的变更(但是我在打符号断点的过程中强制修改这个函数的入参,并不能造成同样的Crash)但是经过这个diff,可以更大概率的确认 Bug 来自 WKWebView 而不是 VisionKit。
▐ 第二种解决方案
继续尝试从 WKWebview 排查。长按触发堆栈查找有用信息。
通过阅读代码后发现这是 iOS 16 新增的功能,同时在源码中查找到了是如何添加的手势
突然发现原来在 iOS 16 以前 WKWebView 里面只有一个手势,当长按时,会触发保存图片菜单。
在 iOS 16 以后,WKWebview 添加了两个手势,竞争用户的长按动作。
- 超时逻辑验证
直接添加符号断点-[WKContentView imageAnalysisGestureDidBegin:]
并添加 Command thread return 中断逻辑。发现果然会命中超时逻辑。
结合代码可以看到超时的菜单中没有 copySubject 逻辑。
- 非超时逻辑
抠图识别成功后,具有 CopySubject 菜单。
因此新的方案为 Hook WKWebView 长按手势图片识别能力。
1 | static void hook2(void) { |
▐ 线上观察
由于 Hook 长按手势后会导致 WKWebview 自带的抠图功能和文字 OCR 功能失效,担心有舆情风险。我们选择在手机淘宝安全气垫 SDK 实现此 Hook,并且通过放量修复。我们在 10.28.11 中通过放量来进行观察,发现Crash 从 500+ 跌倒了 67(冷起生效,有时效性问题),可以确认修复有效,并且没有舆情反馈。全量后,经过观察,带有 Hook 方案的手机淘宝 Crash 基本跌 0,至此此 Bug 彻底修复。 日降低 Crash 1200+,影响设备 1000+ 。
总结
稳定性治理是一个长期的事情,由于前期同事的努力使得用户Crash 基本解决,一些操作系统的 Bug 逐步浮出水面,冲上排行榜,起初我并没有信心解决系统的
Bug,但是在定位过程中利用自己学习到的知识抽丝剥茧逐步定位到问题,也让自己对系统 Crash 不在畏惧,同时感谢同事在排查Bug 期间的经验输出和指导。
同时在定位过程中如有疑问或错误,欢迎讨论、指正。