Mach-O
Mach-O文件格式是 OS X 與 iOS 系統(tǒng)上的可執(zhí)行文件格式,類似于windows的 PE 文件 與 Linux(其他 Unix like)的 ELF 文件。
Mach-O 沒有類似于 XML、YAML、JSON 等諸如此類的特殊格式,它只是一個二進制字節(jié)流,被劃分為了有意義的數(shù)據(jù)塊。這些塊包含元信息,比如字節(jié)順序、cpu 類型、塊的大小等等。
它由3部分組成:
1、Header:保存了Mach-O的一些基本信息,包括了平臺、文件類型、LoadCommands的個數(shù)等等。
2、LoadCommands:這一段緊跟Header,加載Mach-O文件時會使用這里的數(shù)據(jù)來確定內(nèi)存的分布。
3、Data:每一個segment的具體數(shù)據(jù)都保存在這里,這里包含了具體的代碼、數(shù)據(jù)等等。
Mach-O文件的格式如下圖所示:

OS X有兩種類型的目標文件:Mach-O 文件和通用二進制文件,也叫作胖文件。它們之間的區(qū)別是:Mach-O 文件包含一種架構(i386、x86_64、arm64 等等)的對象代碼,而胖文件可能包含若干包含不同架構(i386、x86_64、arm、arm64 等等)對象代碼的對象文件。
Fat Header 文件
胖文件包含不同架構的數(shù)據(jù),不過每一個架構的結構跟Mach-O 文件一樣。
FAT二進制數(shù)據(jù)的 Header數(shù)據(jù)結構定義在 <mach-o/fat.h>里面。
#define FAT_MAGIC 0xcafebabe 32位 大端
#define FAT_CIGAM 0xbebafeca 32位 小端 /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
uint32_t magic; /* FAT_MAGIC or FAT_MAGIC_64 */
uint32_t nfat_arch; /* 結構體實例的個數(shù) */
};
struct fat_arch {
cpu_type_t cputype; /* cpu 說明符 (int) */
cpu_subtype_t cpusubtype; /* 指定 cpu 確切型號的整數(shù) (int) */
uint32_t offset; /* CPU 架構數(shù)據(jù)相對于當前文件開頭的偏移值 */
uint32_t size; /* 數(shù)據(jù)大小 */
uint32_t align; /* 數(shù)據(jù)內(nèi)潤對其邊界,取值為 2 的冪 */
};
/*
* then the 64-bit fat file format is used.
*/
#define FAT_MAGIC_64 0xcafebabf 64位 大端
#define FAT_CIGAM_64 0xbfbafeca 64位 小端 /* NXSwapLong(FAT_MAGIC_64) */
struct fat_arch_64 {
cpu_type_t cputype; /* cpu 說明符 (int) */
cpu_subtype_t cpusubtype; /* 指定 cpu 確切型號的整數(shù) (int) */
uint64_t offset; /* CPU 架構數(shù)據(jù)相對于當前文件開頭的偏移值 */
uint64_t size; /* 數(shù)據(jù)大小 */
uint32_t align; /* 數(shù)據(jù)內(nèi)潤對其邊界,取值為 2 的冪 */
uint32_t reserved; /* 保留位 */
};
大小端
struct fat_header里說 magic 只能取FAT_MAGIC 或者 FAT_MAGIC_64 , 但是為什么這里又來了一個FAT_CIGAM, 這是什么情況?
如果心細的話, 會發(fā)現(xiàn)CIGAM其實就是 MAGIC 反過來寫了而已. 因此這里需要了解一個概念, 就是大小端.
數(shù)據(jù)在內(nèi)存中存儲有2種形式, 一種是高數(shù)值在低內(nèi)存, 另外一種相反則是低數(shù)值在低內(nèi)存, 說起來比較抽象, 直接以 0xabcdef12 來比較大小端存儲情況:
// 大端:
// | 0x00000 | ab | <-- 最大的數(shù)字在低位
// | 0x00008 | cd |
// | 0x00010 | ef |
// | 0x00018 | 12 |
// 小端:
// | 0x00000 | 12 | <-- 最小的數(shù)字在低位
// | 0x00004 | ab |
// | 0x00008 | cd |
// | 0x0000c | ef |
在計算機系統(tǒng)中,我們是以字節(jié)為單位的,每個地址單元都對應著一個字節(jié),一個字節(jié)為8bit。但是在C語言中除了8bit的char之外,還有16bit的short型,32bit的long型(要看具體的編譯器)。另外,對于位數(shù)大于8位的處理器,例如16位或者32位的處理器,由于寄存器寬度大于一個字節(jié),那么必然存在著一個如果將多個字節(jié)安排的問題。因此就導致了大端存儲模式和小端存儲模式。
大小端在不同的架構上是不一樣的。
大端模式,是指數(shù)據(jù)的高字節(jié)保存在內(nèi)存的低地址中,而數(shù)據(jù)的低字節(jié)保存在內(nèi)存的高地址中,這樣的存儲模式有點兒類似于把數(shù)據(jù)當作字符串順序處理:地址由小向大增加,而數(shù)據(jù)從高位往低位放;
小端模式,是指數(shù)據(jù)的高字節(jié)保存在內(nèi)存的高地址中,而數(shù)據(jù)的低字節(jié)保存在內(nèi)存的低地址中,這種存儲模式將地址的高低和數(shù)據(jù)位權有效地結合起來,高地址部分權值高,低地址部分權值低,和我們的邏輯方法一致。

