一、概述
運行時架構(runtime architecture)是針對軟件運行環(huán)境定義的一系列規(guī)則,包括但不限于:
- 如何為代碼和數據(code and data)排位;
- 在內存中怎樣去加載或者追蹤程序的部分代碼;
- 告訴編譯器應該如何組裝代碼;
- 如何調用系統(tǒng)服務,如加載插件;
Mac 系統(tǒng)支持多種運行時架構,但是內核可以直接讀取的可執(zhí)行文件只有一種:Mach-O。因此,mac 的運行時架構也被命名為:Mach-O Runtime Architecture;因此,Mach-O 是一種存儲標準,用于 Mach-O runtime architecture 架構中對程序的磁盤存儲;
Mach-O 是 mach object 的縮寫,在 -objc解決分類不加載的問題的官方文檔中,明確指出所有的源文件都會被轉化成一個 objcet,只不過最后經過鏈接操作,工程或被轉化成靜態(tài)庫、動態(tài)庫或者是可執(zhí)行文件(類型不同的 mach-O);
Mach-O 文件分為三大部分:
- mach-header;
- load commands;
- segment and section;
二、mach_header
header 位于 Mach-O 文件的頭部,其作用是:
- 識別 Mach-O 的格式;
- 文件類型;
- CPU 架構信息;
64 位 header 結構體如下:
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 */
};
1. magic
一個整數,用于標識該文件為 Mach-O 類型??梢岳斫獬啥喾N類型的文件會被加載,而該 Image 如果值為特定的值,則該 Image 為 Mach-O 類型。
另外,如果該 Mach-O 的架構和編譯該 Mach-O 文件的 CPU 字節(jié)序(大小端)一致,則使用 MH_MAGIC,相反則使用 MH_CIGAM;
32 和 64 位為固定的值:
/* 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) */
/* 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) */
如 dyld 源碼中使用這個字段來判斷是否為 Mach-O 文件:

2. cputype
一個整數,標志該文件將被使用在何種 CPU 架構上;
定義在如下文件中:

部分 type 如下:

3. subtype:
arm 架構下有 arm_v7、arm_all 之類的區(qū)別,而 subtype 就是表示這個,部分定義如下:

4. filetype
filetype 就是我們熟知的 Mach-O 文件的類型,比如動態(tài)庫、主工程生成可執(zhí)行文件、bundle 等等,部分 type 如下:

舉個例子??:

如上圖,主工程生成的可執(zhí)行文件就是 MH_EXECUTE、動態(tài)庫則為 MH_DYLIB、ViewController.o 則為 MH_OBJECT,而 dyld 鏈接器 則為 MH_DYLINKER;
需要注意的是,靜態(tài)庫只是一個 mach-o object 的集合:

關于 fat 的格式和靜態(tài)庫為什么沒有 header,暫時不深究???
5. ncmds && sizeofcmds
表示 header 之后的 Load Command 的段數和大??;
實例:
看看 CoreAutoLayout 動態(tài)庫的 Mach-O 文件:

