手把手教你 Debug — iOS 14 ImageIO Crash 分析
背景
去年 9 月份开始,许多用户升级到 iOS 14 之后,线上出现很多 ImageIO 相关堆栈的 Crash 问题,而且公司内几乎所有的 APP 上都有出现,在部分 APP上甚至达到了 Top 3 Crash。
得益于 APM 平台精准数据采集机制和丰富的异常信息现场,我们通过收集到详细的 Crash 日志信息进行分析解决。
问题定位
堆栈信息
从堆栈信息看,是在 ImageIO 解析图片信息的时候 Crash ,并且最后调用的方法都是看起来都是和 INameSpacePrefixMap
相关,推测 Crash 应该是和这个方法 CGImageSourceCopyPropertiesAtIndex
的实现有关。
从堆栈信息看,这段代码是图片库在子线程通过
CGImageSourceCopyPropertiesAtIndex
解析imageSource
中的图片相关信息,然后发生了野指针的 Crash。CGImageSourceCopyPropertiesAtIndex
的输入只有一个imageSource
,imageSource
由图片的 data 生成,调用栈并没有多线程操作,可以排除是多线程操作imageSource
、data 导致的 Crash。看堆栈是在解析 PNG 图片,通过将下发的图片格式换成 JPG 格式,发现量级并没有降低。推测 Crash 不是某种特定图片格式引起的。
反汇编分析
反汇编准备
- iOS 14.3 的 iPhone 8
- ImageIO 系统库:~/Library/Developer/Xcode/iOS DeviceSupport目录下找到对应 iOS 14.3 的 ImageIO
- 一份 iOS 14.3、iPhone 8 上发生的 CrashLog
- Hopper
反汇编
1、从 CrashLog 上找到 Crash 对应的指令偏移地址 2555072
2、通过 Hopper 打开 ImageIO,跳转到指令偏移地址 2555072
Navigate => Go To File Offset 2555072
3、Crash 对应的指令应该是0000000181b09cc0 ldr x8, [x8, #0x10]
,可以看到应该是访问 [x8, #0x10]
指向的内存出错
5、向上回溯查看 x8 的来源
0000000181b09cbc ldr x8, [x20]
x8 是存在 x20 指向的内存中(即x8 = *x20
)0000000181b09c98 ldr x20, [x21, #0x8]
x20 又存在[x21, #0x8]
指向的内存中0000000181b09c8c adrp x21, #0x1da0ed000
,0000000181b09c90 add x21, x21, #0xe10
x21 指向的是一个 data 段,推测 x21 应该是一个全局变量,所以,可能是这个全局变量野了,或者是这个全局变量引用的某些内存(x20)野了
6、运行时 debug 查看 x8、x20、x21 对应寄存器的值是什么
- x21 从内存地址的名字看,应该是一个全局的 Map
8、经过在运行时反复调试,这个
AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)
会在多个方法中调用(并且调用时都加了锁,不太可能会出现 data race):
AdobeXMPCore_Int::INameSpacePrefixMap_I::CreateDefaultNameSpacePrefixMap()
AdobeXMPCore_Int::INameSpacePrefixMap_I::InsertInDefaultNameSpacePrefixMap(char const*, unsigned long long, char const*, unsigned long long)
AdobeXMPCore_Int::INameSpacePrefixMap_I::DestroyDefaultNameSapcePrefixMap()
9、在后台线程访问访问全局变量 sDefaultNameSpacePrefixMap
时 Crash,推测可能是用户手动杀进程后,全局变量在主线程已经被析构,后台线程还会继续访问这个全局变量,从而出现野指针访问异常。发现 Crash 日志的主线程堆栈也出现 _exit 的调用,可以确定是全局变量析构导致。
Crash 发生的原因:
在用户手动杀进程后,主线程将这个全局变量析构了,这时候子线程再访问这个全局变量就出现了野指针。
复现问题
尝试在子线程不断调用 CFDictionaryRef CGImageSourceCopyPropertiesAtIndex(CGImageSourceRef isrc, size_t index, CFDictionaryRef options);
,并且手动杀掉进程触发这个 crash
可以证明上述的推理是正确的。
总结
CFDictionaryRef CGImageSourceCopyPropertiesAtIndex(CGImageSourceRef isrc, size_t index, CFDictionaryRef options);
这个方法在解析部分图片的时候最终会访问全局变量
在用户手动杀进程后,这个
sDefaultNameSpacePrefixMap
被析构,如果这时候在子线程再被访问就可能出现野指针的问题修复 ImageIO Crash 方案
因为
sDefaultNameSpacePrefixMap
是在系统库内部的全局变量,没办法对其进行修改,只能避免在子线程调用CGImageSourceCopyPropertiesAtIndex
方法方法一:
CGImageSourceCopyPropertiesAtIndex
是用来获取图片的宽高、imageOrientation
、动图帧等信息,选择用其他方法来替换,e.g. 宽高用CGImageRef
来获取方法二:将
CGImageSourceCopyPropertiesAtIndex
被调用的线程收敛起来,调用atexit函数来注册一个进程结束回调函数,进程结束的时候将终止线程