Mach Header 文件
Mach-O 文件的 Header數(shù)據(jù)結構定義在 <mach-o/loader.h>里面。
/* 32-bit architectures */
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 */
};
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
/* 64-bit architectures */
struct mach_header_64 {
uint32_t magic; /* mach magic 標識符 */
cpu_type_t cputype; /* CPU 類型標識符,同通用二進制格式中的定義 */
cpu_subtype_t cpusubtype; /* CPU 子類型標識符,同通用二級制格式中的定義 */
uint32_t filetype; /* 文件類型 */
uint32_t ncmds; /* 加載器中加載命令的條數(shù) */
uint32_t sizeofcmds; /* 加載器中加載命令的總大小 */
uint32_t flags; /* dyld 的標志 */
uint32_t reserved; /* 64 位的保留字段 */
};
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
由于 Mach-O 支持多種類型文件,所以此處引入了 filetype 字段來標明,這些文件類型定義在 loader.h 文件中同樣可以找到。
#define MH_OBJECT 0x1 /* Target 文件:編譯器對源碼編譯后得到的中間結果 */
#define MH_EXECUTE 0x2 /* 可執(zhí)行二進制文件 */
#define MH_FVMLIB 0x3 /* VM 共享庫文件(還不清楚是什么東西) */
#define MH_CORE 0x4 /* Core 文件,一般在 App Crash 產(chǎn)生 */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* 動態(tài)庫 */
#define MH_DYLINKER 0x7 /* 動態(tài)連接器 /usr/lib/dyld */
#define MH_BUNDLE 0x8 /* 非獨立的二進制文件,往往通過 gcc-bundle 生成 */
#define MH_DYLIB_STUB 0x9 /* 靜態(tài)鏈接文件(還不清楚是什么東西) */
#define MH_DSYM 0xa /* 符號文件以及調(diào)試信息,在解析堆棧符號中常用 */
#define MH_KEXT_BUNDLE 0xb /* x86_64 內(nèi)核擴展 */
另外在loader.h中還可以找到 flags 中所取值的全部定義,這里只介紹常用的:
#define MH_NOUNDEFS 0x1 /* Target 文件中沒有帶未定義的符號,常為靜態(tài)二進制文件 */
#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 可執(zhí)行 */
#define MH_PIE 0x200000 /* 對可執(zhí)行的文件類型啟用地址空間 layout 隨機化 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 將 Heap 標記為不可執(zhí)行,可防止 heap spray 攻擊 */
Mach-O 文件頭主要目的是為加載命令提供信息。加載命令過程緊跟在頭之后,并且ncmds和 sizeofcmds 字段將會用在加載命令的過程中。

LoadCommands
在mach_header之后的是Load Command加載命令,這些加載命令在Mach-O文件加載解析時,被內(nèi)核加載器或者動態(tài)鏈接器調(diào)用,基本的加載命令的數(shù)據(jù)結構如下:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
此結構對應的成員只有2個:cmd字段代表當前加載命令的類型。cmdsize字段代表當前加載命令的大小。
cmd的類型不同,所代表的加載命令的類型就不同,它的結構體也會有所不一樣,對于不同類型的加載命令,它們都會在load_command結構體后面加上一個或多個字段來表示自己特定的結構體信息。
macOS系統(tǒng)在進化的過程中,加載命令算是比較頻繁被更新的一個數(shù)據(jù)結構體,截止到macOS 10.13系統(tǒng),加載命令的類型cmd的取值共有53種。
通過查看XNU-4570代碼可以發(fā)現(xiàn)內(nèi)核會處理9個命令,其中還有LC_SEGMENT、LC_ENCRYPTION_INFO,它們是32位的處理方式。
| Command類型 | 處理函數(shù) | 用途 |
|---|---|---|
| LC_SEGMENT_64 | load_segment | 將segment數(shù)據(jù)加載映射到進程的內(nèi)存空間 |
| LC_UNIXTHREAD | load_unixthread | 開啟一個UNIX線程 |
| LC_MAIN | load_main | 加載main函數(shù) |
| LC_LOAD_DYLINKER | load_dylinker | 調(diào)用/usr/lib/dyld程序 |
| LC_UUID | load_uuid | 加載128-bit的唯一ID |
| LC_CODE_SIGNATURE | load_code_signature | 進行數(shù)字簽名 |
| LC_ENCRYPTION_INFO_64 | set_code_unprotect | 加密二進制文件 |
至于內(nèi)核和dyld加載mach-o的流程,這里不多介紹(水平不夠 ??),可以自行查閱相關資料,或者看相關源碼。
LoadCommands結構如下圖:

