Crash 符号化 1. 基本流程与相关命令

2018-02-27 11:07:53来源:http://saitjr.com/ios/symbolicatecrash-1.html作者:// TODO:人点击

分享

当遇到 crash 的时候,查看日志是个好办法。绝大多数时候,我们会直接从各大崩溃收集平台上查看日志,那么,调用栈是如何被解析出来的呢?本文将会介绍符号表的一些基础理论,在之后的文章中,会深入源码进行讨论。


环境信息

Xcode 9.0


正文


简介

当应用 crash 的时候,系统会生成一份崩溃日志,存储在设备中。日志描述了在崩溃时,应用的运行状态、调用堆栈、所处线程等信息。绝大多数时候,开发者可以直接通过崩溃日志找到原因所在,这也是为什么日志如此重要的原因之一。


当我们从设备中导出日志时,调用栈是一串地址,并不能看懂,此时所需的是对其进行符号化( symbolicate )。如果是低内存导致的 crash,那么是没有调用栈的,这部分应该在收到内存警告的时候进行处理。


所以,符号表的作用就是将日志进行符号化,让看不懂的十六进制地址,转为对应的调用栈,为开发者提供复现步骤和依据。


Crash 日志生成步骤

通过官方文档,来看看生成步骤:



图中给出了 1~9 个步骤:


编译器将源码编译成机器码时,也会同时生成符号表。根据 Xcode build setting 中的 DEBUG_INFOMATION_FORMAT 设置,将调试符号( debug symbols )放入二进制或者符号表( dSYM )中。默认情况下,DEBUG 模式会将 debug symbol 存入二进制,而 RELEASE 模式会生成 dSYM 文件。两种设置根据环境的不同,发挥着各自的作用:不生成 dSYM 会加快编译速度,而生成 dSYM 则可以减少包大小。

二进制文件与符号表通过 build UUID 一一对应,每一次 build 都会生成新的 UUID。也就是说,即使代码完全相同,UUID 也是不同的,对应的符号表也不同。所以,一份符号表只对应一次 build,如果符号表匹配不上,是无法完全符号化的。关于如果获取二进制与符号表的 UUID,可以查看后文中的 dwarfdump 命令。


在打包的时候,Xcode 会将二进制与 dSYM 存储在 Archived 路径中(可以在 Xcode -> Preferences -> Locations -> Archives 下找到对应路径)。如果是上传 TestFlight 或者 AppStore,那请保管好这些 archives。


如果是上传到 iTunesConnect,可以在上传时看到「Include app symbols for your application…」选框(默认勾选)。该选项可以从 TestFlight 测试过程中拿到未符号化的日志,符号化需要在本地进行。


当应用 crash 时,就会生成一份日志,并存储在设备中。


用户可通过两种方式获得日志文件:1. 打开手机设置(Setting) -> 隐私(Privacy) -> 分析(Analytics) -> 分析数据(Analytics Data),列表文件以 AppName_DateTime_DeviceName 命名,可以通过复制 crash 内容获得文件。2. 通过 Xcode -> Window -> Devices -> 选择设备 -> 选择 View Device Logs 选项,可以直接从列表中将需要的文件拖出来进行发送或保存。


从设备上获取到的日志,都是没有符号化的。如果当前 mac 上能找到二进制文件,系统会自动进行符号化。


如果用户勾选了上传日志,或者用户是从 TestFlight 安装的测试版本,则日志会上传到 App Store。


AppStore 对 crash 进行符号化,并对相似的 crash 进行归类。


在 Xcode -> Window -> Organizer -> Crashes 中可查看日志。


以上便是程序从打包到日志手机的整个流程。主要分为两个大的流程:


企业分发或 AdHoc 安装,需要自行获取崩溃日志。
上传 AppStore 或 TestFlight 分发,可以从 Xcode 中(第 9 步),或直接在 iTunesConnect 上找到崩溃日志。
符号化

接下来,先认识一下 crash 日志中的一些基础含义。


判断是否已经符号化

获取到的 crash 有三种符号化程度:完全符号化、部分符号化、未符号化。判断标准是调用堆栈是否完全被符号化。



设备与日志信息

日志大致可分为四个部分,设备与日志的基本信息、崩溃原因、调用栈、二进制。先从头部谈起:



崩溃原因

在日志中的 Exception Type 部分能找到崩溃原因,这部分之后会单独写,这里仅仅来看下都可能会出现哪些字段:



调用栈

接下来认识一下调用栈,其中包含了程序 crash 时的线程状态,以及方法调用逻辑。以下是已经符号化的调用栈信息。



Binary Images

这部分从 Binary Images: 开始,一直到 crash 文件的最后。其中包含了 crash 时,app 所加载的所有库。



符号化

符号化的命令为 atos ,除此之外,还有 Xcode 提供的 symbolicatecrash ,该命令是对 atos 封装。


atos

atos) 即 address to symbol。如果符号表完整,则使用 atos 命令会输出调用栈与行号。并且, atos 支持单行符号化,这可用于 crash 的归类。


以下是「调用栈」 与 「Binary Images」的关系:



