写点什么

有赞 crash 平台符号化实践

  • 2020-09-19
  • 本文字数:5612 字

    阅读完需:约 18 分钟

有赞crash平台符号化实践

背景

有赞在基础保障平台的实践中完成了 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 日志符号化通常是通过 atossymbolicatecrash 这两个工具来完成。

2.1 atos

atos 是苹果提供的符号化工具,在 Mac OS 系统下默认安装,他的缺点是只能一个地址一个地址逐个翻译。我们看下这个工具的使用说明:



使用方法:


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


$ atos -arch arm64 -o TheElements.App.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc  -[AtomicElementViewController myTransitionDidStop:finished:context:]
复制代码

2.2 symbolicatecrash

symbolicatecrashXcode 自带的一个程序,他是对 atos 的封装,可以翻译整个 crash 文件,有赞就是选择这个工具来进行 crash 符号化的。


具体的路径可以通过以下命令搜索出来:


find /Applications/Xcode.App -name symbolicatecrash -type f
复制代码


使用方法:


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 文件都放进一个目录,然后执行一下命令来进行符号化:


symbolicatecrash log.crash -d TheElement.App.dSYM >result.log
复制代码


但是我有几个疑问:


  1. 如果 App 打包出来多个 dSYM 怎么办?

  2. 发现把目录中的 App 文件删了,dSYM 删了(源文件还在),执行命令的时候也没传他们,竟然也可以符号化,这怎么做到的?

  3. 怎么样知道 crash 日志,dSYM,App 是正确的,可以正确做符号化,如果发现某个 crash 日志没有被正确符号化,怎么查这个问题?

  4. 把 dSYM 丢了,相同代码再去编译一次把 dSYM 拿出来可以用吗?

  5. 我们执行完后发现系统库也都符号化了,系统的 dSYM 在哪里,难道已经包含在 App 的 dSYM 中吗?

  6. 崩溃日志最下面的 Binary Images 是干嘛的?


针对以上这些问题,我们来做下源码分析一探究竟。

3.1 symbolicatecrash 源码分析

官方没有开源,但是网上有类似的实现,是用 perl 实现的一个脚本。


首先,一个基本原则是需要确保你的电脑上有每个 image 对应的 uuid 的符号表文件,这样 crash 文件才能被正确解析和符号化出来。


然后我们看下符号化一个 crash 文件的流程:

3.1.1 解析所有的 Binary Image

这是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 解析所有线程

8   TheElement                0x00000001044dcfc0 0x104058000 + 4739008
转换为如下格式
'0x00000001044dcfc0 0x104058000 + 4739008' => { 'raw_address' => '0x00000001044dcfc0', 'bundle' => 'TheElement', 'address' => '0x00000001044dcfc0'}
复制代码


把所有堆栈存储为以上形式的对象。

3.1.3 翻译 Last Exception Backtrace

这是crash日志中的Last Exception BacktraceLast 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 + 1435801 libsystem_pthread.dylib 0x00000001a16a7094 0x1a16a5000 + 83402 libsystem_c.dylib 0x00000001a1587f4c 0x1a152d000 + 3725563 libsystem_c.dylib 0x00000001a1587eb4 0x1a152d000 + 3724044 libc++abi.dylib 0x00000001a0c54788 0x1a0c53000 + 60245 libc++abi.dylib 0x00000001a0c54934 0x1a0c53000 + 64526 libobjc.A.dylib 0x00000001a0c6be00 0x1a0c66000 + 240647 TheElement 0x0000000104babb18 0x104058000 + 118771448 TheElement 0x00000001044dcfc0 0x104058000 + 47390089 libc++abi.dylib 0x00000001a0c60838 0x1a0c53000 + 5535210 libc++abi.dylib 0x00000001a0c60434 0x1a0c53000 + 5432411 libobjc.A.dylib 0x00000001a0c6bbc8 0x1a0c66000 + 2349612 CoreFoundation 0x00000001a1a1d11c 0x1a1979000 + 67202813 GraphicsServices 0x00000001a3c1d79c 0x1a3c13000 + 4290814 UIKitCore 0x00000001ce253978 0x1cd997000 + 916108015 TheElement 0x0000000104283158 0x104058000 + 227362416 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


  1. 从 search path 中找 (包括命令行输入的几个目录 和 系统符号表所在目录)

  2. mdfind 搜索 uuid 相同的符号表,这就解释了上面第 1 个和第 2 个问题,会使用 uuid 去查找,所以命令行中不传也没关系。

  3. 如果还没找到 返回空 并删除这个 image,与这个 image 相关的都不能被符号化

