教你 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
运行后果然逮到了,一个业务代码会使用 ![9-runtime-header](/Users/derek/Desktop/CoreMotionCrash/9-runtime-header.png)
,然后 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 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!
教你 Debug 的正确姿势——记一次 CoreMotion 的 Crash的更多相关文章
- Golang错误和异常处理的正确姿势
Golang错误和异常处理的正确姿势 错误和异常是两个不同的概念,非常容易混淆.很多程序员习惯将一切非正常情况都看做错误,而不区分错误和异常,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误.从 ...
- 开发函数计算的正确姿势 —— 使用 Fun Local 本地运行与调试
前言 首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传.函数计算 ...
- Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)
笔者所在的趣店 FED 早在去年 10 月份就已全面使用 Taro 框架开发小程序(当时版本为 1.1.0-beta.4),至今也上线了 2 个微信小程序.2 个支付宝小程序. 之所以选用 Taro, ...
- 调用 TBrowseForFolder 的正确姿势
[教程]调用 TBrowseForFolder 的正确姿势 2017-08-22 • C++ Builder.Delphi.教程 • 暂无评论 • swish •浏览 562 次 TBrowseFor ...
- GitHub 热点速览 Vol.18:刷 LeetCode 的正确姿势
作者:HelloGitHub-小鱼干 摘要:找对路子,事半功倍,正如本周 GitHub Trending #刷 LeetCode# 主题想表达的那般,正确的学习姿势方能让人走得更远,走进大厂
- 用 JavaScript 刷 LeetCode 的正确姿势【进阶】
之前写了篇文章 用JavaScript刷LeetCode的正确姿势,简单总结一些用 JavaScript 刷力扣的基本调试技巧.最近又刷了点题,总结了些数据结构和算法,希望能对各为 JSer 刷题提供 ...
- 判断是否为gif/png图片的正确姿势
判断是否为gif/png图片的正确姿势 1.在能取到图片后缀的前提下 1 2 3 4 5 6 7 8 9 //假设这是一个网络获取的URL NSString *path = @"http:/ ...
- 在Linux(ubuntu server)上面安装NodeJS的正确姿势
上一篇文章,我介绍了 在Windows中安装NodeJS的正确姿势,这一篇,我们继续来看一下在Linux上面安装和配置NodeJS. 为了保持一致,这里也列举三个方法 第一个方法:通过官网下载安装 h ...
- 程序员取悦女朋友的正确姿势---Tips(iOS美容篇)
前言 女孩子都喜欢用美图工具进行图片美容,近来无事时,特意为某人写了个自定义图片滤镜生成器,安装到手机即可完成自定义滤镜渲染照片.app独一无二,虽简亦繁. JH定律:魔镜:最漂亮的女人是你老婆魔镜: ...
随机推荐
- 用JDBC连接SQL Server2017数据库
用JDBC连接SQL Server2017数据库 2018年03月26日 17:40:47 yasinzhang 阅读数:8346 安装完SQL server2017之后,选择SQL 身份验证登录,可 ...
- BeanUtils.copyProperties缓解代码压力,释放双手
简单描述:之前在写代码的时候,经常把表单提交到后台的对象的参数,通过getter方法取出来,然后,再通过setter方法传递给需要的对象,代码中写了很多get set这种方法,后来听同事说,sprin ...
- Python3——坦克大战
# coding=utf-8 # Version:python3.6.1 __date__ = '2018/9/20 18:51' __author__ = 'Lgsp_Harold' import ...
- git 提示error setting certificate verify locations 解决方案
问题:使用git extension 拉取或者push代码,提示 "C:\Program Files\Git\bin\git.exe" pull --progress " ...
- CF1093
题解: D: 比较显然这个图得是二分图才行 然后每个二分图上的方案是$(2^a+2^b) (a,b是两种颜色的个数)$ E: 我tm就不该先写bitset的 正解和bitset都很好想 因为是个排列, ...
- linux su失败:无法设置用户ID:资源暂时不可用
环境 linux RHEP 7.+ su - user 提示 :无法设置用户ID,资源暂时不可用 检查 cat /etc/security/limits.d/90-nproc.conf * soft ...
- HTML5+CSS3 1
html5标准模板 <!DOCTYPE html> //<!DOCTYPE>标签 向浏览器声明当前文档使用的HTML版本,<!DOCTYPE html>适用于所 ...
- poj 2253 floyd最短路
题目链接 : http://poj.org/problem?id=2253: 思路:这个题主要是理解了意思就行,题目意思是有两只青蛙和若干块石头,现在已知这些东西的坐标,两只青蛙A坐标和青蛙B坐标是第 ...
- 【省选十连测之一】【线段树】【最小生成树之Kruskal】公路建设
目录 题意 输入格式 输出格式 数据范围 思路 代码 题意 有n个点,m条双向道路,其中第条公路的两个端点是u[i],v[i],费用是c[i]. 现在给出q个询问,每次给定一个L和一个R,要求你只能够 ...
- Lightscape
Lightscape是一种先进的光照模拟和可视化设计系统,用于对三维模型进行精确的光照模拟和灵活方便的可视化设计. Lightscape是一个光照渲染软件,它特有的光能传递计算方式和材质属性所产生的独 ...