一次红米Note 12 诡异的crash排查记录

792 阅读7分钟

起因是社区业务反馈 546 的包在他的测试机上没多久就崩了,困扰很久了,crash backtrace就一行,完全看不出任何问题。

从stack快照大概猜测到与播放器相关,但是播放器so在545到547上版本都是一样的,没有任何变更;但是这个设备545的包不崩,547的包也不崩,546的包却会崩;这就让人很困惑了。同时,我发现同一个包在其他手机上表现正常,只在红米Note 12上能出现这个问题,而且是必现的,只要使用到播放器,不需要多久就能触发崩溃。

获取问题堆栈

由于xcrash监控到的堆栈只有一行,adb bugreport 导出Tomestone 又比较慢,通过分析工具查到的系统进程退出上报的 Tomestone 解析每次都需要使用服务来解析,问题查起来也比较麻烦。

好在之前排查进程退出问题的时候有写过简易的信号监控代码,附带了通过 FP 回溯堆栈的能力,所以我只需要开启对应的功能就能方便的查看crash堆栈信息。开启的方式也很简单,通过摇一摇打开开发者模式,选择性能优化工具,启用 Native信号堆栈打印功能就可以,也可以通过 adb 命令adb shell setprop log.tag.SigCatcher D 来开启,通过过滤日志 tag:achilles 就可以拿到native崩溃堆栈

addr2line 找音视频拿到带符号表的so还原代码位置,期间由于源码看不到以及分支代码看错了导致分析排查到的函数不对,浪费了不少时间。同时,每次崩溃指向的函数地址都不一样,但是都在ffmpeg的ff_read线程上;那么最简单的排查方式就是加日志来定位。由于我本地的播放器环境并没有配置,通过源码加日志编译so的方式显然难度较大;让音视频同学帮忙编译,一次so加日志到so编译完成耗时至少也要十多分钟,同时每次改动完后符号地址都有所改变,验证问题也相当不方便。怎么办?有没有更好的方式来满足我的排查需求呢?答案是有的,我只需要通过hook实现动态日志或者动态trace的能力即可。

Native hook 助力问题排查

首先通过 readelf 命令导出 ffmpeg 这个so的符号表,然后通过inline hook 的能力对可疑函数逐个进行hook,同时在方法的入口和出口处通过ftrace的能力进行跟踪, 通过trace 观测到每次崩溃都是在 avformat_open_input 阶段,

` aarch64-linux-android-readelf -s --wide /Users/admin/Downloads/packages/objs/arm64-v8a/libdewuffmpeg.so > ``ffpeg.txt```

通过一次次追加信息,排查到 func_on_app_event 这个函数上, 试想着如果不让这个函数执行是否能表现正常呢?接着hook func_on_app_event ,将其置空后发现视频竟然能正常播放了,没崩!! 那么问题就确定了,func_on_app_event这个函数地址因为某种原因导致了地址错乱。至此我们知道了crash的原因,但是还不知道什么原因导致的 func_on_app_event 地址错乱。问题还需继续排查。

继续二分,寻找差异

既然546的包会崩,但是545的包不崩,那么首先想到的必然是查看两个包的差异;差异又包括两部分,一个是代码差异,一个是配置差异;找了一会发现实在是太多了,根本查不完。。。

仔细思考了下,如果是代码变更的差异,很难解释java层的代码变更为什么能影响到native层的函数指针,如果是代码逻辑的问题也很难解释为什么其他设备没有问题。所以,有没有可能有什么操作可能导致545的包也能产生崩溃?顺着这个思路,我去看了下两个版本中除了aar不同的影响外,还发现了一个可疑的地方,没错就是 AVC!这个AVC 影响什么呢?只影响apk的versioncode!正好那时候546的灰度包出来了,我用灰度包试了下,发现也不崩!那这个 AVC的可疑度更大了,因为同一个AVC对应的灰度包和测试包的versioncode是不同的。如此,我将重新编译了个545的包,并且将AVC设置为546,神奇的事情发生了,APK它竟然崩了!!amazing!!

image.png

虽然现象难以理解,但对问题排查来说却很有帮助!同时在546的基线上,打了2个包,一个包versionCode是2000010545,不崩;另一个包versionCode是2000010546,崩了;解压对比smali以及so全部都完全一致,唯一的区别只有下面这一行。至此基本可以确定,2000010546这个 version code 会导致 func_on_app_event 函数指针异常。

验证猜想

接下来就是对结论的验证,怎么验证呢?直接打包一个 versionCode =2000010546 和 versionCode = 2000010547 的包即可,查看两个包运行时func_on_app_event 的指针地址指向的内存映射区域,这个通过拿到 proc/pid/smaps 文件就能得到,获取的方式也很简单,之前做内存监控的时候采集过对应数据,只要通过摇一摇打开性能优化工具中的内存监控功能,每分钟都会将最新的 smaps 文件存储在 data/data/package/cache 目录下,只需查看下这个文件内容即可。

崩溃的包,打印出的函数地址空间对应的是 0x14000102,指向的是 dalvik 区域;

不崩的包,打印出的函数地址空间对应的是 0x7485b77d3c,指向的是 libduplayer.so

至此,也能说明 2000010546 这个versioncode 影响到了 func_on_app_event 这个函数指针;而这个func_on_app_event 函数指针也只是指向的一个静态函数地址。理论上它不应该出现在 dalvik 区段,至于为什2000010546会导致这个现象,目前我也不太理解,暂时就先给小米反馈了,或许是触发了HyperOS的什么 bug 吧。

小米针对大部分App在崩溃之后,系统会有一个三方应用异常分析的App记录用户的崩溃数据并进行上报,同时会开启App的小米系统兼容模式。在这个系统模式下,小米系统层会将函数地址进行有策略的打乱,从而导致davlik地址区域指向了duplayer.so的地址,最终引起用户连续崩溃。 此模式的进入标准是根据versionCode作为其中一个key值(为什么我们改versionCode能够恢复的原因)

一点思考

整个排查流程走下来,发现 native crash 问题排查起来工具链路整体支持的会差一点,既不能方便的进行debug也没有查java问题时候的全链路hook能力,需要一个个函数去hook插桩,整体比较费劲;或许是到了建设native so级别的全链路性能、trace、日志动态观测能力了,做到像java方法一样,一个开关一开,程序运行一览无余排查问题将会方便很多。