Mach-O在記憶體中符號表地址、字串表地址的計算
KSCrash 是一個用於 iOS 平臺的崩潰捕捉框架,最近讀了其部分原始碼,在 KSDynamicLinker
檔案中有一個函式,程式碼如下:
/** Get the segment base address of the specified image. * * This is required for any symtab command offsets. * * @param idx The image index. * @return The image's base address, or 0 if none was found. */ static uintptr_t segmentBaseOfImageIndex(const uint32_t idx) { const struct mach_header* header = _dyld_get_image_header(idx); // Look for a segment command and return the file image address. uintptr_t cmdPtr = firstCmdAfterHeader(header); if(cmdPtr == 0) { return 0; } for(uint32_t i = 0;i < header->ncmds; i++) { const struct load_command* loadCmd = (struct load_command*)cmdPtr; if(loadCmd->cmd == LC_SEGMENT) { const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr; if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) { return segmentCmd->vmaddr - segmentCmd->fileoff; } } else if(loadCmd->cmd == LC_SEGMENT_64) { const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr; if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) { return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff); } } cmdPtr += loadCmd->cmdsize; } return 0; }
該函式被如此呼叫:
const uintptr_t segmentBase = segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
0 迷惑現場
一個 image 中會有多個 segment,引數 idx
傳遞的是 image 的索引,如果返回的是 segment base, 那麼是哪個 segment?
有人會說,註釋裡不是說返回非 0 的話,就表示的是 image base。可是從原理上講 vmaddr - fileoff
根本得不到 image base(後文有解釋)。
而在被呼叫處,加上由 ASLR 引起的偏移,賦值給了 segmentBase。
在 fishhook 中,有這麼一行程式碼:
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
暫不考慮由 ASLR 造成的 slide,那麼又是上邊提到的 vmaddr - fileoff
,這裡的變數命名是 linkedit_base
。
KSCrash 中的所謂的 segmentBase
和 fishhook 中所謂的 linkedit_base
,到底指的是什麼?如果指的是 __LINKEDIT 端在記憶體中的真實地址那應該是 vmaddr + ASLR偏移
在查詢資料的過程中,讀了大量的部落格、資料,對於這一塊的解釋,要麼沒提,要麼一帶而過,要麼是錯的。有的認為這個值是__LINKEDIT 段在記憶體中的基址,有的認為是當前 image 在記憶體中的基址。
1 揭開面紗
1.1 前置知識
在理解這個值到底是什麼之前,我們需要一些前置知識。
- Mach-O 檔案的結構
- 虛擬記憶體
- ASLR
下邊我們簡單的說一下 Mach-O 檔案。
Mach-O
我們知道,程序是可執行檔案在記憶體中載入得到的結果,而 Mach-O 就是一種 macOS 平臺的可執行檔案格式。
Mach-O 檔案分為三個區域 Header、Load commands、Data。其中 Load commands 區的指令指導如何設定並載入二進位制資料。下邊列出 32 位平臺下我們關心的幾個:
指令 | 對應的資料結構 | 描述 |
---|---|---|
LC_SEGMENT | segment_command | 定義了這個檔案中的一個 segment,在 Mach-O 檔案被載入到時,這個 segment 會被對映到對應的地址空間。需要留意,segment_command 中有一個 segname ,可通過 segname 來查詢指定的 segment。 |
LC_SYMTAB | symtab_command | 指定了這個檔案的符號表。symtab_command 中包含符號表在檔案中的偏移、符號數量、字串表在檔案中的偏移、字串表的大小。 |
segment_command 程式碼如下:
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
對於每一個 segment 而言,設定程序虛擬記憶體的過程就是將相應的內容載入到記憶體中,也就是從 Mach-O 檔案的 fileoff 初載入 filesize 位元組到虛擬記憶體地址的 vmaddr 處,佔用 vmsize 位元組。**需要留意,對某些 segment 來說,vmsize 可能會大於 filesize,如__Data、__LINKEDIT。**
在後邊的討論中,我們需要關心的是 segname
為 __LINKEDIT
的段。__LINKEDIT 段由 dyld 使用,包含符號表、字串表以及其他資料。
symtab_command 程式碼如下:
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 */
};
在 symtab_command
中,symoff
為符號表在 Mach-O 檔案中的偏移、stroff
為字串表在 Mach-O 檔案中的偏移。
1.2 揭祕
我們可以使用 MachOView 來開啟一個 Mach-O 檔案,觀察 LC_SEGMENT(__LINKEDIT)、LC_SYMTAB。限於篇幅,這裡就不截圖觀察了。但是你應當留意到符號表、字串表在 Mach-O 檔案的位置,位於 __LINKEDIT 段中,這也驗證了上邊對 __LINKEDIT 段的介紹。
我們從符號表在虛擬記憶體中的地址來倒推上邊那個所謂的 segmentBase
、linkedit_base
,看一張圖(不是很準確,但可以幫助我們搞明白這個問題)。
我們先忽略 ASLR,圖中的深灰色背景表示是虛擬記憶體,__TEXT 段、__DATA 段我們不關心,圖中沒有體現。
sym_vmaddr 是指的是符號表在虛擬記憶體中地址,而在虛擬記憶體中符號表在 __LINKEDIT 段中偏移,即 sym_vmaddr - vmaddr
,與其在 MachO 檔案中的偏移,即 symoff - fileoff
相等。
也就是sym_vmaddr - vmaddr = symoff - fileoff
,
vmaddr 移到右邊,即 sym_vmaddr = symoff - fileoff + vmaddr
發現什麼了嗎?
接著上邊推:
減去符號表偏移symoff:sym_vmaddr - symoff = vmaddr - fileoff
(式1),
式 1 等號右邊的部分加上 ASLR 偏移 slide:vmaddr - fileoff + slide
,也就是所謂的 segmentBase
、linkedit_base
。
至此,真相大白。
參考
深入解析Mac OS X & iOS作業系統
深入理解計算機系統
Mach-O File Format