iOS如何区分App和SDK内部crash

2019 年 4 月 17 日 CocoaChina

最近在开发iOS平台上的SDK,提供给合作方使用。为了监控SDK自身的崩溃率,我们在SDK中加入了抓取crash的功能。但收集上来的日志中有较多合作方App的crash,并且接入SDK的App数量很多,产生的崩溃日志量非常大。靠人力从海量的日志中筛选出我们SDK的crash日志非常困难。


于是就有了这个问题,如何自动区分SDK内部的crash和App的crash?


iOS crash日志格式


想区分不同类型的crash,需要先了解iOS的crash日志格式。iOS的crash报告日志可以分为头部(Header)、异常信息(Exception Information)、诊断信息(Additional Diagnostic Information)、线程堆栈(Backtraces)、线程状态(Thread State)、库信息(Binary Images)这六个部分。如图:



其中:

  1. 头部(Header): 硬件型号,系统版本,进程名称、id,bundleid,崩溃时间,crash日志报告格式版本号等信息。

  2. 异常信息(Exception Information): 崩溃类型、崩溃代码及触发崩溃的线程等信息。

  3. 诊断信息(Additional Diagnostic Information): 非常简略的诊断信息。不是每个崩溃都会有诊断信息。

  4. 线程堆栈(Backtraces): 崩溃发生时,各个线程的方法调用栈的详细信息。触发崩溃的线程会被标记上Crashed。

  5. 线程状态(Thread State): 崩溃时寄存器的状态。

  6. 库信息(Binary Images): 加载的动态库信息。

查看crash日志时,首先会在【异常信息(Exception Information)】中通过名叫“Triggered by Thread”的字段查看是哪个线程发了crash。例如上图中的“Triggered by Thread: 0”表示是0号线程也就是主线程发生了crash。在【线程堆栈(Backtraces)】信息中,也会看的线程号下面会用“Thread xx Crashed”标记该线程发生了crash。


在【线程堆栈(Backtraces)】信息中,有方法编号,方法所属的模块名,方法地址,方法符号信息或者方法所在的段地址及偏移量。每个方法的地址是包含在所属模块的地址范围内。如图:



图中显示,0号线程即主线程发生了crash,地址是0x00000001000effdc,在TheElements这个模块内。在【库信息(Binary Images)】信息中可以找到这个二进制模块,也就是App可执行文件TheElements,其他的模块是系统加载的动态库,比如UIKit、CoreFoundation等(这张图并没有显示出来,在第一张图中可以看到另外两个系统的动态库,其他系统库的太多了,图中并没有展示)。


如何确定动态库的crash


熟悉了crash日志报告的格式,我们知道动态库的crash的方法栈中是带有动态库的名字的,一眼就能看出是哪个模块发生了crash。通过格式化好的crash日志,就能够区分App的crash和引入的动态SDK的crash。如果要在App运行时crash后立即判断,可以通过crash的地址,找到包含这个地址的二进制模块就行了。


如何确定静态库的crash


通过Crash的地址可以找到该方法所属的二进制模块。然而,如果SDK是静态库引入的,其代码会被加入到App的代码段中,SDK的代码和App的代码属于同一个二进制模块,这样就不容易判断了。例如第一张图中的crash在 [TestCrash illegalAccess], 这段代码是在SDK中的,而所属的模块是TestSDKApp,即App的代码段,这样就不知道是App还是SDK内部crash了。


Thread 0 Crashed:
0   TestSDKApp                        0x00000001021529d4 +[TestCrash illegalAccess] + 27092 (TestCrash.m:21)
1   UIKitCore                         0x000000020df86768 -[UIApplication sendAction:to:from:forEvent:] + 96

Binary Images:
0x10214c000 - 0x102153fff TestSDKApp arm64  <3f44b26a30f93ebd8f79abd1fe7e0ac9>  /var/containers/Bundle/Application/9293836E-A3C3-4C61-BDAD-BEED79AC9EDE/TestSDKApp.app/TestSDKApp


对于这个问题,当时我们有这几个选择: 一个是服务端收集到crash日志后,通过符号文件解析出对应的堆栈信息,然后通过crash的符号来判断是app的crash还是sdk的内部的crash。另一个方法就是通过地址来判断。


通过符号来判断


