這是 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 = (struct load_command*)cmdPtr; if(loadCmd->cmd == LC_SYMTAB) { // symtab_command LC_SYMTAB const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr; // 找到符號表 const STRUCT_NLIST* symbolTable = (STRUCT_NLIST*)(segmentBase + symtabCmd->symoff); // 找到字串表 const uintptr_t stringTable = segmentBase + symtabCmd->stroff; // 遍歷符號表 for(uint32_t iSym = 0; iSym nsyms; iSym++) { // If n_value is 0, the symbol refers to an external object. if(symbolTable[iSym].n_value != 0) { uintptr_t symbolBase = symbolTable[iSym].n_value; // addr >= symbol.value 說明這個指令在這個函式入口中 // 獲取和address的距離找到最接近的一個 // 離指令地址addr更近的函式入口地址,才是更準確的匹配項 uintptr_t currentDistance = addressWithSlide - symbolBase; if((addressWithSlide >= symbolBase) && (currentDistance dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide); // 符號的名字 info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx); break; } } cmdPtr += loadCmd->cmdsize; } return true; } |
參考連結
- 深入理解 MAC OS X & iOS 作業系統
- KSCrash / KSDynamicLinker
- iOS中執行緒Call Stack的捕獲和解析
- mach 0 原始碼