根據(jù)偏移量找到每一個段區(qū)第一個變量cmd的地址,根據(jù)它的value類型,轉(zhuǎn)成相應的結構體,所以每一個段區(qū)的結構體前面2位都是cmd和cmdsize。
Segment
LC_SEGMENT 意味著這部分文件需要映射到進程的地址空間去,它是是最常見的段結構,這里看下他們的數(shù)據(jù)結構。
段名一般為大寫,節(jié)名一般為小寫,名字前面加2個下劃線__
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]; /* 一個16字節(jié)大小的空間,用來存儲段的名稱 */
uint64_t vmaddr; /* 段要加載的虛擬內(nèi)存地址 */
uint64_t vmsize; /* 段所占的虛擬內(nèi)存的大小 */
uint64_t fileoff; /* 段數(shù)據(jù)所在文件中偏移地址 */
uint64_t filesize; /* 段數(shù)據(jù)實際的大小 */
vm_prot_t maxprot; /* 頁面所需要的最高內(nèi)存保護 */
vm_prot_t initprot; /* 頁面初始的內(nèi)存保護 */
uint32_t nsects; /* 段所包含的節(jié)區(qū)(section) */
uint32_t flags; /* 段的標志信息 */
};
</br>
一個程序編譯后,可執(zhí)行的程序分成了多個段,不同的類型的數(shù)據(jù)放入了不同的段中,如代碼段放入__TEXT。segment_command有幾個常用的段,分別起到不同的作用,定義如下:
| 宏定義 | 名字 | 用途 | 內(nèi)存保護 |
|---|---|---|---|
| SEG_PAGEZERO | __PAGEZERO | 捕捉NULL指針的引用,映射到虛擬內(nèi)存的第一頁 | 0 |
| SEG_TEXT | __TEXT | 包含了執(zhí)行代碼以及其他只讀數(shù)據(jù) | 5 |
| SEG_DATA | __DATA | 程序數(shù)據(jù) | 3 |
| SEG_LINKEDIT | __LINKEDIT | 鏈接器使用的符號以及其他表 | 1 |
</br>
segment_command中的maxprot、initprot是內(nèi)存保護相關的,分別有:
-
VM_PROT_READ(可讀 1) -
VM_PROT_WRITE(可寫 2) -
VM_PROT_EXECUTE(可執(zhí)行 4)
每個段具體的值都是由他們組合相加得到的,但LC_SEGMENT_64(__PAGEZERO)段比較特殊,2個字段的值都是VM_PROT_NONE(0)。
section
通過注釋A segment is made up of zero or more sections可知,segment里面還可以包含sections,下面先看下sections的數(shù)據(jù)結構:
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 的內(nèi)存對齊邊界 (2 的次冪) */
uint32_t reloff; /* 重定位入口的文件偏移 */
uint32_t nreloc; /* 需要重定位的入口數(shù)量 */
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 */
};
segment_command結構中nsects字段標識含有多少個section,它是具體有用的數(shù)據(jù)存放的地方。下面列舉幾個常見的section:
| segment | 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 類名稱 |
| __DATA | __data | 初始化過的可變數(shù)據(jù) |
| __DATA | __la_symbol_ptr | lazy binding 的指針表,表中的指針一開始都指向 __stub_helper |
| __DATA | __nl_symbol_ptr | 非 lazy binding 的指針表,每個表項中的指針都指向一個在裝載過程中,被動態(tài)鏈機器搜索完成的符號 |
| __DATA | __const | 沒有初始化過的常量 |
| __DATA | __cfstring | 程序中使用的 Core Foundation 字符串(CFStringRefs) |
| __DATA | __bss | BSS,存放為初始化的全局變量,即常說的靜態(tài)內(nèi)存分配 |
| __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_ivar | Objective-C 成員變量 |
| __DATA | __objc_catlist | Objective-C 分類 |
| __DATA | __objc_nlclslist | Objective-C 重寫+load方法的類 |
| __DATA | __objc_nlcatlist | Objective-C 重寫+load方法的分類 |