atos 的调用主要分为两步:


指定需要符号化的调用栈,二进制的名称在第二列( TheElements ),地址在第三列( 0x0000000100effdc ),Load Address 在第四列( 0x1000e400 )。
在 Binary Images 中查找二进制名称对应的 UUID。

所以整个符号化的流程为:


遍历调用栈的每一行,解析出对应的 Binary Image Name,地址,行号。


根据 Binary Image Name 找到对应的二进制 UUID:



grep --after-context=1000 "Binary Images:" <Path to Crash Report> | grep <Binary Name>
# 例:
grep --after-context=1000 "Binary Images:" ~/Desktop/symbol/log.crash| grep TheElements



将 UUID 转为 8-4-4-4-12 ( XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX ) 格式,并且全部大写。


调用 mdfind 命令,使用 Spotlight 进行全局搜索 UUID 对应的符号表(不包含 <> 符号)。Spotlight 会输出 dSYM 可能存在的位置(可能有多个):



mdfind "com_apple_xcode_dsym_uuids == <UUID>"
# 例:
mdfind "com_apple_xcode_dsym_uuids == C928F353-B3E7-35C6-92DD-3A8BA62DB772"



如果想要确认找到的符号表是否正确,可以输出符号表的 UUID:



xcrun dwarfdump --uuid xxx.app.dSYM/Contents/Resources/DWARF/Resources/MyApp



调用 atos 逐行进行符号化,注意 dSYM 必须制定到准确的二进制,而不是指定到 bundle:



atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
# 例:
atos -arch arm64 -o TheElements.app.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc



经过以上步骤,控制台输出的就是已经符号化的结果了。


symbolicatecrash

相比自行调用 mdfind , atos 等命令,还有一种更简便的方式,即直接调用 Xcode 提供的 symbolicatecrash 。该命令位于: /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash (之前路径写错了,多谢评论的同学指出 :disappointed:),由 perl 编写,里面整合了逐步解析的操作(也可以将命令拷贝出来,直接进行调用)。



export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
<path of symbolicatecrash>/symbolicatecrash <Path to dSYM file crash log>


也就是说, symbolocatecrash 将整个步骤简化为了一行命令:



symbolicatecrash log.crash


如果仅给出 crash log, symbolocatecrash 会调用 mdfind 命令,使用 Spotlight 进行全局查找,当然,也可以自行指定符号表:



symbolicatecrash log.crash -d TheElement.app.dSYM


注意:很多资料上都没有 -d ,而是直接 symbolicatecrash log.crash TheElement.app.dSYM ,这种情况下, symbolicatecrash 并不会补全到二进制路径,即: TheElements.app.dSYM/Contents/Resources/DWARF/TheElements ,所以,与 TheElement 二进制相关的地址是无法符号化的。正确的写法有以下几种:



symbolicatecrash log.crash -d TheElement.app.dSYM
symbolicatecrash log.crash -d TheElement.app.dSYM/Contents/Resources/DWARF/TheElements
symbolicatecrash log.crash TheElement.app.dSYM/Contents/Resources/DWARF/TheElements


关于这部分实现,可以查看 symbolicatecrash 命令中的 getSymbolDirPaths 函数。


一般情况下,还会直接将结果导出为文件:



symbolicatecrash log.crash -d TheElement.app.dSYM -o result.crash


系统库

之前谈到的路径查找都是项目本身的符号表,而对于系统库的符号化,同样也需要系统库符号表。这些符号表路径可以在 crash 文件的 Binary Images 中看到:


如果出现系统库符号化失败的情况,最常见的原因是磁盘下无法找到与 crash 的设备对应的系统符号表,这些符号表文件路径为:



~/Library/Developer/Xcode/iOS DeviceSupport/


假如我的 mac 上只有 10.3.3 (14G60),那么就只能符号化 iOS10.3.3 (14G60) 的系统调用栈,如果 crash 在 iOS9.2 上出现,解决方案有两种:


找一台 iOS9.2 的设备,连接到 mac,等待 Xcode processing symbol。
到各大云盘上搜索别人分享的各个系统版本的符号表,然后拷贝到之前提到的路径下面。
最后

最后,总结一下符号化的部分:


符号化主要分为两种,一种是自行调用 atos 与 mdfind 命令,进行逐行符号化;一种是调用 Xcode 提供的 symbolicatecrash ,直接指定 crash 文件和符号表,进行符号化。
如果出现无法完全符号化的问题,请检查对应的符号表是否存在。
如果存在依然无法符号化,可检查 mdfind 这一步是否出现问题,之前有朋友遇到过 Spotlight 卡死的情况,请重建索引,并且重启 Spotlight。
一般需要手动符号化有以下几种情况:测试人员反馈、公司自行搭建的持续集成平台。其余情况一般是 Xcode 直接进行符号化,或者使用的三方崩溃平台进行符号化。而对于自己搭建的持续集成平台,也就是服务器部分,需要注意要包含所有系统版本的符号表。

最后的最后,多谢 SwiftGG 小金的指导。下一篇来看看 Xcode symbolicatecrash 命令的实现。


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台