ps:后文會有 ncmds 在 fishhook 中的使用;
三、Load Command
1. Load Command 作用概述
其作用有:
- Mach-O 文件的布局;
這一點和 Mach-O 本身的設計有關,Load Command 本身不包含數據,Load Command 中的 segment 和section 類似于一個指針的作用,其描述(指向)的 segment 或者 section 實體才是真正存儲數據或代碼的地方。
- 鏈接信息;
這一點主要是通過幾個段(LC_SYMTAB、LC_LOAD_DYNLINER __Linkedit 等) 來描述符號表相關的信息,鏈接器位置等。dyld 通過這些信息進行符號表的 rebase 和 bind 等操作;
- Mach-O 文件在虛擬內存中的初始化布局;
這一點應該跟 __PAGEZERO 有關,具體??待補充
- 符號表的位置;
是鏈接信息的一部分,主要由 LC_SYMTAB、LC_DYSYMTAB、__LINKEDIT 來描述符號表、動態(tài)符號表、字符串表的位置;
- 程序 main 線程的初始執(zhí)行狀態(tài);
這里指的應該是 LC_MAIN 段描述的程序的入口函數位置;
- 主工程所導入的共享庫信息;
這一點就不多說了,在 machOView 中可以直觀的看到,也可以通過 otool 指令來獲?。?/p>
2. Load Command 的理解
以上是官方文檔對 Load Command 的表述,這里加上自己的理解。
Load Commands 由多個 command 組成,其大小由 command 的數量和 command 的 size 決定。Load Commands 更多的是一個統(tǒng)稱的概念;
如果 Load Command 按照是否指向數據實體來分類,分為兩種:
- 指向具體數據段
該種 command 存儲了一些信息,且指向 Data 部分的具體數據。
如 LC_SEGMENT(segment_command) 指向存放函數代碼的 __TEXT 段,程序員打交道最多的 __DATA / __DATA_CONST 段;
再比如 LC_CODE_SIGNATURE 指向 Data 中的簽名數據:

再比如動態(tài)鏈接相關的 __LINKEDIT 對應的 command 指向 Data 區(qū)域的 __LINKEDIT 段;
- 不指向具體數據段
該種 command 一般不包含數據實體,只起到描述性作用。
不像 LC_SEGMENT 一般會指向一個 SEGMENT,比如 __TEXT。而 LC_MAIN、LC_RPATH 等這些 command 都只是告訴 dyld 一些信息,不指向具體的數據段。常見的 command 如下在會問會有列舉;
3. Load Command 源碼解讀
接著,再說說 load_command 在代碼層面上的表現。
代碼層面上,command 的基本結構體為:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
這個結構體相當于一個基類。類似的結構體還有很多,比如:dysymtab_command、segment_command 等等,都包含 cmd 和 cmdsize;
因為 load_command 包含的信息太少,編碼時不好用,所以在代碼層面上被使用更多的是 LC_SEGMENT 對應的結構體和其他類型的結構體,如:
LC_SEGMENT:

非 LC_SEGMENT 的 command 結構體如下:
LC_SYMTAB:

其他的還有 dysymtab_command 、dylinker_command 等等,可以自行在源碼中查看。
舉個例子??:
這里以 fishhook 源碼來舉個實例,看如下代碼:
// 定位到 LC 其實位置
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
// 遍歷LC中的所有command,找出__LINKEDIT、LC_SYMTAB、LC_DYSYMTAB
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
// 這里先直接強轉成 segment_command ,因為比load_command 更好用
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
// __LINKEDIT是segment_command類型不需要再強轉
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
// LC_SYMTAB是symtab_command類型需要強轉
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
// LC_DYSYMTAB是dysymtab_command類型需要強轉
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
上述代碼是 fishhook 中尋找 __LINKEDIT 段、LC_SYMTAB、LC_DYSYMTAB 的代碼,其中 __LINKEDIT 對應的類型就是 segment_command,LC_SYMTAB 對應的是 symtab_command 類型,LC_DYSYMTAB 對應的是 dysymtab_command 類型;
總結:
- Load Command 由多個 command 組成;
- command 主要有兩種類型:指向具體數據、不指向具體數據;
- 代碼層面上 load_command 結構體相當于基類,很少被使用;
4. Load Command 和 segment/section 的關系
上文中講到 Load Command 主要分為指向數據實體和不指向數據實體兩種類型。
不指向數據實體的 command 主要作用是為 dyld 提供信息,而指向數據實體的 command 才是 command 和 segment/section 關系的體現;
如 LC_SEGMENT 指向具體的 segment,這個 segment 的實體部分就是 Mach-O 文件的第三部分,主要內容是代碼和數據;
延伸官方的圖片,繪制如下:

