内容简介:教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash
最近的一个手机 QQ 版本发出去后收到比较多关于 CoreMotion 的 crash 上报,案发现场如下:
但是看看这个堆栈发现它完全不按照套路出牌啊!
乍一看是挂在 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 信息:
可以看到当时 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 上报的细节信息,然后提出假设,验证假设;这个过程中辅助以各种工具和经验,最后通过几个这样的迭代定位出问题所在:
如果您觉得我们的内容还不错,就请转发到朋友圈,和小伙伴一起分享吧~
以上所述就是小编给大家介绍的《教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 强大的姿势感知模型用于姿势不变的人脸识别
- 从姿势到图像——基于人体姿势引导的时尚图像生成算法
- 行人重识别告别辅助姿势信息,港中文、商汤等提出姿势无关的特征提取GAN
- 穿越边界的姿势
- 日志打印的正确姿势!
- 修复缺陷的正确姿势
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Book of CSS3
Peter Gasston / No Starch Press / 2011-5-13 / USD 34.95
CSS3 is the technology behind most of the eye-catching visuals on the Web today, but the official documentation can be dry and hard to follow. Luckily, The Book of CSS3 distills the heady technical la......一起来看看 《The Book of CSS3》 这本书的介绍吧!