判断匹配的条件
  1. lipo -info 判断架构是否一致

  2. otool 命令打出来 macho 信息,找到 uuid 并 判断是否一致,这解答了上面第 3 个和第 4 个问题,只有 uuid 相同,才可以被符号化出来。相同代码重新打一个包出来也不能符号化,因为 uuid 不同。

3.1.6 执行 atos 进行符号化

  1. 遍历所有线程

  2. 取到每一条的 bundle 还有地址 在 images 中找到符号表路径

  3. 执行命令 并记录符号化后的内容


 '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 版本对应的符号表文件。如下图所示:



  1. 打包机(gitlab runner):有赞目前有自己的持续构建平台 MBD,业务方在 MBD 上发起打包构建任务后系统会根据算法分配到不同的打包机上。更多关于有赞移动 CI/CD 我们在之前做过一次技术沙龙,详细内容见这里

  2. 项目打包完成后会执行一个保存符号表的脚本,会保存符号表到本地,并且上传到云端做备份。备份完成后调用 MBD 接口,上报符号表 uuid,bundleId,版本号,build 号,打包机唯一标识。

  3. 由于有多台打包机导致每次打包产出的符号表分布在不同的打包机上,我们需要建立 dSYM 文件与打包机的关系。第一步中的保存符号表脚本会上报信息到 MBD,MBD 把 dSYM 符号表 uuid 和打包机唯一标识做一个映射关系。

  4. 当发生一个 crash 时,crash 日志中包含符号表 uuid,通过 uuid 查表,就能定位到执行构建的打包机。

4.2 crash 上报

dSYM 符号表已经保存下来了,接下来就是 crash 的上报和解析,crash 上报大致流程见下图:



  1. crash 信息通过 SDK 上报到埋点平台,我们通过 Flink 监听到 crash 信息的上报,并把它写入数据库。

  2. Flink 是实时计算平台提供的用来实时消费上报的数据的程序,支持大并发量的数据。


更多关于 crash 平台的建设我们近期也发表过一篇文章,详情见 这里

4.3 crash 文件符号化

步骤二中已经上报了 crash 信息并展示在了我们的内部平台中,接下来我们需要对此 crash 文件结合对应的 dSYM 进行符号化解析,具体流程如下:



  1. 在 Crash 前端页面,点击符号化按钮会发起 MBD 的一次符号化构建,并将 crash 的信息传递给 MBD。

  2. MBD 把 crash 的 uuid 拿出来,根据 uuid 去查 dSYM 文件所在的 打包机,并把任务给到这个打包机。

  3. 打包机运行脚本,这个脚本的作用是使用 symbolicatecrash 程序符号化 crash 日志,并把符号化后的结果通知到 MBD。

  4. MBD 把符号化结果写入数据库,并通知 Crash 后端。

  5. Crash 前端页面收到通知后刷新页面,展示符号化后的结果。


至此,我们完成了 crash 文件的符号化解析工作,但是使用过程中暴露出了一些问题:


  1. 目前每次打包都会产生 dSYM 文件并直接保存在打包机上,MBD 每天的打包任务有很多,导致占用空间浪费资源。我们计划只维护符号表的 cdn 链接,用到时再去下载符号表。

  2. 这种方案下线一台打包机后,会造成一部分 crash 日志无法符号化,目前我们正在优化,计划统一把符号表放到一台打包机上,这样就能解决这个问题。

  3. 系统符号表的维护也是一个问题,我们需要在每台打包机上都要加上系统符号表,而且每次苹果发布新版都需要拿新的系统符号表过来,维护起来挺麻烦的。目前的解决方案是人工放到打包机上。

