教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

栏目: IOS · 发布时间: 6年前

内容简介:教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

最近的一个手机 QQ 版本发出去后收到比较多关于 CoreMotion 的 crash 上报,案发现场如下:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

但是看看这个堆栈发现它完全不按照套路出牌啊!

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

乍一看是挂在 CoreMotion 里面的 CLStartStopAdvertisingBeacon 函数,看似是 iBeacon 相关的问题,但实际上是具体函数的符号解不出来,注意 CLStartStopAdvertisingBeacon + 175940 这个巨大的偏移量,一般的函数不可能这么大,所以 这个地址对应的肯定是另外的一个函数!

抛开错误的函数名,看看堆栈的调用顺序,看上去是像是 CoreMotion 在子线程起了一个 Runloop,然后在这个 Runloop 处理来自 IOKit 的回调。

再看看 crash 的 Exception Codes: BUS_ADRALN at 0x006575716572205d ,可以知道这是 访问了一个未对齐的地址 0x006575716572205d 导致的崩溃;同时留意到上报上来的寄存器状态,这个地址正是当前 pcx8 寄存器的值!:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

一般 PC 寄存器保存的是下一条指令的地址,并且要求地址最后的两个比特位是 00 ,这个地址很明显不能满足要求;这种情况通常是因为数据被破坏,导致 读取到的函数指针值异常

有了上面几点发现,我们可以到真机上去探一探究竟。这个上报上来的 crash 是发生在安装了 iOS 10.3.1 (14E304 的一台 64 位机器上,所以我们找来一台符合这两个条件的设备;因为这是发生在系统框架里面,满足这两个条件才能 保证 CoreMotion 的二进制内容和 crash 的机器是一致的 (可以通过 framework 的 UUID 来验证这一点)。

在真机上我们要去找到这几个解错的函数名,而我们的依据就是下图中红色框的地址:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

这些是 crash 所在指令的地址,但这些地址由于 ASLR(地址空间配置随机载入) 的原因是不固定的,所以我们不能在自己的机器上直接用这些地址,而是要利用 crash 时 CoreMotion 框架的载入地址来计算出一个相对的偏移量。通常一个 crash 日志上报上来都会带有一个 Binary Images 信息:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

可以看到当时 CoreMotion 的载入起始地址是 0x199543000 ,然后我们用 crash 堆栈顶部指令的地址 0x00000001995ab62c 减去它得到一个 偏移量 0x6862c0x1995ab62c - 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 ,不过遗憾的是输出结果并没有什么卵用:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

___lldb_unnamed_symbol2303 说明 CoreMotion 把这个符号裁掉了… 不过我们可以在这个地址打个断点 br set -a 0x19863362c ,然后跑进去看一下;进入手机QQ的好友动态页面 (QQ空间),发现这个断点被触发了:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

注意断点位置的上一句 blr x8跳转到 x8 寄存器中的地址,并把 lr 寄存器设置为 pc + 4 的值 ,如果此处 x8 的值出现问题,那么就会出现上报堆栈中的现象: BUS_ADRALN ,并且 x8 和 pc 的值都是这个出错的地址。

然而到这一步后似乎遇到死胡同,函数符号都被裁剪掉,而且这里的回调都是 C 函数,无法从 selector 获取方法名,操作的也不是 OC 对象,唯一可以确定的是进入手机QQ的 好友动态 页面时该函数会被调用。通过查看此页面代码,确实会启动一个 CMMotionManager 然后通过回调监听陀螺仪的回调,但是此段代码并非新增功能,之前版本一直稳定工作,检查后没有发现可疑点。所以进一步推测: 有没有其它业务代码也在使用 CMMotionManager ?

为此,我们查看了上报信息中这些 crash 的发生场景,发现集中发生在两个地方:

TBStoryViewControllerMQZoneVideoRecordViewController ,这两个类都是提供摄像功能 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

运行后果然逮到了,一个业务代码会使用 ![9-runtime-header](/Users/derek/Desktop/CoreMotionCrash/9-runtime-header.png) ,然后 UIAccelerometer 使用了 CMMotionManager :

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

进一步通过 iOSRuntimeHeader 可以确认 UIAccelerometer 有一个 CMMotionManager 作为实例变量:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

看看业务代码,对 UIAccelerometer 的使用也是很简单,似乎没有什么不妥,难道又冤枉了好人?但是仔细看看断点处的堆栈发现一个可疑的地方:调用发生在 Thread 139 ,而 UIAccelerometer 是一个 UIKit 的类, 一般 UIKit 的方法只能在主线程使用 !查看官方文档并没有说明 UIAccelerometer 是否是线程安全,所以我们需要验证一下,如果不是,这里可能是一个突破口。

查看代码发现是通过 -[UIAccelerometer sharedAccelerometer] 获取一个单例对象进行使用,如果这个类是线程安全的,那么 sharedAccelerometer 的实现也应该是线程的,由于这种单例方法一般实现比较简单,所以不妨查看下汇编代码看看实现:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

翻译成ARC代码大概是:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

可以看到整段代码没有任何锁的保护,如果有两个线程同时获取单例,就可能发生 sharedInstance 变量被重复赋值的情况,而且第二次赋值会将第一次构造的对象进行 release,让该对象野掉,而我们知道 UIAccelerometer 有一个 CMMotionManager 的成员变量,它也会随之一起野掉!

同时还发现 -[UIAccelerometer _motionManager] 这个私有方法:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

同样用判断是否为空的形式对 _motionManager 变量进行惰性初始化,同样没有加任何锁的保护,如果多个线程同时调用这个方法也会造成 _motionManager 野掉!

验证是否在多线程使用很简单了, [UIAccelerometer sharedAccelerometer][UIAccelerometer _motionManager] 分别打个断点,然后运行:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

从断点触发的位置可以发现该两个方法会在不同线程进行访问,而且时机非常接近。

最后追溯原因,是之前有同学为了避免 UIAccelerometer 在主线程启动造成卡顿,直接将加速剂的开始和借宿操作通过 dispatch_async 放到了一个 global_queue 里面,都放到了一个 global_queue 里面,属于并发队列, UIAccelerometer 的回调又是在主线程,所以造成了上面的问题:快速开关界面造成多线程同时调用 -[UIAccelerometer sharedAccelerometer]

所以,最终的解决方案是将 UIAccelerometer 的操作全部移动回主线程。

总结

林子大了什么鸟都有,一个大型的应用总会遇到各种奇葩的 BUG,具体解决的手段可能各有不同,但是有一个 科学方法 很值得参考,通过观察收集一个 crash 上报的细节信息,然后提出假设,验证假设;这个过程中辅助以各种工具和经验,最后通过几个这样的迭代定位出问题所在:

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash

如果您觉得我们的内容还不错,就请转发到朋友圈,和小伙伴一起分享吧~

教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash


以上所述就是小编给大家介绍的《教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

神经网络与机器学习(原书第3版)

神经网络与机器学习(原书第3版)

[加] Simon Haykin / 申富饶、徐烨、郑俊、晁静 / 机械工业出版社 / 2011-3 / 79.00元

神经网络是计算智能和机器学习的重要分支,在诸多领域都取得了很大的成功。在众多神经网络著作中,影响最为广泛的是Simon Haykin的《神经网络原理》(第3版更名为《神经网络与机器学习》)。在本书中,作者结合近年来神经网络和机器学习的最新进展,从理论和实际应用出发,全面、系统地介绍了神经网络的基本模型、方法和技术,并将神经网络和机器学习有机地结合在一起。 本书不但注重对数学分析方法和理论的探......一起来看看 《神经网络与机器学习(原书第3版)》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具