这个思路很简单,就是人查看crash日志识别不同crash的过程。大致步骤如下:


  1. 符号化服务端crash日志

  2. 收集SDK中所有的特征符号,比如类名,方法名等等

  3. 处理crash日志,对比SDK中的特征符号,确定该crash是SDK内部crash


思路简单,但是会带来很多问题。如收集SDK中类名费时费力;SDK中某些引入的第三方库没有符号信息;不同版本的SDK符号特征不一样。每个问题都能折磨人。


通过地址来判断


既然都知道了crash发生的地址,为什么不通过地址来判断是否在SDK内部呢,就像动态库的crash一样?于是问题就变为了如何确定SDK代码被连接进App后的起始地址和结束地址。


静态库SDK二进制文件结构


这里对SDK的文件格式不详细介绍,看图:



图中红框中标出的是该SDK包含的object文件内容,即文件编译后的产物。这里的顺序是如何确定的呢?看下xcode中【Targets->Build Phases->Complie Sources】就明白了,是xcode编译这些源文件的顺序。如图:



也就说,SDK的文件相当于一个object文件的容器,把源文件的编译产物按顺序打包组织在一起就是SDK的二进制文件了。把SDK二进制连接进App可执行文件后是什么样的?我们先看下App可执行文件的结构。


App可执行文件结构


同样的,我们对App可执行文件的格式不详细展开,只看连接进App的SDK是如何组织的。如图:



这两张图是App可执行文件中反汇编后,获取的方法地址列表。SDK中方法太多,只截取了开始部分和结束部分。可以看出,SDK中的方法都是集中在一块,并且是按照SDK文件中的object文件顺序排列。
于是可以得出这样一个结论

SDK中文件的编译顺序最终体现在连接进App可执行文件中方法的地址顺序。也就是,SDK连接进App可执行文件后,基本上是在一块连续的地址上;App执行时,加载进内存也会在一块连续的内存地址上。

这样我们就通过crash方法地址就能确定是否是SDK内部的crash。于是乎我们就剩下最后一个问题了,如何确定SDK代码段的起始地址和结束地址?


如何确定SDK代码起始和结束地址


细心的人可能已经从前面的图片中看出来怎么做了。对,就是在编译文件最前面添加一个文件,最后面添加一个文件。比如图片中的CodeTextBegin.m 和 CodeTextEnd.m ,然后第一个文件的第一个方法地址就是SDK编译文件中所有方法的起始地址,最后一个文件的最后一个方法地址就是SDK编译文件中所有方法的结束地址。例如:


CodeTextBegin.m放在所有编译文件的最前面,里面的第一个方法是获取该方法自身的地址。


// CodeTextBegin.m
#import "CodeAddress.h"

// 返回这个方法的地址
extern void * getSDKStartAddress(void) {
    return &getSDKStartAddress;
}


CodeTextEnd.m放在所有编译文件的最后面,里面的最后一个方法是获取该方法自身的地址。


// CodeTextEnd.m
#import "CodeAddress.h"

// 返回这个方法的地址
extern void * getSDKEndAddress(void) {
    return &getSDKEndAddress;
}


这样我们调用这个两个方法就能都到所有编译文件中的方法的起止地址。


为什么是SDK编译文件中的所有方法呢?因为,SDK中还可以引入其他SDK。这个时候,引入的其他SDK的所有方法会添加在编译文件的最后一个方法后面。所以,如果我们要包含SDK所有方法时,应该在SDK中引入一个SDK,并且放在所有引入的SDK最后面,这个最后面的SDK中,有个方法可以返回自身的地址。


这样,我们就能通过crash时的方法地址来判断是否是SDK内部的crash了。


在根据上面的方法做完后测试,SDK运行时获取的方法地址和在MachOView工具中看到的App可执行文件中的地址对应不上,这是为什么?


其实是iOS系统引入了ASLR机制,即Address space layout randomization。
在App运行时,iOS系统会给加载进内存的二进制模块一个随机的偏移地址,我们只需要把运行时的地址减掉这个偏移地址好了 代码如下:


// 获取可执行模块的slide
extern long getExecuteImageSlide(void) {
    long slide = -1;
    for (uint32_t i = 0; i < _dyld_image_count(); i++) {
        if (_dyld_get_image_header(i)->filetype == MH_EXECUTE) {
            slide = _dyld_get_image_vmaddr_slide(i);
            break;
        }
    }
    return slide;
}


