这是 Mach-O 系列的第二篇,这篇文章我理解起来感觉不是特别深入,如有问题还望指出,目前也在做更多 Mach-O 的总结,如有新的理解,我也会及时更新文章
符号解析中会遇到很多名词和函数,首先介绍一下这些知识点,然后符号解析会参照 KSCrash
的源码来进行分析
Dsym
debugging SYMbols
:调试符号表
当我们build
的时候,就会在.app
文件中同时生成一个 Dsym 文件,我们在后期捕获到线上 Crash 或者 卡顿 堆栈的地址信息时,会结合 Dsym 进行符号还原,进而确认卡顿、崩溃的具体位置
ASLR
这个在 趣探 Mach-O:文件格式分析 这篇文章中也有提到,这里再深入阐述一下。
ASLR:Address space layout randomization,将可执行程序随机装载到内存中,这里的随机只是偏移,而不是打乱,具体做法就是通过内核将 Mach-O 的段“平移”某个随机系数
我们再来看一下crash
堆栈信息
1 2 3 4 5 6 7 |
Thread 0: 0 libsystem_kernel.dylib 0x10e58110a 0x10e56a000 + 94474 1 libsystem_c.dylib 0x10e303b0b 0x10e285000 + 518923 2 QYPerformanceMonitor 0x10afcdf82 0x10afcc000 + 8066 3 UIKit 0x10bf764f4 0x10bdf3000 + 1586420 4 UIKit 0x10bf7662c 0x10bdf3000 + 1586732 5 UIKit 0x10bf4ad4f 0x10bdf3000 + 1408335 |
我们先关注第三列,这个是函数调用完的返回地址,我们也叫做Frame Pointer Addreass。第四列,是共享对象的的起始地址(Base address of shared object,下面叫做基地址,比如上面的 UIKit)
在终端下,如果我们配合 Dsym 进行符号解析的话,需要这样子写指令
1 |
atos -o Your.app.dSYM/Contents/Resources/DWARF/Your 0x10bf764f4 -arch arm64 -l 0x10bdf3000 |
可以发现仅仅依靠 Frame Pointer 和 Dsym 并不能够进行符号化解析,还需要基地址的配合。为什么呢?
因为ASLR
引入了一个 slide
(偏移),可以通过dyld_get_image_vmaddr_slide()
来进行获取,函数对应在符号表的地址、slide
、frame Pointer address
满足下面这个公式。slide
可以通过程序的 api 获取,也可以通过 Dsym 文件拿到
1 |
symbol address = frame pointer address + slide |
Dl_info
1 2 3 4 5 6 7 8 9 |
/* * Structure filled in by dladdr(). */ typedef struct dl_info { const char *dli_fname; /* Pathname of shared object */ void *dli_fbase; /* Base address of shared object */ const char *dli_sname; /* Name of nearest symbol */ void *dli_saddr; /* Address of nearest symbol */ } Dl_info; |
我们一会经过 dladdr()
处理后的有效信息都会放进这个结构体中
fname:
路径名,例如
1 |
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation |
dli_fbase:
刚才讲到的共享对象的的起始地址(Base address of shared object,下面叫做基地址,比如上面的 CoreFoundation)dli_saddr :
符号的地址dli_sname:
符号的名字,即下面的第四列的函数信息
12345Thread 0:0 libsystem_kernel.dylib 0x11135810a __semwait_signal + 944741 libsystem_c.dylib 0x1110dab0b sleep + 5189232 QYPerformanceMonitor 0x10dda4f1b -[ViewController tableView:cellForRowAtIndexPath:] + 79633 UIKit 0x10ed4d4f4 -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 1586420
LC_SYMTAB
1 2 3 4 5 6 7 8 |
struct symtab_command { uint32_t cmd; /* LC_SYMTAB */ uint32_t cmdsize; /* sizeof(struct symtab_command) */ uint32_t symoff; /* symbol table offset */ uint32_t nsyms; /* number of symbol table entries */ uint32_t stroff; /* string table offset */ uint32_t strsize; /* string table size in bytes */ }; |
符号表在 Mach-O目标文件中的地址可以通过LC_SYMTAB
加载命令指定的 symoff
找到,对应的符号名称在stroff
,总共有nsyms
条符号信息
nlist
nlist
的数据结构看似比较简单,但是里面具有很多的学问,这里先只简单介绍一下它的数据结构,足够我们接下来的分析源码即可
1 2 3 4 5 6 7 8 9 10 11 12 |
/* * This is the symbol table entry structure for 32-bit architectures. */ struct nlist { union { uint32_t n_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 */ int16_t n_desc; /* see */ uint32_t n_value; /* value of this symbol (or stab offset) */ }; |
符号解析
符号解析基本思路如下
- 根据 Frame Pointer 拿到函数调用的地址(address)
- 寻找包含地址 (address) 的目标镜像(image)
- 拿到镜像文件的符号表、字符串表
- 根据 address 、符号表、字符串表的对应关系找到对应的函数名
下面分析的主要是ksdl_dladdr
函数
首先根据 address,找到目标镜像
1 2 3 4 5 6 7 8 |
bool ksdl_dladdr(const uintptr_t address, Dl_info* const info) { // 初始 Dl_info info->dli_fname = NULL; info->dli_fbase = NULL; info->dli_sname = NULL; info->dli_saddr = NULL; // image index const uint32_t idx = ksdl_imageIndexContainingAddress(address); |
后面两步骤是关键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
const struct mach_header* header = _dyld_get_image_header(idx); // slide const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx); const uintptr_t addressWithSlide = address - imageVMAddrSlide; // 段基址 const uintptr_t segmentBase = ksdl_segmentBaseOfImageIndex(idx) + imageVMAddrSlide; // 拿到了 Pathname info->dli_fname = _dyld_get_image_name(idx); // 拿到了基地址 info->dli_fbase = (void*)header; // Find symbol tables and get whichever symbol is closest to the address // 在符号表中查找哪个符号最接近这个指令的地址 // nlist const STRUCT_NLIST* bestMatch = NULL; uintptr_t bestDistance = ULONG_MAX; // load commond uintptr_t cmdPtr = ksdl_firstCmdAfterHeader(header); // header->ncmds 代表所有的加载命令,这里进行遍历,查找 LC_SYMTAB for(uint32_t iCmd = 0; iCmd ncmds; iCmd++) { const struct load_command* loadCmd = ( |