在iOS App开发过程中,我们会利用Xcode打包,生成.xcarchive的包文件,通过Xcode的Organizer工具可以管理、导出发布文件,相信iOS开发对于这些过程都相当的熟悉,这里就不再赘述。主要想说的是,打包之后的dSYM文件。
通过以下方式获取dSYM文件,首先打开Archives管理窗口,如下图:
每Archive一次,都会生成一条记录,找到当前记录所在的目录,如下图:
打开.xcarchive包文件会看到其目录结构,dSYMs中的dSYM包文件就是我们接下来要剖析的文件了。dSYM同样个包文件,打开之后,我们会找到一个二进制文件,如下图,例子中是一个叫Demo的二进制文件。
从目录名中,可以看出iOS使用的是DWARF文件结构,DWARF(可能的解释是:Debugging With Attributed RecordFormats)是一种调试文件结构标准,结构相当的复杂。关于DWARF的前世今生,从何而来,为何而来,如何发展,请参考DWARF官网或网上搜索。
dSYM文件的一个重要的作用在于当我们的程序发生崩溃,通过crash log或其他方式,会看到调用栈信息,通过log信息,我们并不知道具体是在那个文件的哪个位置出了问题,这个时候这个二进制文件就非常的有用了,通过它我们可以通过工具去符号化,比如Xcode自带的atos,这样可以直接定位到某个文件的具体位置。
目前有很多的工具可以解析DWARF文件,比如,Mac OS中就有dwarfdump,otool等,但现有工具并不能满足我们所有的需求,现在我们了解下其内部结构,在日后开发中有需要,可以用来参考。
下面我们打开这个二进制文件。注:以下“段”单位为4个字节。
打开文件后,先来看下文件头,结构定义参考<fat.h>。
FAT二进制数据
第一段为magic,这里需要注意字节序,读出来之后需要看下是0xCAFEBABE还是0xBEBAFECA,需要根据这个来转后续读取的字节的字节序。
第二段为arch count,也就是该app或dSYM中包含哪些cpu架构,比如armv7,arm64等,这个例子中为2,表示包含了两种cpu架构。
后续段中包含cputype(0x0000000C)、cpusubtype(0x00000009)、offset(0x00000040)、size(0x000F6825)等数据,根据fat中的结构定义,依次读取,这里需要说明的是,如果只包含一种cpu架构的话,是没有这段fat头定义的,可以跳过这部分,直接读取Arch数据。
根据fat头中读取的offset数据,我们可以跳到文件对应的arch数据的位置,当然如果只有一中架构的话就不需要计算偏移量了。例子中32-bit arch的offset为0x00000040,64-bit arch的offset为0x000F6880。以下数据结构参考<mach.h>。
Mach二进制数据
(32-bit)
(64-bit)
通过magic我们可以区分出是32-bit 还是64-bit,64-bit多了4个字节的保留字段,这里同样需要注意字节序的问题,也就是判断magic,来确定是否需要转换字节序。
以下部分解析,以32-bit为例。
UUID二进制数据
UUID是16个字节(128bit)的一段数据,是文件的唯一标识,前面提到的符号化时,这个UUID必须要和app二进制文件中的UUID一致,才能被正确的符号化。dwarfdump查看的UUID就是这段数据。读取这部分数据时通过Command结构读取的,也就是第一段(0x0000001B)表示接下来的数据类型,第二段(0x00000018)数据的大小(包含Command数据)。
SymTab二进制数据
符号表数据块结构,前二段依然是Command数据。后边4段分别为符号在文件中的偏移量(0x00001000)、符号个数(0x00000015)、字符串在文件中的偏移量(0x000010FC)、字符串大小(0x00000297)。
接下来就是读取Segment和Section数据块了,和上面读取数据块结构一样是根据Command结构读取,下图展示的Segment数据和Section数据是分开的,实际在二进制文件中它们是连续的,也就是每一条Segment数据后面会紧跟着多条对应的Section数据,Section的数据总数是通过Segment结构中的nsects决定的。
Segment数据
从Segment数据中我们可以看到, __TEXT的vmaddr是0x00004000,也就是程序的加载地址,当然这个是指32-bit的程序,64-bit是不同的。__DWARF中表明了DWARF数据块的信息,表示dSYM是DWARF格式的数据结构。
Section数据
以上为Section数据的一部分,从Section数据中,我们可以找到__debug_info, __debug_pubnames, __debug_line等调试信息,通过这些调试信息我们可以找到程序中符号的起始地址、变量类型等信息。如果我们要符号化的话,就可以通过解析这些数据得到我们想要的信息。
关于Segment和Section中类型的定义,请参考DWARF官网。
关于如何解析解析数据得到符号在文件中的位置,下篇再做分享。
到这里我们已经读取了符号文件头中的大部分数据,在文件头里还有一部分数据也是很重要的,就是符号块数据,他是我们程序里所有的方法信息。
Symbol二进制数据
通过SymTab中的数据可以得到Symbol在文件中的位置和个数,Symbol块数据中包含了符号的
起始地址,字符串的便宜量等数据,这部分数据结构可以参考<nlist.h> 和 <stabl.h>。这部分数据全部读取后,就可以读取所有的符号数据了,也就是接下来的数据。
Symbol String二进制数据
通过SymTab和Symbo中的数据可以得到每个符号字符串在文件中的偏移量和大小,每个符号数据是以0结尾的字符串。
我们通过以上两部分数据的组合就可以得到每个symbo在程序中的加载地址了。这些数据对于以后做符号工作都非常的有帮助。64-bit的数据解析与以上方法相同,不过要注意64-bit中的有些数据是有点差别的,解析时需要注意。
到此,关于dSYM文件中头部数据读取就完成了。头部数据都有相应的数据结构定义,读取时相对会比较容易些,解析数据时要注意字节序的问题,32-bit和64-bit数据结构的差异、字节长度的差异,DWARF版本的差异,每个数据块之间都是紧密联系的,一个字节的读取偏差就会造成后续数据的读取错误,正所谓差之毫厘,失之千里。
本文由TestinAPM李明湘原创
欲知更多干货,请访问apm.testin.cn
或加入学习小组qq群:232336330
来源:oschina
链接:https://my.oschina.net/u/2416924/blog/489789