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 段的介紹。

我們從符號表在虛擬內存中的地址來倒推上邊那個所謂的 segmentBaselinkedit_base,看一張圖(不是很准確,但可以幫助我們搞明白這個問題)。

Mach-O Map

我們先忽略 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,也就是所謂的 segmentBaselinkedit_base

至此,真相大白。

參考

  • 深入解析Mac OS X & iOS操作系統

  • 深入理解計算機系統

  • Mach-O File Format

  • The Mac Hacker's Handbook


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM