之前在項(xiàng)目中使用 fishhook 來替換系統(tǒng)的 C 函數(shù)厂榛,其中涉及到很多和 iOS 系統(tǒng)相關(guān)的編譯、鏈接等方面的知識,由于內(nèi)容比較多,所以打算分幾篇文章來進(jìn)行講解瞳收,本文主要是分析 Mach—O 文件。
在 macOS 以及 iOS 系統(tǒng)中厢汹,可執(zhí)行文件的格式為 Mach-O螟深,理解 Mach-O 文件格式對于我們探究操作系統(tǒng)的運(yùn)作機(jī)制起著關(guān)鍵的作用。Mach-O 文件格式如下圖所示烫葬,它由 Header界弧、Load commands 以及 Data 三部分組成:
Header:記錄 Mach-O 文件的基本信息,包括文件類型搭综、支持的 CPU 類型以及加載命令的個(gè)數(shù)垢箕、大小等。
Load commands: 位于 Header 之后兑巾,向操作系統(tǒng)描述如何解析文件条获。
Data: 用于保存程序的 TEXT、DATA蒋歌、LINKEDIT 等 segment 數(shù)據(jù)帅掘。
以下是人民群眾喜聞樂見的一段代碼:
#include <stdio.h>
int main(int argc, const char * argv[]) {
printf("Hello, World!\n");
return 0;
}
使用 clang main.c -o MachOExplore
命令編譯上述代碼,我們得到名為 MachOExplore 的目標(biāo)文件堂油,也就是本文將要探究的 Mach-O 文件修档。otool 是用來查看 Mach-O 文件的常用工具,但是本文會使用另一種工具 MachOVie?w 來完成任務(wù)府框。
Header
MachOView 查看 Header 的內(nèi)容如下:
Header 中記錄了 Mach-O 文件的屬性信息吱窝,相關(guān)數(shù)據(jù)結(jié)構(gòu)定義在 loader.h ????中,分為32位和64位兩種:
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 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) */
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
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 */
};
/* 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) */
下表描述了各個(gè)字段的含義,mach_header_64 除了比 mach_header 多出一個(gè) reserved 字段外院峡,其他方面并無區(qū)別兴使。
字段 | 說明 |
---|---|
magic | 表明文件適用于64位操作系統(tǒng)還是32位操作系統(tǒng),因?yàn)榇笮《说拇嬖谒河瑁杂? MH_MAGIC_64 和 MH_CIGAM_64 兩種形式 。 |
cputype | 描述文件所支持的 CPU 架構(gòu)蜈首,包括 ARM64实抡、X86_64、i386等欢策。 |
cpusubtype | 描述文件對應(yīng)的具體 CPU 架構(gòu)吆寨,例如 ARM_V7S、ARM64_V8踩寇、X86_ARCH1 等啄清。 |
filetype | 描述文件的類型,常見的類型有可執(zhí)行文件俺孙、可重定位文件辣卒、共享庫文件等。 |
ncmds | 記錄加載命令的個(gè)數(shù)睛榄。 |
sizeofcmds | 記錄所有加載命令的大小荣茫。 |
flags | 描述文件在編譯、鏈接等過程中的信息场靴,示例中的 MH_NOUNDEFS 表示文件中不存在未定義的符號啡莉,MH_DYLDLINK 表示文件要交由 DYLD 進(jìn)一步處理,MH_TWOLEVEL 表示文件使用兩級命名空間旨剥,MH_PIE 表示啟用地址空間布局隨機(jī)化咧欣。 |
Load commands
Load commands 緊隨在 Header 后,它包含了一系列的加載命令轨帜,目的是向操作系統(tǒng)描述如何處理 Mach-O 文件魄咕。示例中包含的加載命令如下:
LC_SEGMENT_64
命令表示將相應(yīng)的 segment 映射到虛擬地址空間中,以 LC_SEGMENT_64(__PAGEZERO)
為例:
Command: 表示加載命令;
Command Size: 表示加載命令的大小;
Segment Name: 被加載的段的名字;
VM Address: 段所在的虛擬空間地址;
VM Size: 段所占用的虛擬空間的大小;
File Offset: 段在文件中的偏移量;
File Size: 段在文件中的大小;
Maximum VM protection: 表示與段相對應(yīng)的最大操作權(quán)限;
Initial VM protection: 表示段的初始操作權(quán)限;
Number of Sections: 段包含多少個(gè) Section;
Flags: 描述與段相關(guān)的加載信息蚌父,具體解釋請參考 loader.h 文件;
所以上述命令是將 __PAGEZERO
段映射到虛擬地址 0x0 處蚕礼,占用虛擬空間大小為 4GB,但是這4GB并不是真實(shí)的文件大小梢什,它僅表明將虛擬地址空間的前4GB映射為不可讀奠蹬、不可寫、不可執(zhí)行嗡午,與 NULL 指針相對應(yīng)囤躁。如果程序試圖訪問 __PAGEZERO
段,那么將會引起系統(tǒng)的崩潰。
接下來我們來看 LC_SEGMENT_64(__TEXT)
:
它將 __TEXT
段映射到虛擬地址空間 0x100000000 處狸演,也就是緊隨 __PAGEZERO
段言蛇,占用虛擬空間大小為 4096B,所對應(yīng)的權(quán)限是可讀宵距、可執(zhí)行腊尚、不可寫入。我們的 __TEXT
段包含以下5個(gè) Section:
__text
: 包含程序的機(jī)器碼;__stubs
和__stub_helper
: 用來幫助 DYLD 綁定符號;__cstring
: 記錄了文件中的常量字符串信息(包含在雙引號中)满哪,我們可以依據(jù)此信息找到字符串的地址;__unwind_info
: 用于確定異常發(fā)生時(shí)棧所對應(yīng)的信息婿斥,包括棧指針、返回地址哨鸭、寄存器信息等民宿,它同樣包含相應(yīng)的處理函數(shù)來支持像 catch、final 等特性;
LC_SEGMENT_64(__DATA)
的作用是將 __DATA
段映射到緊隨 __TEXT
段的虛擬地址空間上像鸡,它包含兩個(gè) Section:
__nl_symbol_ptr Section
包含的符號指針需要在加載時(shí)綁定活鹰,而 __la_symbol_ptr Section
包含的符號指針則是在其第一次被程序使用時(shí)綁定。
LC_SEGMENT_64(__LINKEDIT)
則是將與動態(tài)鏈接相關(guān)的信息映射到虛擬地址空間只估,__LINKEDIT
段包括 rebase志群、bind、lazy bind 等信息蛔钙。
LC_DYLD_INFO_ONLY
記錄了有關(guān)鏈接的重要信息赖舟,它的數(shù)據(jù)結(jié)構(gòu)如下:
struct dyld_info_command {
uint32_t cmd; /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
uint32_t cmdsize; /* sizeof(struct dyld_info_command) */
uint32_t rebase_off; /* file offset to rebase info */
uint32_t rebase_size; /* size of rebase info */
uint32_t bind_off; /* file offset to binding info */
uint32_t bind_size; /* size of binding info */
uint32_t weak_bind_off; /* file offset to weak binding info */
uint32_t weak_bind_size; /* size of weak binding info */
uint32_t lazy_bind_off; /* file offset to lazy binding info */
uint32_t lazy_bind_size; /* size of lazy binding infs */
uint32_t export_off; /* file offset to lazy binding info */
uint32_t export_size; /* size of lazy binding infs */
};
根據(jù)它所記錄的偏移量,我們便可以找到在 Dynamic Loader Info 中的相關(guān)信息夸楣。它的 ONLY 后綴表明這是程序運(yùn)行所必須的宾抓,如果鏈接器不支持,那么加載過程就會終止豫喧。
LC_SYMTAB
記錄了程序的符號表以及字符串表的偏移量及大小石洗,符號表中記錄了程序用到的函數(shù)以及全局變量的信息,符號表?xiàng)l目的數(shù)據(jù)結(jié)構(gòu)定義在 nlist.h 中:
/*
* Format of a symbol table entry of a Mach-O file for 32-bit architectures.
*/
struct nlist {
union {
#ifndef __LP64__
char *n_name; /* for use when in-core */
#endif
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
int16_t n_desc; /* see <mach-o/stab.h> */
uint32_t n_value; /* value of this symbol (or stab offset) */
};
/*
* This is the symbol table entry structure for 64-bit architectures.
*/
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
數(shù)據(jù)結(jié)構(gòu)中相關(guān)字段的含義都可以在 nlist.h 中找到紧显,這里值得一說的是 n_un 字段讲衫,它用來記錄符號的名字,但為什么是 uint32_t 類型呢孵班?又為什么在注釋中標(biāo)明是 string table 的 索引呢涉兽?
這是因?yàn)樵诔绦蛑校址拈L度是不固定的篙程,所以會將其放在 string table 中枷畏,然后存儲它在 string table 中的偏移。如果其他部分想要引用某個(gè)字符串虱饿,那么他首先需要找到 string table 的起始地址拥诡,然后根據(jù)偏移量找到相應(yīng)字符串的起始位置并向后讀取字符触趴,直到遇見 \0
才會停止讀取過程,最后返回讀到的字符串渴肉。
這也是 LC_SYMTAB
額外記錄 string table 地址的原因冗懦,string table 通常用于記錄 section 名、符號名等信息仇祭。
其他的加載命令如下表所示:
加載命令 | 描述 |
---|---|
LC_DYSYMTAB | 包括動態(tài)鏈接過程中所需要的信息 |
LC_LOAD_DYLINKER | 指定動態(tài)鏈接器的地址 |
LC_LOAD_DYLIB | 記錄了程序所需要的動態(tài)庫的相關(guān)信息 |
LC_UUID | 靜態(tài)鏈接器為其生成的文件所提供的唯一標(biāo)識符 |
LC_MAIN | 指定 main 函數(shù)的地址 |
LC_FUNCTION_STARTS | 記錄文件中每個(gè)函數(shù)的起始地址 |
LC_DATA_IN_CODE | 記錄那些寫在程序二進(jìn)制執(zhí)行指令中的數(shù)據(jù) |
LC_CODE_SIGNATURE | 代碼簽名 |
Data
Data 包括文件所需的 segment 數(shù)據(jù)披蕉,除了 MachOView 工具,你也可以通過 size 工具來查看乌奇,運(yùn)行 size -x -l -m MachOExplore
命令后可得到以下內(nèi)容:
我們還可以通過輸入 otool -s __DATA __la_symbol_ptr MachOExplore
命令來查看 __la_symbol_ptr
Section 的數(shù)據(jù):
也可以使用 otool -V -s __TEXT __text MachOExplore
命令來查看 __text
Section 的反匯編代碼:
同時(shí) otool 也為一些常見的命令設(shè)置了縮寫没讲,例如 -t
就是 -s __TEXT __text
簡稱,而 -d
就是 -s __DATA __data
的簡稱华弓,相關(guān)信息都可以通過 man otool
命令來查看食零。這些數(shù)據(jù)都可以在 MachOView 中看得很清楚困乒,但是這些命令行工具記一下也無妨寂屏。
總結(jié)
以上便是 Mach-O 文件的探究(其實(shí)總結(jié)部分就是強(qiáng)行湊的(??????)??)。