作者:杨杨 & 姜豪
部门:电商移动
背景
有赞在基础保障平台的实践中完成了 Crash平台 的建设,但是iOS的崩溃日志未经符号化,排查问题比较困难。为了降低iOS App的crash率,快速排查线上crash,疑难crash的跟踪处理,符号化崩溃日志显得尤为重要!
一、crash日志的收集与分析
1.1 如何收集crash日志
1.手机上直接看,在隐私-分析与改进 -分析数据,可以找到所有崩溃日志,未符号化。
2.连接电脑,通过“音乐”同步到本地 ~/Library/Logs/CrashReporter/MobileDevice/xxx的 iPhone. 缺点:日志没有符号化,需要自己手动符号化
3.连接电脑,打开Xcode-window-Diveces and Simulators。
Xcode会尝试在本地查找符号表文件,自动符号化。
以上3种方法都局限于拿得到设备的情况。
4.查看别人手机上的crash日志 Xcode-Window-Organizer。
这种方式找符号表会有2种途径
- 上传AppStore的时候会让你勾选上传符号表「Include App symbols for your Application…」,如果上传了,苹果自动帮你在云端做解析。
- 如果没有上传,Xcode尝试在本地找符号表文件进行符号化。
缺点:这种方式也只能收集在手机设置中打开了上传crash开关,以及TestFlight用户的crash日志。企业分发或 AdHoc 安装,需要自行获取崩溃日志。信息不全,线程信息不够。
5.自己收集crash日志,比如接入KSCrash、plcrashreporter等,但是要自己做符号化。
1.2 crash日志的结构
日志可以分成4个部分,基本信息,崩溃的原因,所有线程调用,Binary Images (二进制文件列表)。
1.2.1 基本信息
1.2.2 崩溃原因
线程
Binary Images
二、如何进行crash日志符号化
crash日志符号化通常是通过 atos
和 symbolicatecrash
这两个工具来完成。
2.1 atos
atos
是苹果提供的符号化工具,在Mac OS系统下默认安装,他的缺点是只能一个地址一个地址逐个翻译。我们看下这个工具的使用说明:
使用方法:
代码语言:javascript复制atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
需要传入这几个信息:arch 架构、dSYM路径、binary image 载入内存的初始地址、崩溃的地址。
参数内容可以从crash日志中取得,如下图所示:
example
代码语言:javascript复制$ atos -arch arm64 -o TheElements.App.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc
-[AtomicElementViewController myTransitionDidStop:finished:context:]
2.2 symbolicatecrash
symbolicatecrash
是 Xcode
自带的一个程序,他是对 atos
的封装,可以翻译整个crash文件,有赞就是选择这个工具来进行 crash
符号化的。
具体的路径可以通过以下命令搜索出来:
代码语言:javascript复制find /Applications/Xcode.App -name symbolicatecrash -type f
代码语言:javascript复制
使用方法:
代码语言:javascript复制export DEVELOPER_DIR="/Applications/Xcode.App/Contents/Developer"
<path of symbolicatecrash>/symbolicatecrash <Path to dSYM file crash log>
例子:
symbolicatecrash log.crash > result.log
// dSYM可以跟多个
symbolicatecrash log.crash -d TheElement.App.dSYM >result.log
下文会对此工具做一个详细的原理分析。
三、symbolicatecrash符号化原理分析
通过网上找的教程来看,一般是把对应版本的crash日志,dSYM文件,App文件都放进一个目录,然后执行一下命令来进行符号化:
代码语言:javascript复制symbolicatecrash log.crash -d TheElement.App.dSYM >result.log
但是我有几个疑问:
- 如果App打包出来多个dSYM怎么办?
- 发现把目录中的App文件删了,dSYM删了(源文件还在),执行命令的时候也没传他们,竟然也可以符号化,这怎么做到的?
- 怎么样知道crash日志,dSYM,App是正确的,可以正确做符号化,如果发现某个crash日志没有被正确符号化,怎么查这个问题?
- 把dSYM丢了,相同代码再去编译一次把dSYM拿出来可以用吗?
- 我们执行完后发现系统库也都符号化了,系统的dSYM在哪里,难道已经包含在App的dSYM中吗?
- 崩溃日志最下面的Binary Images是干嘛的?
针对以上这些问题,我们来做下源码分析一探究竟。
3.1 symbolicatecrash 源码分析
官方没有开源,但是网上有类似的实现,是用perl实现的一个脚本。
首先,一个基本原则是需要确保你的电脑上有每个 image
对应的 uuid
的符号表文件,这样crash文件才能被正确解析和符号化出来。
然后我们看下符号化一个crash文件的流程:
3.1.1 解析所有的Binary Image
代码语言:javascript复制这是crash日志中的Binary Image格式
0x1cd997000 - 0x1cea7bfff UIKitCore arm64 <40a93e939f8635c1905c7b947c7c2305> /System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore
转换为如下格式
'UIKitCore' =>
{
'extent' => '0x1cea7bfff',
'plus' => '',
'bundlename' => 'UIKitCore',
'uuid' => '40a93e939f8635c1905c7b947c7c2305',
'base' => '0x1cd997000',
'path' => '/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore',
'arch' => 'arm64',
'nextID' => ''
}
把每一个Binary Image都存储为以上形式的对象。
Binary Image的作用是建立UIKitCore与uuid的关系,当需要符号化一个UIKitCore的地址时,会找到对应的uuid,并从文件系统中查找到这个符号表。这也解释了上面第6个问题。
3.1.2 解析所有线程
代码语言:javascript复制8 TheElement 0x00000001044dcfc0 0x104058000 4739008
转换为如下格式
'0x00000001044dcfc0 0x104058000 4739008' =>
{
'raw_address' => '0x00000001044dcfc0',
'bundle' => 'TheElement',
'address' => '0x00000001044dcfc0'
}
把所有堆栈存储为以上形式的对象。
3.1.3 翻译Last Exception Backtrace
代码语言:javascript复制这是crash日志中的Last Exception Backtrace
Last Exception Backtrace:
(0x1a1a9127c 0x1a0c6b9f8 0x1a19adab8 0x1a1a96ac4 0x1a1a9875c 0x10566d498 0x10423ab84 0x1ce255040 0x1cdcfe1c8 0x1cdcfe4e8 0x1cdcfd554 0x1ce28c304 0x1ce28d52c 0x1ce26d59c 0x10437fd20 0x1ce333714 0x1ce335e40 0x1ce32f070 0x1a1a23018 0x1a1a22f98 0x1a1a22880 0x1a1a1d7bc 0x1a1a1d0b0 0x1a3c1d79c 0x1ce253978 0x104283158 0x1a14e28e0)
翻译为:
0 libsystem_kernel.dylib 0x00000001a162e0dc 0x1a160b000 143580
1 libsystem_pthread.dylib 0x00000001a16a7094 0x1a16a5000 8340
2 libsystem_c.dylib 0x00000001a1587f4c 0x1a152d000 372556
3 libsystem_c.dylib 0x00000001a1587eb4 0x1a152d000 372404
4 libc abi.dylib 0x00000001a0c54788 0x1a0c53000 6024
5 libc abi.dylib 0x00000001a0c54934 0x1a0c53000 6452
6 libobjc.A.dylib 0x00000001a0c6be00 0x1a0c66000 24064
7 TheElement 0x0000000104babb18 0x104058000 11877144
8 TheElement 0x00000001044dcfc0 0x104058000 4739008
9 libc abi.dylib 0x00000001a0c60838 0x1a0c53000 55352
10 libc abi.dylib 0x00000001a0c60434 0x1a0c53000 54324
11 libobjc.A.dylib 0x00000001a0c6bbc8 0x1a0c66000 23496
12 CoreFoundation 0x00000001a1a1d11c 0x1a1979000 672028
13 GraphicsServices 0x00000001a3c1d79c 0x1a3c13000 42908
14 UIKitCore 0x00000001ce253978 0x1cd997000 9161080
15 TheElement 0x0000000104283158 0x104058000 2273624
16 libdyld.dylib 0x00000001a14e28e0 0x1a14e1000 6368
这里为什么可以翻译,因为第一步已经把所有Binary Image存储起来,上面的每一个地址,都可以找到对应的Binary Image,从而获得Binary Image的名称,基地址,以及偏移量。
3.1.4 删除不需要的image
因为crash日志把App用到的所有Binary Image都列举出来了,而崩溃堆栈中只用到了一小部分,所以这里把没有用到的Binary Image删除。后续要遍历所有images,去找到每个二进制对应的dSYM,这样做提高了效率。
3.1.5 查找Binary Image的符号表
符号表的类型
- App编译出来的dSYM ( 一般输入命令时指定在哪里,如果没有会自动去查找)
- 系统库的符号表 (自动查找),这也解释了第五个问题,系统符号表和APP符号表是分开的。在 ~/Library/Developer/Xcode/iOS DeviceSupport/os/Symbols 这个路径再拼上image中的path,就是完整路径 比如 ~/Library/Developer/Xcode/iOS DeviceSupport/os/Symbols/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore
- 从search path中找 (包括命令行输入的几个目录 和 系统符号表所在目录)
- mdfind搜索uuid相同的符号表,这就解释了上面第1个和第2个问题,会使用uuid去查找,所以命令行中不传也没关系。
- 如果还没找到 返回空 并删除这个image,与这个image相关的都不能被符号化
判断匹配的条件
- lipo -info 判断架构是否一致
- otool 命令打出来macho信息,找到uuid 并 判断是否一致,这解答了上面第3个和第4个问题,只有uuid相同,才可以被符号化出来。相同代码重新打一个包出来也不能符号化,因为uuid不同。
3.1.6 执行atos进行符号化
- 遍历所有线程
- 取到每一条的bundle 还有地址 在images中找到符号表路径
- 执行命令 并记录符号化后的内容
'0x00000001044dcfc0 0x104058000 4739008' =>
{
'symbolled' => 'CPPExceptionTerminate() (SentryCrashMonitor_CPPException.cpp:179)',
'raw_address' => '0x00000001044dcfc0',
'bundle' => 'TheElement',
'address' => '0x00000001044dcfc0'
}
3.1.7 字符串替换 生成最终的报告
逐行开始替换
比如将'0x00000001044dcfc0 0x104058000 4739008'替换为'CPPExceptionTerminate() (SentryCrashMonitor_CPPException.cpp:179)'
四、有赞符号化方案
通过上面的原理分析,我们基本掌握了 crash
符号化的步骤,下面介绍下我们有赞是如何做符号化的。
4.1 dSYM符号表保存
首先,进行符号化必不可少的一个文件就是 dSYM
符号表,我们需要保存每次正式发布的App版本对应的符号表文件。如下图所示:
- 打包机(gitlab runner):有赞目前有自己的持续构建平台
MBD
,业务方在MBD
上发起打包构建任务后系统会根据算法分配到不同的打包机上。更多关于有赞移动CI/CD
我们在之前做过一次技术沙龙,详细内容见这里。 - 项目打包完成后会执行一个保存符号表的脚本,会保存符号表到本地,并且上传到云端做备份。备份完成后调用MBD接口,上报符号表uuid,bundleId,版本号,build号,打包机唯一标识。
- 由于有多台打包机导致每次打包产出的符号表分布在不同的打包机上,我们需要建立dSYM文件与打包机的关系。第一步中的保存符号表脚本会上报信息到MBD,MBD把dSYM符号表uuid和打包机唯一标识做一个映射关系。
- 当发生一个crash时,crash日志中包含符号表uuid,通过uuid查表,就能定位到执行构建的打包机。
4.2 crash上报
dSYM符号表已经保存下来了,接下来就是crash的上报和解析,crash上报大致流程见下图:
- crash信息通过SDK上报到埋点平台,我们通过Flink监听到crash信息的上报,并把它写入数据库。
- Flink是实时计算平台提供的用来实时消费上报的数据的程序,支持大并发量的数据。
更多关于crash平台的建设我们近期也发表过一篇文章,详情见 这里。
4.3 crash文件符号化
步骤二中已经上报了crash信息并展示在了我们的内部平台中,接下来我们需要对此crash文件结合对应的dSYM进行符号化解析,具体流程如下:
- 在 Crash前端页面,点击符号化按钮会发起 MBD 的一次符号化构建,并将 crash 的信息传递给 MBD。
- MBD把crash的uuid拿出来,根据uuid去查 dSYM文件所在的 打包机,并把任务给到这个打包机。
- 打包机运行脚本,这个脚本的作用是使用symbolicatecrash程序符号化crash日志,并把符号化后的结果通知到MBD。
- MBD 把符号化结果写入数据库,并通知Crash后端。
- Crash前端页面收到通知后刷新页面,展示符号化后的结果。
至此,我们完成了crash文件的符号化解析工作,但是使用过程中暴露出了一些问题:
- 目前每次打包都会产生dSYM文件并直接保存在打包机上,MBD每天的打包任务有很多,导致占用空间浪费资源。我们计划只维护符号表的cdn链接,用到时再去下载符号表。
- 这种方案下线一台打包机后,会造成一部分crash日志无法符号化,目前我们正在优化,计划统一把符号表放到一台打包机上,这样就能解决这个问题。
- 系统符号表的维护也是一个问题,我们需要在每台打包机上都要加上系统符号表,而且每次苹果发布新版都需要拿新的系统符号表过来,维护起来挺麻烦的。目前的解决方案是人工放到打包机上。
总结
至此,我们了解了如何收集crash日志,明白了crash日志中每个部分的意思,符号化的工具,以及如何对crash日志进行符号化。已经可以解答出来上面提出的问题,对符号化的原理有了非常清晰的认识。
我们的符号化方案对于有赞多台打包机环境而言,非常合适,下线一台或者新增一台打包机,可以无缝支持。另外,整套方案非常轻量,能够快速集成符号化功能,符号化链路清晰。
Crash平台拥有符号化crash日志的能力后,极大的提高了大家排查、解决线上问题的效率,提升了App的稳定性。