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

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

环境信息

Xcode 9.0


正文

简介

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

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

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

Crash 日志生成步骤

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. 企业分发或 AdHoc 安装,需要自行获取崩溃日志。
  2. 上传 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 的调用主要分为两步:

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

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

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

  2. 根据 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
  3. 将 UUID 转为 8-4-4-4-12 (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) 格式,并且全部大写。

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

    mdfind "com_apple_xcode_dsym_uuids == <UUID>"

    # 例:
    mdfind "com_apple_xcode_dsym_uuids == C928F353-B3E7-35C6-92DD-3A8BA62DB772"
  5. 如果想要确认找到的符号表是否正确,可以输出符号表的 UUID:

    xcrun dwarfdump --uuid xxx.app.dSYM/Contents/Resources/DWARF/Resources/MyApp
  6. 调用 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

相比自行调用 mdfindatos 等命令,还有一种更简便的方式,即直接调用 Xcode 提供的 symbolicatecrash。该命令位于: /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash(之前路径写错了,多谢评论的同学指出 😞),由 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 上出现,解决方案有两种:

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

最后

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

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

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