Objective-C runtime機制(前傳)——Mach-O格式
Mach-O
Mach-O是Mach Object檔案格式的縮寫。它是用於可執行檔案,動態庫,目的碼的檔案格式。作為a.out格式的替代,Mach-O格式提供了更強的擴充套件性,以及更快的符號表資訊訪問速度。
Mach-O格式為大部分基於Mach核心的作業系統所使用的,包括NeXTSTEP, Mac OS X和iOS,它們都以Mach-O格式作為其可執行檔案,動態庫,目的碼的檔案格式。
具體到我們的iOS程式,當用XCode打包後,會生成一個.app為副檔名的檔案(位於工程目錄/Products資料夾下),其實.app是一個資料夾,我們用滑鼠右鍵選擇‘Show Package contents’,就可以檢視資料夾的內容,其中會發現有一個和我們工程同名的unix 可執行檔案,這個就是iOS可執行檔案,它是符合Mach-O格式的。
Mach-O檔案結構
關於Mach-O的檔案格式,在蘋果官網已經找不到相關說明了,但是你可以通過下面連結獲取PDF版說明:
Mach-O File Format Reference
Mach-O格式如下圖所示,它被分為header, load commands, data三大部分:
header:對Mach-O檔案的一個概要說明,包括Magic Number, 支援的CUP型別等。
load commands: 當系統載入Mach-O檔案時,load command會指導蘋果的動態載入器(dyld)h或核心,該如何載入檔案的Data資料。
data: Mach-O檔案的資料區,包含程式碼和資料。其中包含若干Segment塊(注意,除了Segment塊之外,還有別的內容,包括code signature,符號表之類,不要被蘋果的圖所誤導!
我們可以使用MachOView(一個檢視MachO 格式檔案資訊的開源工具)工具來檢視一個具體的檔案的Mach-O格式。
header
我們以一個普通的iOS APP為例,看看Mach-O檔案header部分的具體內容。通過MachOView開啟可執行檔案,可以看到header的結構:
是不是有些懵?下面我們就結合Darwin核心原始碼,來了解下Mach header的定義。
Mach header的定義位於Darwin原始碼中的 EXTERNAL_HEADERS/mach-o/loader.h
32位:
struct mach_header {
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 */
};
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 */
};
可以看到,32位和64位的Mach header基本相同,只不過64位header中多了一個保留引數reserved。
- magic:魔數,用來標識這是一個Mach-O檔案,有32位和64位兩個版本:
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
- cputype:支援的CUP架構型別,如arm。
- cpusubtype:在支援的CUP架構型別下,所支援的具體機器型號。在我們的例子中,APP是支援所有arm64的機型的:CUP_SUBTYPE_ARM64_ALL
- filetype: Mach-O的檔案型別。包括:
#define MH_OBJECT 0x1 /* Target 檔案:編譯器對原始碼編譯後得到的中間結果 */
#define MH_EXECUTE 0x2 /* 可執行二進位制檔案 */
#define MH_FVMLIB 0x3 /* VM 共享庫檔案(還不清楚是什麼東西) */
#define MH_CORE 0x4 /* Core 檔案,一般在 App Crash 產生 */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* 動態庫 */
#define MH_DYLINKER 0x7 /* 動態聯結器 /usr/lib/dyld */
#define MH_BUNDLE 0x8 /* 非獨立的二進位制檔案,往往通過 gcc-bundle 生成 */
#define MH_DYLIB_STUB 0x9 /* 靜態連結檔案(還不清楚是什麼東西) */
#define MH_DSYM 0xa /* 符號檔案以及除錯資訊,在解析堆疊符號中常用 */
#define MH_KEXT_BUNDLE 0xb /* x86_64 核心擴充套件 */
- ncmds:load command的數量
- sizeofcmds: 所有load command的大小
- flags: Mach-O檔案的標誌位。主要作用是告訴系統該如何載入這個Mach-O檔案以及該檔案的一些特性。有很多值,我們取常見的幾種:
#define MH_NOUNDEFS 0x1 /* Target 檔案中沒有帶未定義的符號,常為靜態二進位制檔案 */
#define MH_SPLIT_SEGS 0x20 /* Target 檔案中的只讀 Segment 和可讀寫 Segment 分開 */
#define MH_TWOLEVEL 0x80 /* 該 Image 使用二級名稱空間(two name space binding)繫結方案 */
#define MH_FORCE_FLAT 0x100 /* 使用扁平名稱空間(flat name space binding)繫結(與 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES 0x8000 /* 二進位制檔案使用了弱符號 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二進位制檔案連結了弱符號 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允許 Stack 可執行 */
#define MH_PIE 0x200000 /* 載入程式在隨機的地址空間,只在 MH_EXECUTE中使用 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 將 Heap 標記為不可執行,可防止 heap spray 攻擊 */
MH_PIE 隨機地址空間
每次系統載入程序後,都會為其隨機分配一個虛擬記憶體空間。
在傳統系統中,程序每次載入的虛擬記憶體是相同的。這就讓黑客有可能篡改記憶體來破解軟體。
dyld
dyld是蘋果公司的動態連結庫,用來把Mach-O檔案載入入記憶體。
二級名稱空間
表示其符號空間中還會包含所在庫的資訊。這樣可以使得不同的庫匯出通用的符號。與其相對的是扁平名稱空間。
Load commands
load commands 緊跟在header之後,用來告訴核心和dyld,如何將各個Segment載入入記憶體中。
load command被源碼錶示為struct,有若干種load command,但是共同的特點是,在其結構的開頭處,必須是如下兩個屬性:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
蘋果為cmd定義了若干的巨集,用來表示cmd的型別,下面列舉出幾種:
// 描述該如何將32或64位的segment 載入入記憶體,對應segment command型別
#define LC_SEGMENT 0x1
#define LC_SEGMENT_64 0x19
// UUID, 2進位制檔案的唯一識別符號
#define LC_UUID 0x1b
// 啟動動態載入器dyld
#define LC_LOAD_DYLINKER 0xe
Segment load command
在這麼多的load command中,需要我們重點關注的是segment load command。segment command解釋了該如何將Data中的各個Segment載入入記憶體中,而和我們APP相關的邏輯及資料,則大部分位於各個Segment中。
而和我們的Run time相關的Segment,則位於__DATA型別Segment下。
Segment load command分為32位和64位:
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 */
};
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_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 */
};
32位和64位的Segment load command基本類似,只不過在64位的結構中,把和定址相關的資料型別,由32位的uint32_t改為了64位的uint64_t型別。
結構體的定義,看註釋基本能夠看懂,就是maxprot, initprot不太明白啥意思。
這裡介紹一個特殊的‘Segment’,叫做__PAGEZERO Segment。 這裡說它特殊,是因為這個Segment其實是蘋果虛擬出來的,只是一個邏輯上的段,而在Data中,根本沒有對應的內容,也沒有佔用任何硬碟空間。
__PAGEZERO Segment在VM中被置為Read only,邏輯上佔用APP最開始的4GB空間,用來處理空指標。
我們用MachOV點開__PAGEZERO Segment所對應的Segment load command,LC_SEGMENT_64(__PAGEZERO):
可以看到其vm size是4GB,但其真正的實體地址File size和offset都是0。
Section header
在Data中,程式的邏輯和資料是按照Segment(段)儲存,在Segment中,又分為0或多個section,每個section中在儲存實際的內容。而之所以這麼做的原因在於,在section中,可以不用記憶體對齊達到節約記憶體的作用,而所有的section作為整體的Segment,又可以整體的記憶體對齊。
在Mach-O檔案中,每一個Segment load command下面,都會包含對應Segment 下所有section的header。
section header的定義如下:
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
這樣,關於load commonds部分,其真正的結構其實和蘋果提供的圖片有些許的差異:
Data
Mach-O的Data部分,其實是真正儲存APP 二進位制資料的地方,前面的header和load command,僅是提供檔案的說明以及載入資訊的功能。
Data部分也被分為若干的部分,除了我們前面提到的Segment外,還包括符號表,程式碼簽名,動態載入器資訊等。
而程式的邏輯和資料,則是放在以Segment分割的Data部分中的。我們在這裡,僅關心Data中的Segment的部分。
Segment根據內容的不同,分為若干型別,型別名稱均是以“雙下劃線+大寫英文”表示,有的Segment下面還會包含若干的section,section的命名是以”雙下劃線+小寫英文”表示。
先來看Segment,Mach-O中有如下幾種Segment:
#define SEG_PAGEZERO "__PAGEZERO" /* 當時 MH_EXECUTE 檔案時,表示空指標區域 */
#define SEG_TEXT "__TEXT" /* 程式碼/只讀資料段 */
#define SEG_DATA "__DATA" /* 資料段 */
#define SEG_OBJC "__OBJC" /* Objective-C runtime 段 */
#define SEG_LINKEDIT "__LINKEDIT" /* 包含需要被動態連結器使用的符號和其他表,包括符號表、字串表等 */
這裡面注意到到SEG_OBJC,是和OC的runtime相關的。但是根據這篇文章中說所,在OC 2.0中已經廢棄掉__OBJC段,而是將其放入到了__DATA段中以__objc開頭的section中。這些和runtime相關的sections是本文的終點,我們稍後再分析。我們先看看其他的段。
__TEXT段
__TEXT是程式的只讀段,用於儲存我們所寫的程式碼和字串常量,const修飾常量等。
下面是__TEXT段下常見的section:
Section | 用途 |
---|---|
__TEXT.__text | 主程式程式碼 |
__TEXT.__cstring | C 語言字串 |
__TEXT.__const | const 關鍵字修飾的常量 |
__TEXT.__stubs | 用於 Stub 的佔位程式碼,很多地方稱之為樁程式碼。 |
__TEXT.__stubs_helper | 當 Stub 無法找到真正的符號地址後的最終指向 |
__TEXT.__objc_methname | Objective-C 方法名稱 |
__TEXT.__objc_methtype | Objective-C 方法型別 |
__TEXT.__objc_classname | Objective-C 類名稱 |
例如,我們點選__TEXT.__objc_classname, 會看到我們程式中所使用到的類的名稱:
而在__TEXT.__cstring section中,則看到我們定義的字串常量(如@”I’m a cat!! miao miao”):
值得注意的是,這些都是以明文形式展現的。如果我們將加密key用字串常量或巨集定義的形式儲存在程式中,可以想象其安全性是得不到保障的。
__DATA段
__DATA段用於儲存程式中所定義的資料,可讀寫。__DATA段下常見的sectin有:
Section | 用途 |
---|---|
__DATA.__data | 初始化過的可變資料 |
__DATA.__la_symbol_ptr | lazy binding 的指標表,表中的指標一開始都指向 __stub_helper |
__DATA.nl_symbol_ptr | 非 lazy binding 的指標表,每個表項中的指標都指向一個在裝載過程中,被動態鏈機器搜尋完成的符號 |
__DATA.__const | 沒有初始化過的常量 |
__DATA.__cfstring | 程式中使用的 Core Foundation 字串(CFStringRefs) |
__DATA.__bss | BSS,存放為初始化的全域性變數,即常說的靜態記憶體分配 |
__DATA.__common | 沒有初始化過的符號宣告 |
__DATA.__objc_classlist | Objective-C 類列表 |
__DATA.__objc_protolist | Objective-C 原型 |
__DATA.__objc_imginfo | Objective-C 映象資訊 |
__DATA.__objc_selfrefs | Objective-C self 引用 |
__DATA.__objc_protorefs | Objective-C 原型引用 |
__DATA.__objc_superrefs | Objective-C 超類引用 |
可見,在__DATA段下,有許多以__objc開頭的section,而這些section,均是和runtime的載入有關的。
我們將在後續的文章中,繼續探討這些section和runtime的關係。
總結
這次我們一起了解了XNU核心下的二進位制檔案格式Mach-O。它由header,load command以及data三部分組成:
我們重點應該瞭解的應該是data部分,因為這裡儲存著我們程式真正的資料和程式碼。
在data部分中,又區分為以Segment劃分的部分以及程式碼簽名等其他部分。
在Segment下,有區分有若干的section。
常用的Segment有__PAGE_ZERO, __TEXT, __DATA(注意區分Mach-O的data和這裡的__DATA段名稱)。
參考資料
趣探 Mach-O:檔案格式分析
深入理解Macho檔案(二)- 消失的__OBJC段與新生的__DATA段
mach-o格式分析
Mach-O 檔案格式探索
Mach-O 維基百科