分析'/   

  

  作者:字节移动技术――陈奕   

  

  #背景   

  

  从去年9月份开始,在很多用户升级到iOS 14后,很多ImageIO相关栈的Crash问题出现在网上,公司几乎所有的应用都出现了。   

  

  他们都出现了,甚至在一些应用上达到了Top 3 Crash。   

  

  得益于APM平台精准的数据采集机制,以及现场丰富的异常信息,我们收集了详细的Crash日志信息进行分析和解决。   

  

  #问题位置。   

  

  #堆栈信息   

  

  从栈信息来看,ImageIO解析图片信息的时候是Crash,最后调用的所有方法好像都和INameSpacePrefixMap有关,推测。   

  

  崩溃应该与此方法cgimagesourcecopy properties index的实现有关。   

  

     

  分析'/   

  

     

  分析'/   

  

     

  分析'/   

  

  #问题聚合特征。   

  

  机型集中在iOS14以上版本,同时出现在后台。   

  

     

  分析'/   

  

  #分析   

  

  从CrashLog进行初步分析。   

  

     

  分析'/   

  

  图片   

  

  *从堆栈信息来看,这段代码是图片库通过子线程中的cgimagesourcecopy properties index解析imageSource中的图片相关信息,然后发生通配符指针的Crash。   

  

  * cgimagesourcecopy properties index的输入只有一个imagesource,由图片的数据生成,调用栈没有多线程操作,可以排除Crash是imageSource和数据多线程操作导致的。   

  

  *查看堆栈,我们正在解析PNG图片。通过将发布的图片格式改为JPG格式,我们发现震级并没有降低。据推测,崩溃不是由特定的图片格式引起的。   

  

  #拆卸分析   

  

  #拆卸准备。   

  

  *适用于IOs 14.3的iPhone 8。   

  

  * ImageIO系统库:在目录下找到iOS 14.3对应的ImageIO ~/library/developer/xcode/iOS device support。   

  

  * iOS 14.3和iPhone 8上的CrashLog。   

  

  *料斗   

  

  #拆卸   

  

  1.从CrashLog中找到与Crash对应的指令偏移地址2555072。   

p>

  

分析' />   

2、通过 Hopper 打开 ImageIO,跳转到指令偏移地址 2555072

  

Navigate => Go To File Offset 2555072

  

  

分析' />   

3、Crash 对应的指令应该是0000000181b09cc0 ldr x8, [x8, #0x10],可以看到应该是访问 [x8,

  

#0x10]指向的内存出错

  

  

分析' />   

4、查看 Crashlog 中对应寄存器的值,错误地址 far: 0x000021a1ee2fa271,而且 x8 寄存器已经是一个错误的值

  

0x000021a1ee2fa261

  

  

分析' />   

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

  

ImageIO`AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)::sDefaultNameSpacePrefixMap

  

  

分析' />   

  

分析' />   

7、从 Hopper 上看,这个sDefaultNameSpacePrefixMap只在

  

AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)

  

这个函数中调用。可能会在多线程下调用这个函数,而导致这个全局变量的出现 data race 导致了野指针。

  

__ZZN16AdobeXMPCore_IntL31ManageDefaultNameSpacePrefixMapEbE26sDefaultNameSpacePrefixMap: // AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)::sDefaultNameSpacePrefixMap00000001da0ede10 dq 0x0000000000000000 ; DATA XREF=__ZN16AdobeXMPCore_IntL31ManageDefaultNameSpacePrefixMapEb+44, __ZN16AdobeXMPCore_IntL31ManageDefaultNameSpacePrefixMapEb+120, __ZN16AdobeXMPCore_IntL31ManageDefaultNameSpacePrefixMapEb+392

  

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); 这个方法在解析部分图片的时候最终会访问全局变量ImageIO`AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool)::sDefaultNameSpacePrefixMap

  

* 在用户手动杀进程后,这个sDefaultNameSpacePrefixMap被析构,如果这时候在子线程再被访问就可能出现野指针的问题

  

# 修复 ImageIO Crash 方案

  

因为sDefaultNameSpacePrefixMap 是在系统库内部的全局变量,没办法对其进行修改,只能避免在子线程调用

  

CGImageSourceCopyPropertiesAtIndex 方法

  

* 方法一:CGImageSourceCopyPropertiesAtIndex 是用来获取图片的宽高、imageOrientation、动图帧等信息,选择用其他方法来替换,e.g. 宽高用 CGImageRef 来获取

  

* 方法二:将 CGImageSourceCopyPropertiesAtIndex 被调用的线程收敛起来,调用atexit函数来注册一个进程结束回调函数,进程结束的时候将终止线程

  

# 关于字节移动平台团队

  

字节跳动移动平台团队(Client

  

Infrastructure)是大前端基础技术行业领军者,负责整个字节跳动的中国区大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率,支持的产品包括但不限于抖音、今日头条、西瓜视频、火山小视频等,在移动端、Web、Desktop等各终端都有深入研究。

  

就是现在! 客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘! 一起来用技术改变世界 ,感兴趣可以联系邮箱

  

chenxuwei.cxw@bytedance.com ,邮件主题 简历-姓名-求职意向-电话