总结

至此,我们了解了如何收集 crash 日志,明白了 crash 日志中每个部分的意思,符号化的工具,以及如何对 crash 日志进行符号化。已经可以解答出来上面提出的问题,对符号化的原理有了非常清晰的认识。


我们的符号化方案对于有赞多台打包机环境而言,非常合适,下线一台或者新增一台打包机,可以无缝支持。另外,整套方案非常轻量,能够快速集成符号化功能,符号化链路清晰。


Crash 平台拥有符号化 crash 日志的能力后,极大的提高了大家排查、解决线上问题的效率,提升了 App 的稳定性。


本文转载自公众号有赞 coder(ID:youzan_coder)。


原文链接


有赞crash平台符号化实践


2020-09-19 10:002057

评论 1 条评论

发布
用户头像
方案一般,还有更好的解析方式
2021-08-19 08:45
回复
没有更多了
发现更多内容

模块四作业 - 考试试卷存储方案

张大彪

第四次作业

Geek_9cf7b5

为什么有的系统的事务码BSP_WD_CMPWB看不见Enhance Component这个按钮

Jerry Wang

CRM SAP abap

HBase常见问题

数据社

大数据 HBase 5月日更

实战|教你用Python玩转Mysql

Python研究者

Python MySQL MySQL 运维

模块4 学习总结

TH

架构实战营

SAP CRM 和 Cloud for Customer 的 Document flow API 介绍

Jerry Wang

CRM SAP abap C4C documentFlow

数据科学指南#基础篇 Matplotlib 入门

Lev

Python 数据科学 matplotlib data-science

Kafka-详细笔记

ninetyhe

分布式 高并发系统设计 消息系统 Kafk

美团二面:Redis与MySQL双写一致性如何保证?

捡田螺的小男孩

数据库 面试 一致性 缓存;

模块4作业 千万级学生管理系统考试试卷存储方案

TH

架构实战营

千万级考试管理系统的考试试卷存储方案

白发青年

架构实战营

架构实战营 模块四:课后作业

👈

架构实战营

python-运算函数-sum

Geek_6370d5

千万级学生管理系统的考试试卷存储方案

颜培攀

架构实战营

模块 4 - 千万级学生管理系统的考试试卷存储方案

小遵

SAP ABAP的权限检查跟踪(Authorization trace)工具使用步骤介绍

Jerry Wang

SAP abap Authorization Authentication

架构实战营 模块四:学习总结

👈

架构实战营

Flink的Time与Window

五分钟学大数据

大数据 5月日更

【Flutter 专题】123 图解简易 GroupList 二级分组列表

阿策小和尚

5月日更 Flutter 小菜 0 基础学习 Flutter Android 小菜鸟

架构实战营 - 模块4- 作业

笑春风

Java Elasticsearch 使用

Java elasticsearch

Flume知识点总结

大数据技术指南

flume 5月日更

Scrum Team不等于Development Team——《Scrum指南》重读有感(2)

Bruce Talk

Scrum 敏捷 随笔 Agile

架构师实战营:模块四 千万级学生管理系统的考试试卷存储方案

ifc177

#架构实战营

谣言粉碎机 - 极短时间内发送两个Odata request,前一个会自动被cancel掉?

Jerry Wang

JavaScript SAP SAP UI5

架构训练营模块四作业

Geek_e0c25c

架构训练营

【架构实战营】第 4 模块作业

swordman

架构实战营

第四次作业 设计千万级学生管理系统的考试试卷存储方案

函数方程(弘宇)

架构实战营模块四作业

薛定谔的指南针

架构实战营

架构实战营作业4

大肚皮狒狒

有赞crash平台符号化实践_架构_杨杨_InfoQ精选文章