一個iOS程序員的自我修養(二)Mach-O裏面有什麼

Mach-O

Mach-O 文件網上介紹的比較多,可是大多數都只是介紹了文件內的結構,並無說明爲何會以這樣的結構排布。經過閱讀《程序員的自我修養》一書,結合 MachOView 工具,從新梳理一下 Mach-O 文件。程序員

除了 iOS 系統的 Mach-O,與之對應的還有 Windows 下的 PE 和 Linux 下的 ELF。它們都是基於一種叫作 COFF 文件的變種,它的主要貢獻是引入了「段」的機制,咱們編寫的應用程序正是被以這種段的形式存儲在 Mach-O 中。Mach-O 中除了包含機器代碼指令和數據,還包括符號表、調試信息、字符串表等等,它們都被以段的形式存儲。蘋果官方描述 Mach-O 的結構以下:緩存

下面會經過這個圖逐步展開分析 Mach-O 的內部細節,Mach-O 總體分爲三部分:markdown

  • Header:最前面的部分是 Mach-O 文件頭,用來描述文件版本、目標機器型號、程序入口等信息。
  • Load commands: 多個 Segment 組成,每一個 Segment 又包含了多個相同類型的 Section。爲什麼叫加載命令,由於它是用來被系統加載使用的。
  • Data: 被 Load commands 描述的各個 Section,包括編寫的指令代碼,定義的常量變量等,還包括符號表,字符串表等等其餘咱們比較熟悉的段,也就是說咱們所寫的應用程序會被拆分紅一個個的 Section 存儲在 Mach-O 文件中。

那麼 Mach-O 爲何要以這種「段」的形式存儲呢?其實這種分段的好處有不少:架構

  1. 每一個段能夠根據它們的讀寫權限被映射到不一樣的內存區域,例如程序的指令是可讀的,因此會被映射到可讀區域,這樣能夠防止程序的指令被有意或無心的改寫。
  2. 對於現代的 cpu 來講,它們有着強大的緩存體系,按段的形式存放對緩存命中提升有好處。
  3. 當系統中運行着多個該程序的副本時,它們的指令都是同樣的,因此內存中的只讀數據只保存一份,能夠節省大量的內存。

Header

下面以螞蟻財富的可執行文件爲例,經過 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 */
};
複製代碼
  • magic:一個叫作 「魔數」 的字段,標識了 Mach-O 文件的格式。
  • cputype:cpu 類型。
  • cpusubtype:機器標識符。
  • filetype:文件類型。
  • ncmds:Load Commands 的數量。
  • sizeofcmds:Load Commands 大小。
  • flags:動態連接器標識。
  • 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

Load Command 是 Mach-O 文件中除了文件頭之外最重要的結構,它描述了 Data 中的各個段信息,好比每一個段的段名、段的長度、在文件中的偏移、讀寫權限以及段的其餘屬性,它的位置要由 Header 的大小決定,下面是 Load Command 的起始位置和結束位置:spa

Load Command 的起始偏移和結束偏移分別爲 0x200x2380。0x20 換算成 10 進制是 32,正好是 Header 的大小,0x23800x20 差值正好是 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 */
};
複製代碼
  • cmd:Segment 的類型,和下面的 flags 標記位決定着這個段如何被裝載。
  • segname:段名。
  • vmaddr:當前段在虛擬內存起始地址。
  • vmsize:當前段在虛擬內存地址佔用的長度。
  • fileoff:在文件中的偏移。
  • filesize:在文件中的長度。
  • nsects:包含section 的個數。
  • flags:標記位,表示在進程虛擬地址空間中的屬性,好比是否可寫、是否可執行等。

vmaddrvmsize 是在應用程序被加載進虛擬內存用到的,在將 Mach-O 加載到虛擬內存的時候,會在虛擬內存上的 vmaddr 位置開始,取出 vmsize 大小的空間來存放這個段。然而在 Section 內就不存在這兩個字段,由於這兩個字段是給裝載用的,這也是 Segment 和 Section 最主要的區別。

Data

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 存放了哪些信息,下面列舉一些:

  • __text:可執行的機器碼。
  • __cstring:一些C字符串。
  • __const:常量。
  • __data:存儲初始化的可變數據。
  • __bss:存儲未初始化的全局變量和局部靜態變量。
  • __objc_clasname:存儲 OC 類名。
  • __objc_classlist:方法列表。
  • __objc_protocollist:協議列表。

Section 和 Segument

上面關於 Section 和 Segment 的主要區別沒有細說,既然有了 Section,爲何還要有 Segment ?

這塊涉及到內存分頁加載的概念,以前很火的抖音的二進制重排也是利用這種分頁加載的機制,Mach-O 被虛擬內存加載的時候是以頁爲單位,在 iOS 上,一頁的大小被劃定爲 16kb,每一個 Section 在映射時都是系統頁長度的整數倍,不足一個頁的部分也會佔用一個頁,這樣在 Section 增多後會帶來大量內存碎片。Segment 是在裝載的角度從新劃分了 Mach-O 的各個段,對於相同權限的 Section,把它們合併到一塊兒做爲一個 Segment 進行映射。從目標文件連接的角度看,Mach-O 文件是按照 Section 存儲的,但從裝載的角度看,它是按照 Segment 劃分的,到這裏就應該很容易的理解上面蘋果官方給出的 Mach-O 結構圖了。

引用

《程序員的自我修養》

相關文章
相關標籤/搜索