Mach-O
Mach-O是Mach Object文件格式的縮寫罐盔。它是用于可執(zhí)行文件担平,動態(tài)庫,目標代碼的文件格式芒率。作為a.out格式的替代囤耳,Mach-O格式提供了更強的擴展性,以及更快的符號表信息訪問速度偶芍。
Mach-O格式為大部分基于Mach內(nèi)核的操作系統(tǒng)所使用的充择,包括NeXTSTEP, Mac OS X和iOS,它們都以Mach-O格式作為其可執(zhí)行文件匪蟀,動態(tài)庫椎麦,目標代碼的文件格式。
具體到我們的iOS程序材彪,當用XCode打包后观挎,會生成一個.app為擴展名的文件(位于工程目錄/Products文件夾下),其實.app是一個文件夾段化,我們用鼠標右鍵選擇‘Show Package contents’嘁捷,就可以查看文件夾的內(nèi)容,其中會發(fā)現(xiàn)有一個和我們工程同名的unix 可執(zhí)行文件显熏,這個就是iOS可執(zhí)行文件雄嚣,它是符合Mach-O格式的。
Mach-O文件結構
關于Mach-O的文件格式佃延,在蘋果官網(wǎng)已經(jīng)找不到相關說明了现诀,但是你可以通過下面鏈接獲取PDF版說明:
Mach-O File Format Reference
Mach-O格式如下圖所示,它被分為header履肃, load commands仔沿, data三大部分:
header:對Mach-O文件的一個概要說明,包括Magic Number, 支持的CPU類型等尺棋。
load commands: 當系統(tǒng)加載Mach-O文件時封锉,load command會指導蘋果的動態(tài)加載器(dyld)h或內(nèi)核,該如何加載文件的Data數(shù)據(jù)膘螟。
data: Mach-O文件的數(shù)據(jù)區(qū)成福,包含代碼和數(shù)據(jù)。其中包含若干Segment塊(注意荆残,除了Segment塊之外奴艾,還有別的內(nèi)容,包括code signature内斯,符號表之類蕴潦,不要被蘋果的圖所誤導O裉洹),每個Segment塊中包含0個或多個seciton潭苞。Segment根據(jù)對應的load command被dyld加載入內(nèi)存中忽冻。
我們可以使用MachOView(一個查看MachO 格式文件信息的開源工具)工具來查看一個具體的文件的Mach-O格式。
header
我們以一個普通的iOS APP為例此疹,看看Mach-O文件header部分的具體內(nèi)容僧诚。通過MachOView打開可執(zhí)行文件,可以看到header的結構:
是不是有些懵蝗碎?下面我們就結合Darwin內(nèi)核源碼湖笨,來了解下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中多了一個保留參數(shù)reserved肩豁。
- magic:魔數(shù)脊串,用來標識這是一個Mach-O文件,有32位和64位兩個版本:
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
- cputype:支持的CPU架構類型清钥,如arm琼锋。
- cpusubtype:在支持的CPU架構類型下,所支持的具體機器型號祟昭。在我們的例子中缕坎,APP是支持所有arm64的機型的:CUP_SUBTYPE_ARM64_ALL
- filetype: Mach-O的文件類型。包括:
#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)核擴展 */
- ncmds:load command的數(shù)量
- sizeofcmds: 所有l(wèi)oad command的大小
- flags: Mach-O文件的標志位搬葬。主要作用是告訴系統(tǒng)該如何加載這個Mach-O文件以及該文件的一些特性荷腊。有很多值,我們?nèi)〕R姷膸追N:
#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 /* 加載程序在隨機的地址空間女仰,只在 MH_EXECUTE中使用 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 將 Heap 標記為不可執(zhí)行,可防止 heap spray 攻擊 */
MH_PIE 隨機地址空間
每次系統(tǒng)加載進程后抡锈,都會為其隨機分配一個虛擬內(nèi)存空間疾忍。
在傳統(tǒng)系統(tǒng)中,進程每次加載的虛擬內(nèi)存是相同的床三。這就讓黑客有可能篡改內(nèi)存來破解軟件一罩。
dyld
dyld是蘋果公司的動態(tài)鏈接庫,用來把Mach-O文件加載入內(nèi)存撇簿。
二級命名空間
表示其符號空間中還會包含所在庫的信息聂渊。這樣可以使得不同的庫導出通用的符號推汽。與其相對的是扁平命名空間。
Load commands
load commands 緊跟在header之后歧沪,用來告訴內(nèi)核和dyld歹撒,如何將各個Segment加載入內(nèi)存中。
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 加載入內(nèi)存邪码,對應segment command類型
#define LC_SEGMENT 0x1
#define LC_SEGMENT_64 0x19
// UUID, 2進制文件的唯一標識符
#define LC_UUID 0x1b
// 啟動動態(tài)加載器dyld
#define LC_LOAD_DYLINKER 0xe
Segment load command
在這么多的load command中裕菠,需要我們重點關注的是segment load command。segment command解釋了該如何將Data中的各個Segment加載入內(nèi)存中闭专,而和我們APP相關的邏輯及數(shù)據(jù)奴潘,則大部分位于各個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位的結構中平委,把和尋址相關的數(shù)據(jù)類型奈虾,由32位的uint32_t改為了64位的uint64_t類型。
結構體的定義廉赔,看注釋基本能夠看懂肉微,就是maxprot, initprot不太明白啥意思蜡塌。
這里介紹一個特殊的‘Segment’碉纳,叫做__PAGEZERO Segment。 這里說它特殊岗照,是因為這個Segment其實是蘋果虛擬出來的村象,只是一個邏輯上的段,而在Data中攒至,根本沒有對應的內(nèi)容厚者,也沒有占用任何硬盤空間。
__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中,程序的邏輯和數(shù)據(jù)是按照Segment(段)存儲烫止,在Segment中蒋荚,又分為0或多個section,每個section中在存儲實際的內(nèi)容馆蠕。而之所以這么做的原因在于期升,在section中,可以不用內(nèi)存對齊達到節(jié)約內(nèi)存的作用互躬,而所有的section作為整體的Segment播赁,又可以整體的內(nèi)存對齊。
在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 二進制數(shù)據(jù)的地方房维,前面的header和load command沼瘫,僅是提供文件的說明以及加載信息的功能。
Data部分也被分為若干的部分咙俩,除了我們前面提到的Segment外,還包括符號表湿故,代碼簽名阿趁,動態(tài)加載器信息等。
而程序的邏輯和數(shù)據(jù)坛猪,則是放在以Segment分割的Data部分中的脖阵。我們在這里,僅關心Data中的Segment的部分墅茉。
Segment根據(jù)內(nèi)容的不同命黔,分為若干類型,類型名稱均是以“雙下劃線+大寫英文”表示就斤,有的Segment下面還會包含若干的section悍募,section的命名是以”雙下劃線+小寫英文”表示。
先來看Segment洋机,Mach-O中有如下幾種Segment:
#define SEG_PAGEZERO "__PAGEZERO" /* 當時 MH_EXECUTE 文件時坠宴,表示空指針區(qū)域 */
#define SEG_TEXT "__TEXT" /* 代碼/只讀數(shù)據(jù)段 */
#define SEG_DATA "__DATA" /* 數(shù)據(jù)段 */
#define SEG_OBJC "__OBJC" /* Objective-C runtime 段 */
#define SEG_LINKEDIT "__LINKEDIT" /* 包含需要被動態(tài)鏈接器使用的符號和其他表,包括符號表绷旗、字符串表等 */
這里面注意到到SEG_OBJC喜鼓,是和OC的runtime相關的副砍。但是根據(jù)這篇文章
中說所,在OC 2.0中已經(jīng)廢棄掉__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”):
值得注意的是熏版,這些都是以明文形式展現(xiàn)的纷责。如果我們將加密key用字符串常量或宏定義的形式存儲在程序中,可以想象其安全性是得不到保障的撼短。
__DATA段
__DATA段用于存儲程序中所定義的數(shù)據(jù)再膳,可讀寫。__DATA段下常見的sectin有:
Section | 用途 |
---|---|
__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 超類引用 |
我們將在后續(xù)的文章中昭娩,繼續(xù)探討這些section和runtime的關系。
總結
這次我們一起了解了XNU內(nèi)核下的二進制文件格式Mach-O黍匾。它由header栏渺,load command以及data三部分組成:
我們重點應該了解的應該是data部分,因為這里存儲著我們程序真正的數(shù)據(jù)和代碼膀捷。
在data部分中迈嘹,又區(qū)分為以Segment劃分的部分以及代碼簽名等其他部分。
在Segment下,有區(qū)分有若干的section秀仲。
常用的Segment有__PAGE_ZERO, __TEXT, __DATA(注意區(qū)分Mach-O的data和這里的__DATA段名稱)融痛。
參考資料
趣探 Mach-O:文件格式分析
深入理解Macho文件(二)- 消失的__OBJC段與新生的__DATA段
mach-o格式分析
Mach-O 文件格式探索
Mach-O 維基百科