微信号:baiduqa

介绍:提供百度质量部的动态、技术分享等资讯.

【专家专栏】杨咏臻 | 实战iOS崩溃堆栈的符号化解析

2017-08-10 17:24 杨咏臻

简介



对于iOS开发的同学来说,在开发、调试和应用上线后,程序会经常出现崩溃。但是,不管我们的崩溃堆栈是获取的苹果官方的导出文件,还是通过PlCrashReporter类似的第三方开源库集成捕获的,都会面临同一个问题,堆栈大概率是没有被符号化过的,类似下图:


注意红框处,堆栈的详情,全都是十六进制内存地址形式。


这样的Crash log的形式,对于开发者来说,是根本看不懂的,那就无从谈起问题的定位了。那要怎么让它可以看懂呢?其实,我们需要的Crash log应该是下面这种形式:



而为了得到解析后的堆栈,就会引出下一个问题,iOS的堆栈符号化。而对于堆栈符号化,一般有3种方式,我会一一给大家分享。



符号化解析三种的方法



symbolicatecrash

symbolicatecrash是Xcode提供的一个内置工具,是一个将堆栈地址符号化的脚本,它的输入参数是苹果官方格式的Crash log文件和app的dSYM文件,然后会在标准输出中输出符号化之后的信息。


注意,Xcode的Organizer内置了symbolicatecrash,所以我们才可以直接看到符号化的崩溃堆栈日志。


通过终端的方式进行符号化,一般步骤是这样的:


  1. 打开终端,然后输入以下命令获取到工具所在的目录


  2. 这个时候会得到symbolicatecrash的文件路径,然后用cp命令将它拷贝到一个文件夹里,然后把需要分析的Crash log和dSYM文件一起cp进去,然后执行以下命令进行符号化:


  3. 如果没有报错的话,重定向后的crab.crash.txt就是符号化之后的文件了,就可以进行崩溃定位了。


但是,使用这种方式,会有很大的局限性:


  1. 只能分析官方格式的崩溃日志,需要从具体的设备中导出,获取和操作都不是很方便。


  2. 符号化的结果也是没有具体的行号信息的,也经常会出现符号化失败的情况。


而对于使用了PlCrashReporter等第三方开源工具捕获的Crash log,这个工具就无能为力了,因此,我们需要寻找一个更通用的方法来进行堆栈符号化。



atos工具

atos命令是苹果官方提供的符号化工具,在MAC系统中是默认安装的,使用的命令格式如下:


atos–o executable(也可以是系统库) –arch cpu架构–l loadAddress


而我们如何正确的传入这3个参数呢?请看下图:


我们得到了一个只有部分解析的Crash log,然后我们可以在文件后半部分找到一个Binary Image块。最后,通过Binary Image Name,直接定位到加载模块的信息,包括地址范围,架构和UUID,有了这几个信息之后,我们就可以通过以下命令来进行符号化:


掌握了基本原理之后,我们就可以通过脚本的方式,批量的实现Crash log的符号化了。


但是,这还是会有一个限制,atos工具是MAC上独有的,也就意味着我们的批量符号化只能在MAC上进行。试想一下,如果你是管理着多个iOS产品线的符号化解析,你只能在Mac上或者是自己搭建的黑苹果上解析,稳定性、扩展性、效率、系统库升级等都是一个一个令人头大的问题。因此,我们就想,如果能在Linux上实现atol那该多好,以上问题就会迎刃而解了。


在经过了大量时间的摸索后,在GitHub上发现了一个Facebook开源的Linux上atosl的工具实现,分享给大家:

https://github.com/facebookarchive/atosl


因为Realease版本支持的CPU架构太少,可以直接使用master分支上的代码进行编译,然后就可以开开心心的在Linux上完成iOS符号化了。



通过dSYM文件符号化

使用了atos(或者是atosl)实现了符号化之后,还是会存在一个问题,调用的脚本每次都需要通过shell命令调用atos(atosl)工具,但是只能一次符号化一行堆栈,这样的话,对于拥有上百行的堆栈来说,就意味着上百次的atos(atosl)进程启动,无疑这块的I/O成本是极其高的,对于大规模解析来说,效率很慢。


因此,就需要我们寻找一种更好、更快、跨平台的符号化工具。而以上两种方法,都有一个共同特点,就是用到了dSYM文件,我们就从他入手。


dSYM文件格式

dSYM文件是DWARF文件结构,它是一种标准的文件调试结构,打开一个dSYM文件,可以看到如下的目录结构:


而最后目录的二进制文件其实是一个Mach-O格式,我们符号化的依据就来源于它,它的格式如下:

Mach-O大概分为三部分:


  1. Header:保存了Mach-O的一些基本信息,包括了平台、文件类型、LoadCommands的个数等等;


  2. LoadCommands:紧跟Header,加载Mach-O文件时会使用这里的数据来确定内存的分布;


  3. Data段:由多个Segment构成,这里包含了具体的代码、数据等信息;


Header段详解