用获取到的方法地址减去偏移值就是App可执行文件中的地址:


// 真实的起始地址
void* startAddr =  getSDKStartAddress() -  getExecuteImageSlide();

// 真实的结束地址
void* endAddr = getSDKEndAddress() -  getExecuteImageSlide();


总结


前面按照解决问题的步骤贴图讲的,比较混乱。这里总结下,我们可以通过SDK的起止地址是否包含crash的方法地址来判断crash是否发生在SDK内部。 具体如下:

  1. 动态库有明确的起止地址,可以用crash方法的地址直接来判断。

  2. 静态库中的方法会按编译时的顺序连接进App可执行文件。

  3. 在第一个编译文件最前面添加一个方法,返回其自身地址,作为SDK的起始地址

  4. 在最后一个编译文件的最后面添加一个方法,返回其自身地址,作为SDK的所有编译文件方法的结束地址,如果SDK中有引入其他第三方库则需步骤5

  5. 在最后一个被引入SDK的三方库后面,添加一个库,库中导出一个(4)中一样的方法,作为SDK的结束地址

  6. crash时,把获取到的crash地址与SDK的起止地址进行比较就能知道是否是SDK内部的crash。主要比较地址时,如果crash地址减去了slide,则对应的SDK起止地址也应减去slide。

Demo地址
查看二进制文件工具MachOView地址


作者:偶尔登南山
链接:https://www.jianshu.com/p/f72f10a921b9


本公众号转载内容已尽可能注明出处,如未能核实来源或转发内容图片有权利瑕疵的,请及时联系本公众号进行修改或删除【联系方式QQ : 3442093904  邮箱:support@cocoachina.com】。文章内容为作者独立观点,不代表本公众号立场。版权归原作者所有,如申请授权请联系作者,因文章侵权本公众号不承担任何法律及连带责任。

---END---

登录查看更多
11

相关内容

SDK(Software Development Kit, 即软件开发工具包 )一般是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
58+阅读 · 2020年6月26日
Python导论,476页pdf,现代Python计算
专知会员服务
261+阅读 · 2020年5月17日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
118+阅读 · 2020年5月10日
【实用书】流数据处理,Streaming Data,219页pdf
专知会员服务
77+阅读 · 2020年4月24日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
96+阅读 · 2019年12月4日
【课程】伯克利2019全栈深度学习课程(附下载)
专知会员服务
57+阅读 · 2019年10月29日
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
已删除
创业邦杂志
5+阅读 · 2019年3月27日
C# 10分钟完成百度人脸识别
DotNet
3+阅读 · 2019年2月17日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
Android P正式发布,你需要尽快做适配了
前端之巅
3+阅读 · 2018年8月7日
占坑!利用 JenKins 持续集成 iOS 项目时遇到的问题
iOS高级调试&逆向技术
CocoaChina
3+阅读 · 2017年7月30日
Deep Learning for Deepfakes Creation and Detection
Arxiv
6+阅读 · 2019年9月25日
Learning to Focus when Ranking Answers
Arxiv
5+阅读 · 2018年8月8日
Arxiv
3+阅读 · 2018年3月27日
Arxiv
3+阅读 · 2018年3月22日
Arxiv
7+阅读 · 2018年3月19日
Arxiv
15+阅读 · 2018年2月4日
VIP会员
相关VIP内容
相关资讯
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
已删除
创业邦杂志
5+阅读 · 2019年3月27日
C# 10分钟完成百度人脸识别
DotNet
3+阅读 · 2019年2月17日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
Android P正式发布,你需要尽快做适配了
前端之巅
3+阅读 · 2018年8月7日
占坑!利用 JenKins 持续集成 iOS 项目时遇到的问题
iOS高级调试&逆向技术
CocoaChina
3+阅读 · 2017年7月30日
相关论文
Deep Learning for Deepfakes Creation and Detection
Arxiv
6+阅读 · 2019年9月25日
Learning to Focus when Ranking Answers
Arxiv
5+阅读 · 2018年8月8日
Arxiv
3+阅读 · 2018年3月27日
Arxiv
3+阅读 · 2018年3月22日
Arxiv
7+阅读 · 2018年3月19日
Arxiv
15+阅读 · 2018年2月4日
Top
微信扫码咨询专知VIP会员