做者:林蓝东java
最近的一个手机 QQ 版本发出去后收到比较多关于 CoreMotion 的 crash 上报,案发现场以下:git
可是看看这个堆栈发现它彻底不按照套路出牌啊!github
乍一看是挂在 CoreMotion
里面的CLStartStopAdvertisingBeacon
函数,看似是 iBeacon 相关的问题,但其实是具体函数的符号解不出来,注意 CLStartStopAdvertisingBeacon + 175940
这个巨大的偏移量,通常的函数不可能这么大,因此这个地址对应的确定是另外的一个函数!正则表达式
抛开错误的函数名,看看堆栈的调用顺序,看上去是像是 CoreMotion
在子线程起了一个 Runloop,而后在这个 Runloop 处理来自 IOKit 的回调。安全
再看看 crash 的 Exception Codes: BUS_ADRALN at 0x006575716572205d
,能够知道这是访问了一个未对齐的地址 0x006575716572205d
致使的崩溃;同时留意到上报上来的寄存器状态,这个地址正是当前 pc
和 x8
寄存器的值!:微信
通常 PC
寄存器保存的是下一条指令的地址,而且要求地址最后的两个比特位是 00
,这个地址很明显不能知足要求;这种状况一般是由于数据被破坏,致使读取到的函数指针值异常。多线程
有了上面几点发现,咱们能够到真机上去探一探究竟。这个上报上来的 crash 是发生在安装了 iOS 10.3.1 (14E304
的一台 64 位机器上,因此咱们找来一台符合这两个条件的设备;由于这是发生在系统框架里面,知足这两个条件才能保证 CoreMotion
的二进制内容和 crash 的机器是一致的(能够经过 framework 的 UUID 来验证这一点)。并发
在真机上咱们要去找到这几个解错的函数名,而咱们的依据就是下图中红色框的地址:框架
这些是 crash 所在指令的地址,但这些地址因为 ASLR(地址空间配置随机载入) 的缘由是不固定的,因此咱们不能在本身的机器上直接用这些地址,而是要利用 crash 时 CoreMotion
框架的载入地址来计算出一个相对的偏移量。一般一个 crash 日志上报上来都会带有一个Binary Images
信息:dom
能够看到当时 CoreMotion
的载入起始地址是 0x199543000
,而后咱们用 crash 堆栈顶部指令的地址 0x00000001995ab62c
减去它获得一个偏移量 0x6862c( 0x1995ab62c - 0x199543000 = 0x6862c)
。
接下来在真机上编译运行手机QQ,启动后暂停进入 lldb
,执行命令:image list
命令能够获得当前 CoreMotion
的载入地址:
[ 36] 1EE3BF50-5BBD-3BB1-B441-6468626F84D6 0x00000001985cb000 /.../Library/Frameworks/CoreMotion.framework/CoreMotion
咱们把 0x00000001985cb000
加上以前计算出来的偏移量 0x6862c
就得出一个新地址: 0x1985cb000 + 0x6862c = 0x19863362c
这个就是当前机器上对应的地址。有了这个地址咱们能够尝试解下真实的函数名:image lookup -a 0x19863362c
,不过遗憾的是输出结果并无什么卵用:
___lldb_unnamed_symbol2303
说明 CoreMotion
把这个符号裁掉了... 不过咱们能够在这个地址打个断点 br set -a 0x19863362c
,而后跑进去看一下;进入手机QQ的好友动态页面 (QQ空间),发现这个断点被触发了:
注意断点位置的上一句 blr x8
:跳转到 x8 寄存器中的地址,并把 lr 寄存器设置为 pc + 4 的值,若是此处 x8 的值出现问题,那么就会出现上报堆栈中的现象: BUS_ADRALN
,而且 x8 和 pc 的值都是这个出错的地址。
然而到这一步后彷佛遇到死胡同,函数符号都被裁剪掉,并且这里的回调都是 C 函数,没法从 selector 获取方法名,操做的也不是 OC 对象,惟一能够肯定的是进入手机QQ的 好友动态 页面时该函数会被调用。经过查看此页面代码,确实会启动一个 CMMotionManager
而后经过回调监听陀螺仪的回调,可是此段代码并不是新增功能,以前版本一直稳定工做,检查后没有发现可疑点。因此进一步推测:有没有其它业务代码也在使用 CMMotionManager
?
为此,咱们查看了上报信息中这些 crash 的发生场景,发现集中发生在两个地方:
TBStoryViewController
和 MQZoneVideoRecordViewController
,这两个类都是提供摄像功能 ViewController,并且继承自一样的父类,界面展现出来以后确实也会触发以前 crash 的函数;可是找遍这几个类的代码,没有发现直接使用 CMMotionManager
的地方,因而推测是间接使用了 CMMotionManager
。
为了找到谁间接使用了 CMMotionManager
,首先想到的是给全部的 CMMotionManager
方法打上断点,这样一调用就会停住,而后从堆栈上就能看出谁使用了它
(lldb) br set -r "CMMotionManager"
这里使用了 -r
选项来传入一个正则表达式,用于匹配全部 CMMotionManager
的方法,而后打上符号断点。当是最后仍是行不通,由于 CMMotionManager
的几乎全部的符号都被裁掉了,因此打不上.... 这时候 Frida 这个工具就派上用场了,将它提供的 framework 编译到本身的工程里后,咱们就能够在命令行监控到全部的 Objective-C 方法调用记录:
frida-trace -U -f re.frida.Gadget -m "-[CMMotionManager \*]"
经过这个方法发现那两个 controller 一旦展现,就会出现包括 -[CMMotionManager isAccelerometerActive]
的几个调用。那么给-[CMMotionManager isAccelerometerActive]
打个断点看看谁在使用,符号断点咱们打不上,那么咱们就直接打到函数地址上,利用运行时 API 取出该方法的 IMP 值:
(lldb) po method_getImplementation((Method)class_getInstanceMethod([CMMotionManager class], @selector(isAccelerometerActive))) 0x0000000198612918 (lldb) br set -a 0x0000000198612918
运行后果真逮到了,一个业务代码会使用 
,而后 UIAccelerometer
使用了 CMMotionManager
:
进一步经过 iOSRuntimeHeader 能够确认 UIAccelerometer
有一个 CMMotionManager
做为实例变量:
看看业务代码,对 UIAccelerometer
的使用也是很简单,彷佛没有什么不妥,难道又冤枉了好人?可是仔细看看断点处的堆栈发现一个可疑的地方:调用发生在 Thread 139
,而 UIAccelerometer
是一个 UIKit
的类,通常 UIKit 的方法只能在主线程使用!查看官方文档并无说明 UIAccelerometer
是不是线程安全,因此咱们须要验证一下,若是不是,这里多是一个突破口。
查看代码发现是经过 -[UIAccelerometer sharedAccelerometer]
获取一个单例对象进行使用,若是这个类是线程安全的,那么 sharedAccelerometer
的实现也应该是线程的,因为这种单例方法通常实现比较简单,因此不妨查看下汇编代码看看实现:
翻译成ARC代码大概是:
能够看到整段代码没有任何锁的保护,若是有两个线程同时获取单例,就可能发生 sharedInstance
变量被重复赋值的状况,并且第二次赋值会将第一次构造的对象进行 release,让该对象野掉,而咱们知道 UIAccelerometer
有一个 CMMotionManager
的成员变量,它也会随之一块儿野掉!
同时还发现 -[UIAccelerometer _motionManager]
这个私有方法:
一样用判断是否为空的形式对 _motionManager
变量进行惰性初始化,一样没有加任何锁的保护,若是多个线程同时调用这个方法也会形成 _motionManager
野掉!
验证是否在多线程使用很简单了,[UIAccelerometer sharedAccelerometer]
和 [UIAccelerometer _motionManager]
分别打个断点,而后运行:
从断点触发的位置能够发现该两个方法会在不一样线程进行访问,并且时机很是接近。最后追溯缘由,是以前有同窗为了不 UIAccelerometer
在主线程启动形成卡顿,直接将加速剂的开始和借宿操做经过 dispatch_async
放到了一个 global_queue
里面,都放到了一个 global_queue
里面,属于并发队列,UIAccelerometer
的回调又是在主线程,因此形成了上面的问题:快速开关界面形成多线程同时调用 -[UIAccelerometer sharedAccelerometer]
!
因此,最终的解决方案是将 UIAccelerometer
的操做所有移动回主线程。
林子大了什么鸟都有,一个大型的应用总会遇到各类奇葩的 BUG,具体解决的手段可能各有不一样,可是有一个 科学方法 很值得参考,经过观察收集一个 crash 上报的细节信息,而后提出假设,验证假设;这个过程当中辅助以各类工具和经验,最后经过几个这样的迭代定位出问题所在:
更多精彩内容欢迎关注腾讯 Bugly的微信公众帐号:
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!