Mach-O的Header格式的结构定义在<mach-o/fat.h>中,然后对照着结构定义,打开一个dSYM文件进行查看,其每一段定义如下:


  1. 第一段为Magic数,需要注意这4个字节的大小端,我们需要根据这个来转后续读取的字节的字节序。然后我们根据fat.h的定义,可以通过这个4字节的数字判断是Fat binarty还是Thin Binary;


  2. 第二段为Arch count,即该dSYM文件包含了多少种CPU架构;


  3. 后续段中,可以根据fat.h的定义,依次获得cputype、cpusubtype、offset、size等数据。注意,如果只包含1种架构的话,是不存在这段结构定义的,直接读取后面的arch数据。


  4. 根据上一步获取到的offset,直接跳到文件对应的arch数据起始位置。到了arch数据起始位置后,我们开始读取Mach Header数据。首先,开始的4个字节是magic数,可以判断应用是32位还是64位,那也就决定了我们是该用struct mach_header还是struct mach_header_64。然后通过相关的定义,就可以依次得到cputype、cpusubtype、filetype、offset、size等数据。


mach-header的结构定义如下:

 

至此,我们就完成了Header的读取,接下来可以开始处理arch数据。


Load Commands部分

该部分是紧跟Header的,其结构定义在mach-o/loader.h中,表明在加载过Header后,如何通过cmd参数来调用不同的函数来加载。


而cmd支持的类型,也在mach-o/loader.h中,部分命令如下图:


 Data段读取


我们根据上一部分每个LC的定义,依次读取UUID、SymTab、Segment、Section、Symbol和Symbol String数据。其中,对于上述每一个命令的定义,在mach-o/loader.h中,都有相应的命令结构定义。


  • UUID

    对于UUID,它是一个16字节的数据,是文件的唯一标示符,只有Crash log的UUID和dSYM文件中的UUID匹配,才能进行正确的符号化。


  • SymTab

    这块是符号表的符号块数据,它包含符号在文件中的偏移量、符号个数、字符串在文件中的偏移量、字符串大小。


  • Segment和Section数据

    接下来,我们将会读取Segment数据,它是由LC_SEGMENT或LC_SEGMENT_64命令进行加载的。其中在每一个dSYM文件中,每一条Segment后都会跟着多条Setction数据,这两块数据是我们提取符号表的关键。


接下来,我们需要查看segment相关的命令定义,如下图:


在这里,我们最关心的应该是nsects字段,这个字段表明了后续跟了多少个Section数据,而每一个Section则是所有具体有用数据的存放位置,其定义如下:



在Section数据中,我们需要关注sectname为__debug_info、__debug_line、__debug_pubnames等调试信息,这些调试信息存储了Symbol的起始位置、行号、变量类型等数据,是我们符号化的关键。


  • Symbol和SymbolString数据

    通过SymTab中的数据可以得到Symbol在文件中的位置和个数,Symbol块数据中包含了符号的起始地址、字符串的偏移量等数据,其结构定义在nlist.h和stabl.h中,然后结合Symbol String数据,就可以导出最终的符号表。一般步骤如下:


    a.  通过SymTab和Symbo中的数据可以得到每个符号字符串在文件中的偏移量和大小,每个符号数据是以0结尾的字符串。

    b.  然后我们通过以上两部分数据的组合就可以得到每个symbo在程序中的加载地址了。


符号的重整

至此,我们就读取到了该dSYM文件所有的符号表数据了,但是接下来可能还会存在一个问题,那就是对于符号重整(mangled symbol names)的还原(demangle)。这块主要是针对C++和Swift编写的模块。


目前,经过探索,我只找到在Linux上对于C++的符号重整的还原,而对于Swift还是只能在Mac或高版本的Ubuntu上进行手动还原。相关信息如下表所示:


 

至此,所以工作做完之后,我们就完成了dSYM文件符号表的提取,解析出来的符号表样例如下:


接下来符号化的事情就很简单了,用你最擅长的语言,写一个脚本,通过Binary Image块给出的appName(或者系统库)、uuid、加载地址等,再结合堆栈信息进行还原就能拿到最终的符号化信息了。



小结


好了,三种符号化方法都介绍完了,每一种都有自己的应用场景,大家可以结合自己的实际情况来使用。

 

作者介绍


 杨咏臻

百度QEC质量工程中心Crab平台负责人

2011加入百度,先后在网页搜索测试部(PSQA)、新业务测试部(NSQA)、基础技术测试部(FTQA)、平台测试部(PLATQA)从事EP工作。

带有典型狮子座性格特征:追求完美-关注用户体验;有工作狂的性质-习惯每天9点到公司,喜欢将工作做到极致。



 
百度质量部 更多文章 【专家专栏】于晖 | Web App新趋势——PWA 【专家专栏】尹飞 |基于UI响应时间的移动App性能测试解决方案 【专家专栏】周伟衢 | 历历在目的2年测试生活 百度MTC:一站式移动App测试解决方案之应用商店 【专家专栏】项光特 | XCode远程调试iOS设备
猜您喜欢 盘点:基于Linux生态的十大AI开源框架 隔空操控来了!谷歌推出黑科技"空气操作"!全世界都疯了! 恭贺新春 年终奖不够?红包来凑!UPYUN 新春有礼 告知你不为人知的UDP-疑难杂症和使用