如上圖, LC_SEGMENT 類型的 command 指向具體的 section data。常見的 segment_command 一般也就幾個:__TEXT、__DATA、__DATA_CONST、__LINKEDIT、__PAGEZERO;
_TEXT、__DATA、__DATA_CONST 這三個不用贅述了,指向代碼、數據、常量區(qū)等;
這里其實可以很簡單的理解成大數據都放在 Data 中并在 command 中添加相關的信息,使用時可以很方便的找到。小數據則直接存放在 command 中(再大你也放不下?。_@里的設計思想和索引/目錄的思想很類似,Load Command 就相當于目錄;
總結:
- __LINKEDIT 指向存放 link 操作必要的數據段,是鏈接操作奠基石般的存在;
- 非數據類型的 command 用于未 dyld 提供簡短的信息;
- 數據類型的 command 在提供信息的同時,指向了 Data 段具體的數據/代碼;
- 具體的數據使用 segment 和 section 進行分段和分組;
5. __LINKEDIT是否屬于段
__LINKEDIT 也屬于 segment, command 指向 __LINKEDIT 這個段。只是在 machOView 軟件上沒有體現:

而使用 image lookup memory 是可以看到的:

另外,代碼層面上也有體現:

從上圖可以看到,很多 command (LC_CODE_SIGNATURE等)都是用了 linkedit_data_command 這個結構體。而其中的 dataoff 則描述了對應數據在 __LINKEDIT 中的位置;
而 __LINKEDIT 這個 command 使用了 LC_SEGMENT ,對應著 segment_command 這個結構體。所以,這個 linkedit_data_command 更像是一個補充的作用。
四、segment 和 section
1. segment
segment 命令在 64 位下的結構體:
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 */
};
解讀:

其實關于 segment 和 section 在上文中基本講的差不多了,一言以蔽之:
- Data 段的代碼是一團一團(blob)的,而 segment 和 section 以段和組的維度指向 Data 區(qū)域的數據或代碼的實體,利于尋址和使用;
2. segment 和 section的關系
segment 相當于一個數組,section 相當于數組中的元素;
這里需要注意的是,segment_command 中的 nsects 。該字段起到了數組的作用,用于 section 的尋址。這個數組是采用(數量 + 大?。┑姆绞絹碇苯荧@取對應的地址,從而獲取到對應的 section 。
其實這種方式在 Mach-O 文件中很常見。比如 Header 后面跟的就是 Load Command, Load Command 地址 = Header 的初始地址 + Header.size ,這也是為什么 Header 結構體中包含 load commands 的個數,而 segment 結構體又包含 section 的個數的原因,fishhook 源碼中有體現:
- 計算load command的初始位置
// 計算load command的初始位置
// header 是一個地址,指向這個 mach-O object 的初始位置
// 頭部是一個Header(mach_header_t結構體),緊接著是Loac Command
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
- 遍歷 Load Command
// ncmds 表示load command 的個數
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
// .....
}
- 遍歷 segment 中的 section
// nsects為number of sections
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
// ......
}
3. section
section 以“組”的維度指向 Data 部分中的數據。在 64 位架構中的結構體:
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 */
};
上文說了 segment 相當于一個數組,section 相當于數組中的元素。但是有一點需要注意:segment 本身是會存儲一些信息的,其實這一點在 Mach-O 文件中也可以看到:
- segment 初始地址 ≠ 第一個 section header 的地址;


即:
- segment 中不僅像數組一樣描述了該 segment 包含的 section,還存儲了 segment 段的一些信息;
4. 數據在 Data 的表現形式
再次強調一下,在 Data 中只有數據/代碼,并沒有描述信息,只有數據/代碼。
如下可以看到 Data 部分中的某個 section 初始地址 = 第一個 section 的地址:


即:
- Data 部分的數據/代碼是一團一團的純數據按順序排列,沒有描述信息,segment 和 section 是一個統(tǒng)籌的概念;
來張圖吧:

