Crash 符号化 3. Mach-O 与 atos

2018-02-27 11:08:26来源:http://saitjr.com/ios/symbolicatecrash-3.html作者:// TODO:人点击

分享

继续符号化这个话题,之前谈到了 Xcode 通过调用 symbolicatecrash 命令进行符号化,而这个命令中除了查找符号表以外,另外一件非常重要的事情就是调用 atos 。所以,继续学习 atos 。


环境信息

Xcode 9.2


atos (Address to symbol)是将地址转为符号的命令,源码可以参考 facebook 开源的 C 语言版本atosl,虽然 facebook 表示这个库已经停止更新,任何风险自行承担,但是并不妨碍我们学习源码。


整个解析过程就是对符号表的读取,所以首先要知道应该怎么看符号表。符号表为 Mach-O 文件,可以借助 otool 和可视化工具 MachOView ,当然,也可以选择 hopper 。这里我用的 MachOView 。


Mach-O

Mach-O (Mach Object)是一种文件格式,macOS、iOS 等系统上的可执行文件、 libraries 和 object code 均使用这种格式。


首先需要了解的就是它的布局方式(Mach-O file layout)。借用网上流传得很广泛的一张图来解释一下:



Mach-O 主要分为三部分:Header、Load commands、Data。读取过程中,每一个部分都标明了各自的大小,读取 Data 部分时,也有对应的 offset 值,所以非常方便。


Fat Header

如果直接用编辑器打开 Mach-O 打开,可以看到一串地址,通过 <mach-o/fat.h> 中对 fat header 的定义,可以知道前几位的含义:



struct fat_header {
uint32_t
magic;
/* FAT_MAGIC or FAT_MAGIC_64 */
uint32_t
nfat_arch;
/* number of structs that follow */
};


前 8 个字节为 magic,紧接着的 8 个字节表示 fat 包含的架构数:



magic 的定义也可以在 fat.h 中看到,它对应着两种 fat_arch 结构,通过读取 magic 就可以知道对应的 fat_arch 布局方式。


除了直接看定义以外,也可以利用之前提到的工具来进行查看:


otool

otool 是一个展示 object file 的工具,通过传入不同的参数,来读取不同的片段。



# 可以利用 otool -h 来读取 header 部分
otool -h ~/Desktop/test_dsym/TestSymbol


MachOView

之后都会使用 MachOView 这个 app,比较直观。用 MachOView 打开符号表的 Mach-O 文件,可以看到已经解析完成的 fat header:



需要关心的是 offset 字段,可以直接通过偏移量,找到对应架构的的地址。


Header

可以看到当前符号表包含两种架构,直接来看 ARM64 吧。偏移 756448 字节,就是该架构的起始地址。



mach header 的结构在 <mach-o/loader.h> 这个头文件中,之后读取 load commands 的结构,也都定义在这个文件。同样,这个 header 也分为 32 和 64 两种内存布局:



struct mach_header_64 {
uint32_t
magic;
/* mach magic number identifier */
cpu_type_t
cputype;
/* cpu specifier */
cpu_subtype_t
cpusubtype;
/* machine specifier */
uint32_t
filetype;
/* type of file */
uint32_t
ncmds;
/* number of load commands */
uint32_t
sizeofcmds;
/* the size of all the load commands */
uint32_t
flags;
/* flags */
uint32_t
reserved;
/* reserved */
};


Load Commands

Header 和 Load Commands 之间的地址连续,所以 offset + sizeof(mach_header) 就可以读取到第一个 Load Command。


每个 Load Command 包含 cmd 与 cmdsize ,即类型与大小:



struct load_command {
uint32_t cmd;
/* type of load command */
uint32_t cmdsize;
/* total size of command in bytes */
};


头文件中定义了不同的 cmd 类型,可以将它看成枚举。 atosl 中的代码大致是:



switch (load_command.cmd) {
case LC_UUID:
ret = parse_uuid(obj, load_command.cmdsize);
break;
case LC_SEGMENT:
ret = parse_segment(obj, load_command.cmdsize);
break;
case LC_SEGMENT_64:
ret = parse_segment_64(obj, load_command.cmdsize);
break;
case LC_SYMTAB:
ret = parse_symtab(obj, load_command.cmdsize);
break;
...
}


这里我们需要关心三个地方: LC_UUID 、 LC_SYMTAB 和 LC_SEGMENT(__TEXT) 。


LC_UUID

每个符号表的每个架构都有不同的 UUID,如果 UUID 不匹配,是无法正确符号化的。获取 UUID 的方式很多,一一介绍一下:


通过 otool -l 显示整个 Load Commands,然后找到对应的 UUID:



