Mach-O 文件網上介紹的比較多,可是大多數都只是介紹了文件內的結構,並無說明爲何會以這樣的結構排布。經過閱讀《程序員的自我修養》一書,結合 MachOView
工具,從新梳理一下 Mach-O 文件。程序員
除了 iOS 系統的 Mach-O,與之對應的還有 Windows 下的 PE
和 Linux 下的 ELF
。它們都是基於一種叫作 COFF
文件的變種,它的主要貢獻是引入了「段」的機制,咱們編寫的應用程序正是被以這種段的形式存儲在 Mach-O 中。Mach-O 中除了包含機器代碼指令和數據,還包括符號表、調試信息、字符串表等等,它們都被以段的形式存儲。蘋果官方描述 Mach-O 的結構以下:緩存
下面會經過這個圖逐步展開分析 Mach-O 的內部細節,Mach-O 總體分爲三部分:markdown
那麼 Mach-O 爲何要以這種「段」的形式存儲呢?其實這種分段的好處有不少:架構
下面以螞蟻財富
的可執行文件爲例,經過 MachOView 來看下內部具體細節: ide
因爲 Data 中段比較多這裏只截取了部分。工具
Mach-O 文件頭結構及相關常數被定義在 「/usr/include/mach-o/loader.h」
文件中,由於 5s 以後的版本 cpu 架構都是 64 位,這裏以 64 位版本爲例來看一下它的結構體定義:post
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 */
};
複製代碼
單純的看這個結構體不太好理解,下面再經過 MachOView 看一下螞蟻財富 App 的 Header 構成: ui
Header 部分主要是對 Mach-O 文件的描述,從上圖中能夠看出,Header 的最後一個字段相對於文件開始的偏移量爲 0x1c
,reserved 字段雖然沒有值,但它仍然佔用了 4 個字節,這樣整個 Header 一共佔用了 32
個字節。Mach-O 文件被系統裝載的時候,會先讀出 Header 部分,經過 Header 就能夠找到 Load Commands 加載指令部分,讀取到加載指令就能夠加載到咱們編寫的代碼了。在 Header 中有個重要的字段 sizeofcmds 用來表示 Load Commands 的大小,經過它就能夠找到 Load Commands 的位置。this
Load Command 是 Mach-O 文件中除了文件頭之外最重要的結構,它描述了 Data 中的各個段信息,好比每一個段的段名、段的長度、在文件中的偏移、讀寫權限以及段的其餘屬性,它的位置要由 Header 的大小決定,下面是 Load Command 的起始位置和結束位置:spa
Load Command 的起始偏移和結束偏移分別爲 0x20
和 0x2380
。0x20 換算成 10 進制是 32
,正好是 Header 的大小,0x2380
和 0x20
差值正好是 Header 中 Size of Load Commands 的值 0x2368
,由此也驗證了 Load Command 的位置是緊隨 Header 後面的。
在 MachOView 中 Load Command 的主要結構以下:
Load Command 由多個 Segment 構成,一個 Segment 包含一個或多個屬性相似的 Section。關於 Segment 結構的定義也能夠在 「/usr/include/mach-o/loader.h」
中找到:
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 */
};
複製代碼
vmaddr
和 vmsize
是在應用程序被加載進虛擬內存用到的,在將 Mach-O 加載到虛擬內存的時候,會在虛擬內存上的 vmaddr 位置開始,取出 vmsize 大小的空間來存放這個段。然而在 Section 內就不存在這兩個字段,由於這兩個字段是給裝載用的,這也是 Segment 和 Section 最主要的區別。
Data 中存放的全部的 Section,例如機器指令,全局變量和局部靜態變量,符號表,調試信息等都會被存儲到對應的 Section 中:
Mach-O 被裝載的時候會經過 Segment 尋找對應的 Section,在 Load Commands 中 經過 Segment 能夠直接找到每一個 Section 的位置和大小,例如上圖 _text
段在文件中的偏移爲 0x40E0
,這與它在 Segment 中的 offset
是一致的,以下圖紅框內所示:
在 loader.h
中也能找到 Section 的結構體的定義:
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 */
};
複製代碼
關於 sectname
特別說明一下,它表示了這個 Section 存放了哪些信息,下面列舉一些:
上面關於 Section 和 Segment 的主要區別沒有細說,既然有了 Section,爲何還要有 Segment ?
這塊涉及到內存分頁加載的概念,以前很火的抖音的二進制重排也是利用這種分頁加載的機制,Mach-O 被虛擬內存加載的時候是以頁爲單位,在 iOS 上,一頁的大小被劃定爲 16kb
,每一個 Section 在映射時都是系統頁長度的整數倍,不足一個頁的部分也會佔用一個頁,這樣在 Section 增多後會帶來大量內存碎片。Segment 是在裝載的角度從新劃分了 Mach-O 的各個段,對於相同權限的 Section,把它們合併到一塊兒做爲一個 Segment 進行映射。從目標文件連接的角度看,Mach-O 文件是按照 Section 存儲的,但從裝載的角度看,它是按照 Segment 劃分的,到這裏就應該很容易的理解上面蘋果官方給出的 Mach-O 結構圖了。
《程序員的自我修養》