手把手教你如何分析 iOS 系统栈 crash

时间:2022-05-05
本文章向大家介绍手把手教你如何分析 iOS 系统栈 crash,主要内容包括第一步、第二步、第三步、第四步、第五步、第六步、第七步、第八步、第九步、第十步、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

先上栈,这个 crash 是我们目前开发产品的 top5 crash

第一步

对于死在 ojbc _ msgSend 的函数(不仅仅是 msgSend, objc_retain 等一切没有创建栈帧的都需要注意),请先检查 crash 上报的寄存器信息

一般来说,lr 肯定不等于第一个栈。 目前的 crash上报功能,丢失了最顶层的栈。因为 objc_msgSend 并没有创建栈帧。

这样,我们就得根据 lr,来计算真实的最后一个栈了。

栈帧介绍

栈帧,保存当前函数返回地址,以及上级栈帧地址。 这样,通过枚举栈帧,即可得到函数的调用栈。


第二步

在模块列表查找,lr 是那个模块的

找到了,计算绝对偏移,找出对应函数地址。(函数绝对偏移 = lr - 模块基址)同样,反过来就可以在本机或者 IDA 查找函数了)

libAVFAudio.dylib`AVAudioSessionPropertyListener(void, unsigned int, unsigned int, void const) + 1796

好了,终于找到调用 objc_msgSend 的点了。


第三步

运行程序,找到野掉的对象到底是个什么

AVAudioSessionRouteDescription (这里需要根据 selector 再次核实上一步骤的调用点是否正确。crash 时 selector 存放在 x1 寄存器, 有时候上报平台会打印出 x-selector detect, 对比下 selector 是否一致,一致则说明上一步得到的地址没有问题)


第四步

这个对象是哪里来的

需要调试神器 lzmalloc 命令(话说,这个命令实在是太好用了。) lzmalloc 为我们自己开发的调试器辅助命令,用于打印对象分配以及释放点的堆栈信息。 下面为 lzmalloc 结果


第五步

到这里为止,首先排查了自己代码内部对于 AVAudioSessionRouteDescription 确定不存在过度释放的问题,不得已,只有逆向了。(最蛋疼的步骤了)

首先确定野掉的 AVAudioSessionRouteDescription 来源于 libAVFAudio.dylib`AVAudioSessionPropertyListener(void, unsigned int, unsigned int, void const) + 1768

而此行是调用函数 -[AVAudioSession privateConfigureRouteDescription:]

而 privateConfigureRouteDescription 从lzmalloc 结论来看,内部是调用 +[AVAudioSessionRouteDescription privateCreateOrConfigure:withRawDescription:]


第六步

首先逆向 +[AVAudioSessionRouteDescription privateCreateOrConfigure:withRawDescription:]

函数逻辑大概如下

test config

if(!change) return orgDes;

else  release(orgDes); alloc newDes。 newDes retain autorelease

从这个逻辑,可以看出来,如果是 new 出来的对象,那是绝对不可能野的。 所以,对象只可能是返回了 orgDes。


第七步

逆向 -[AVAudioSession privateConfigureRouteDescription:]

函数逻辑大概如下

lock
{
   get orgDes
   newdes = call privateCreateOrConfigure:withRawDescription:
   return newdes
}

发现问题了吗? 如果 newdes = orgdes 呢。 而函数返回后,刚好另一个线程执行了 privateCreateOrConfigure:withRawDescription: 而这个时候,config 又恰好变动呢。 orgDes 会被释放!! 哎,这个锁算是白加了。


第八步

问题原因可能猜到了。但是如何修改呢?

hook -[AVAudioSession privateConfigureRouteDescription:] 内部调用原函数之后加上 retain autorelease? 似乎挺理想,但是仔细想想,还是没什么用啊,照样阻止不了其他线程 privateCreateOrConfigure:withRawDescription:的调用。当然这个可以很大降低概率,因为间隔代码很少。

so,换种思路,根据之前动态调试的结果 privateCreateOrConfigure:withRawDescription: 触发时机,有两个,一个是系统耳机插拔通知的时候,另一个就是我们自己调用 audiosession.currentroute 的时候。 而系统通知只在 audio 线程调用。所以呢,既然如此,那我们自己干脆不调用了,在系统通知的时候,在回调里面保存最新的。 当需要访问 audiosession.currentroute 直接返回我们保存的值。 这样,冲突不就没了


第九步

修改外发 很幸运,已经消灭了这个问题。


第十步

总结下

最近发现不少苹果的内存问题。 不知道为什么苹果自己代码很多都不使用 arc,也许这样做很 cool!!

不过,连苹果这么牛这么自信的开发,都弄出了这么多难缠的问题。我们还是不要向他学习,老老实实的用好 ARC 吧。


本文系腾讯Bugly独家内容,转载请在文章开头显眼处注明注明作者和出处“腾讯Bugly(http://bugly.qq.com)”

腾讯Bugly 最专业的质量跟踪平台

精神哥、小萝莉,为您定期分享应用崩溃解决方案