otool -l ~/Desktop/test_dsym/Test | grep uuid


更好的方式是使用之前提到过的 dwarfdump :



dwarfdump --uuid ~/Desktop/test_dsym/Test


当然,最直观的还是 MachOView ��:



结构体定义:



struct uuid_command {
uint32_t
cmd;
/* LC_UUID */
uint32_t
cmdsize;
/* sizeof(struct uuid_command) */
uint8_t
uuid[16];
/* the 128-bit uuid */
};


LC_SYMTAB

继续往下读取,就可以读到 LC_SYMTAB ,其中需要关注的是 Symbol Table Offset 和 String Table Offset 字段,它指明了符号表和代码信息的偏移量。



这里的偏移量是从 mach header 开始,4096 也就是 0x1000,加上 mach header 起始的 0x00C8E00,即 0xC9E00,这也就是符号表的起始地址:



而这里,就是存储的方法名地址、对应虚拟地址等信息了。这部分定义在 <mach-o/nlist.h> 中:



struct nlist_64 {
union {
uint32_tn_strx; /* index into the string table */
} n_un;
uint8_t n_type;/* type flag, see below */
uint8_t n_sect;/* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value;/* value of this symbol (or stab offset) */
};


符号化的过程,就是找到通过 value 找到 n_strx ,再通过之前读取的 String Table Offset 找到方法名的过程。


LC_SEGMENT(__TEXT)

由于 ASLR 的原因,在程序启动时,会在指定的进程空间上加上一个偏移量(slide),导致我们看到的 stack address(crash 日志中的地址)和 symbol address(符号对应的地址)无法对应。


Address space layout randomization ( ASLR ) is a memory-protection process for operating systems (OSes) that guards against buffer-overflow attacks by randomizing the location where system executables are loaded into memory.


Emmm…大致意思就是:随机内存布局(ASLR)是操作系统为防止缓冲区溢出攻击而存在的内存保护机制。该机制通过在程序载入内存时,将地址进行随机偏移来实现。


这个偏移量是每次程序启动时给出的随机值,运行时可以通过 _dyld_get_image_vmaddr_slide 函数获得。但是在符号表中并不能体现。


在 LC_SEGMENT(__TEXT) 的 VM Address 为未偏移的虚拟地址:



加上偏移的虚拟地址,可以在 crash log 中的 Binary Images 找到:



Binary Images 中的地址,为二进制加载的起止位置。所以可以通过计算获得偏移量:



slide = load address - vm address
偏移量 = 0x1000b4000 - 4294967296 = 0xb4000


符号化

那么,下一步,进行符号化。



来看下以上三个地址(因为 Demo 太简单了,所以调用栈很短,最后一个还是 main ,为了更好的介绍,就不符号化 main 了,直接看上面两个地址。这两个地址是我用系统符号化之后,得到的两个属于 Test 的 crash 地址)。


0x1000ba9c8 和 0x1000ba964 都是经过偏移的地址,所以首先减去偏移量:



symbol address = stack address - slide
对应符号的地址_1 = 0x1000ba9c8 - 0xb4000 = 0x1000069C8
对应符号的地址_2 = 0x1000ba964 - 0xb4000 = 0x100006964


到这里,可以通过调用 dwarfdump 命令查看地址对应的方法名:



dwarfdump --arch arm64 --lookup 0x1000069C8 ~/Desktop/test_dsym/Test


可以在输出中看到对应的文件、方法名等信息:



当然,通过 MachOView 也可以查看:



当前地址为 0x1000069C8 ,也就对应着 -[ViewController method1] 方法。


优化

具体 atos 的实现不得而知,估计也躲不过这个套路。针对 symbolicatecrash 对 atos 的调用,列出了一些优化点,因为没有具体实施,所以不清楚优化效果如何。


符号表加载:每次都从硬盘中加载符号表,在符号多的情况下开销还是挺大的。可以针对常用符号进行缓存,比如 CoreFoundation , libdispatch 之类的。
符号查找:不太清楚 atos 的查找方式,但是根据目前网上的代码看,很多代码都是拿到 symbol address 后,开始遍历 Symbol Table 进行查找。但其实 Symbol Table 部分地址连续,可以直接上二分,效率能提升不少(这部分是组内小伙伴优化后得出的结论,但是是对比的网上代码,并不是 atos 实现)。
参考
Krush
深入剖析 iOS 编译 Clang / LLVM
iOS crash log 解析 symbol address = stack address - slide 运行时获取slide的api 利用dwarfdump从dsym文件中得到symbol
iOS崩溃堆栈信息的符号化解析
atosl
Mach-O Programming Topics
PARSING MACH-O FILES

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台