總結:
- 數據和代碼都是一坨一坨的存儲在 Data 中;
- segment 和 section 按照兩個維度劃分了 Data 部分,并描述了相關的信息;
5. 為什么要有 segment 和 section
從上文看,Data 中的數據都是一團一團的二進制,Mach-O 為此區(qū)分出了 section 和 segment。section 好理解,相似類型或者相同作用的數據作為一組數據嘛~~
比如懶加載符號都在 __la_symbol_ptr 這個 section 中,非懶加載符號都在 __got 這個 section 中,代碼都在 __text 這個 section 中,樁函數都在 __stub 中,樁函數的包裝函數都在 __stub_helper 中,這樣不就得了?為什么還要個 segment???
先說結論:
- 功能細化,segment 負責內存對齊以及保持 section 相對位置不變。section 則只管數據/代碼的存儲;
怎么解釋呢?這里其實分為兩點:
- segment 和內存對齊;
- 位置相對不變;
首先說內存對齊,官方文檔描述如下:

即:segment 中的數據都會被印射到虛擬內存中,所以 segment 是按頁對齊的。

即:segment 中的數據在虛擬內存中占得大小要比在磁盤中所占大小更大。
比如 __PAGEZERO 段,因為沒有數據,所以在磁盤中不占內存,但是在虛擬內存中占一個頁的內存。
這里需要解釋一下,__PAGEZERO 在 Load Command 中還是會占據少許磁盤空間的,即一個 command 結構體的大小。但是其描述的 segment 位于 Data 段,因為沒有具體數據,所以在磁盤中不占空間,即為 0;當 __PAGEZERO 動態(tài)鏈接器加載時,因為是 segment,所以要按頁對齊,最少分配一個 Page,所以雖然沒有數據,但是仍然占據了一個 Page;
至此我們知道 segment 在內存中是需要按照一定規(guī)則對齊的,以此實現 I/O 或者 CPU 指令的優(yōu)化;
再說說 section 的位置相對不變。
假設只有 section,那么內存對齊之后,section 如果未占滿一頁,那么該 section 后面的數據會留白,而在對齊之前,下一個 section 是緊跟著上一個 section 的。對齊之后,后面的 section 的位置就會發(fā)生變化。
這就是為什么 segment command 既有 vmaddr 又有 fileoff ,而 section 只有 fileoff(如symoff、stroff);
也就是說,section 只記錄相對于磁盤中文件初始位置的偏移,而 segment 已經根據對齊原則,算好了在虛擬內存中位置。如果是 segment 對齊后補 0,因為是補在最末尾,所以對當前 segment 中所有的 section 完全沒有影響,影響的只是下一個 segment 的位置,如下圖:

即:使用 section 來記錄 vmaddr 理論上也是可以實現,但是相對復雜,而且功能劃分不夠明確,設計感更糟糕;
dyld 和 fishhook 中計算動態(tài)鏈接相關表的位置的公式就是基于 segment 的 vmaddr 和 fileoff 來計算基地址,最后加上 section 中的 fileoff,詳見(fishhool原理分析)[http://m.itdecent.cn/p/c856f5cbbadb]
五、常見的 command

六、常見的 segment
常見的 segment 如下:
- __PAGEZERO;
- __TEXT;
- __DATA;
- __DATA_CONST;
- __LINKEDIT;
其實還有 __OBJC 、__IMPORT 等,具體定義在 loader.h 中,定義了常見的 segment 和 section:

注釋中也說明了,這些 segment name 和 section name 對于鏈接器而言沒有什么意義。但是為了支持傳統(tǒng)的 UNIX 可執(zhí)行文件,需要鏈接器和匯編器使用約定的名稱;

所以,不需要糾結有哪些 segment,只需要關注幾點:
- command 分為指向具體的數據和不指向具體數據兩種類型;
- section 指向 data 中一團一團的數據,segment 整合 section,在虛擬內存的加載時,屏蔽掉分頁對 section 位置的影響;