iOS程序員的自我修養-MachO文件結構分析(二)

目錄

可執行文件

進程是特殊文件在內存中加載獲得的結果。那這種文件的格式必須是系統內核能夠理解的,系統內核才能正確解析。python

不一樣操做系統的可執行文件格式不一樣:git

可執行格式 魔數 用途
PE32/PE32+ MZ Windows的可執行文件
ELF \x7FELF Linux和大部分UNIX的可執行文件和庫文件
腳本 #! 主要用於shell腳本,也有一些解釋器腳本使用這個格式。這是一種特殊的二進制文件格式,#! 後面指向真正的可執行文件(好比python),而腳本其它內容,都被當作輸入傳遞給這個命令。
通用二進制格式(胖二進制格式) 0xcafebabe(小端) 包含多種架構支持的Mach-O格式,iOS和OS X支持的格式
Mach-O 0xfeedface(32位) 0xfeedfacf(64位) iOS和OS x支持的格式

系統內核將文件讀入內存,而後尋找文件的頭簽名(魔數magic),根據magic就能夠判斷二進制文件的格式。程序員

其實PE/ELF/Mach-O這三種可執行文件格式都是COFF(Common file format)格式的變種。COFF的主要貢獻是目標文件裏面引入了「段」的機制,不一樣的目標文件能夠擁有不一樣數量和不一樣類型的「段」。github

接下來我將介紹通用二進制文件和Mach-O文件:shell

通用二進制文件

爲何有通用二進制文件

爲何有了Mach-O格式了,蘋果還搞通用二進制格式?由於不一樣CPU平臺支持的指令不一樣,好比arm64和x86,那咱們是否是能夠把arm64和x86對應的Mach-O格式打包在一塊兒,而後系統根據本身的CPU平臺,選擇合適的Mach-O。通用二進制格式就是多種架構的Mach-O文件「打包」在一塊兒,因此通用二進制格式,更多被叫作胖二進制格式。緩存

通用二進制文件格式

通用二進制格式定義在<mach-o/fat.h>中。bash

#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
	uint32_t	magic;		/* FAT_MAGIC or FAT_MAGIC_64 */
	uint32_t	nfat_arch;	/* number of structs that follow */
};

struct fat_arch {
	cpu_type_t	cputype;	/* cpu specifier (int) */
	cpu_subtype_t	cpusubtype;	/* machine specifier (int) */
	uint32_t	offset;		/* file offset to this object file */
	uint32_t	size;		/* size of this object file */
	uint32_t	align;		/* alignment as a power of 2 */
};
複製代碼

通用二進制文件開始是fat_header結構體,magic可讓系統內核讀取該文件時候知道是通用二進制文件;nfat_arch代表下面有多少個fat_arch結構體(也能夠說這個通用二進制文件包含多少個Mach-O)。架構

fat_arch結構體是描述Mach-O。cputype和cpusubtype說明Mach-O適用什麼平臺;offset(偏移)、size(大小)和align(頁對齊)能夠清楚描述Mach-O二進制位於通用二進制文件哪裏。ide

操做通用二進制文件的經常使用命令

file 命令查看
$ file bq   
bq: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64]
bq (for architecture armv7):	Mach-O executable arm_v7
bq (for architecture arm64):	Mach-O 64-bit executable arm64

otool 命令查看fat_header信息
$ otool -f -V bq
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture armv7
    cputype CPU_TYPE_ARM
    cpusubtype CPU_SUBTYPE_ARM_V7
    capabilities 0x0
    offset 16384
    size 74952848
    align 2^14 (16384)
architecture arm64
    cputype CPU_TYPE_ARM64
    cpusubtype CPU_SUBTYPE_ARM64_ALL
    capabilities 0x0
    offset 74973184
    size 84135936
    align 2^14 (16384)
    
    
lipo(脂肪) 能夠增、刪、提取胖二進制文件中的特定架構(Mach-O)

提取特定Mach-O
lipo bq -extract armv7 -o bq_v7   

刪除特定Mach-O
lipo bq -remove armv7 -o bq_v7

瘦身爲Mach-O文件格式
lipo bq -thin armv7 -o bq_v7

複製代碼

通用二進制文件意義

從上面能夠知道,儘管通用二進制文件會佔用大量的磁盤空間,可是系統會挑選合適的Mach-O來執行,不相關的架構代碼不會佔用內存空間,且執行效率高了。函數

挑選合適的Mach-O的函數定義在<mach-o/arch.h>中,NXGetLocalArchInfo()函數得到主機的架構信息,NXFindBestFatArch()函數匹配最合適的Mach-O。

Mach-O文件

網上不少介紹Mach-O格式的文章,可是大篇幅都是介紹各類加載命令,讓剛接觸Mach-O的讀者一上來就懵逼了,覺得掌握Mach-O,就是記憶各類加載命令,讓學習Mach-O文件格式變得枯燥且困難。

讀者只需跟着我這系列文章,由淺入深,保你早日拿下Mach-O~~

Mach-O文件是什麼

Mach-O文件格式就是COFF(Common file format)格式的變種。而COFF引入了「段」的機制,不一樣的Mach-O文件能夠擁有不一樣數量和不一樣類型的「段」。Mach-O目標文件是源代碼編譯獲得的文件,那至少文件裏有機器指令、數據吧。其實除了這些以外,還有連接時候須要的一些信息,好比符號表、調試信息、字符串等。而後按照不一樣的信息,放在不一樣的「段」(segment)中。機器指令通常放在代碼段裏,全局變量和局部靜態變量通常放在數據段裏。

這裏簡單說下數據分段的好處,好比數據和機器指令分段:

  1. 數據和指令能夠被映射到兩個不一樣的虛擬內存區域。數據區域是可讀寫的,指令區域是隻讀可執行。那就能夠方便分別設置這兩個區域的操做權限。
  2. 兩個區域分離,有助於提升緩存的命中率。(提升了程序的局部性)
  3. 最主要是,系統運行多個該程序的副本時,它們指令是同樣的,那內存只須要保存一份指令部分,可讀寫的數據區域進程私有。是否是節約了內存,動態連接那篇也是講這樣的方式來節約內存。

Mach-O文件格式

從很早之前蘋果官網的這個老圖中,咱們知道了Mach-O文件由:Header、Load Commands、Data三部分組成。

文件最開始的Header是mach_header結構體,定義在<mach-o/loader.h>。

//後面默認都講64位操做系統的,老早就淘汰的古董機iPhone5s就是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 */
};
複製代碼
  1. magic:0xfeedface(32位) 0xfeedfacf(64位),系統內核用來判斷是不是mach-o格式
  2. cputype和cpusubtype: 做用同上面胖二進制文件裏的
  3. filetype:因爲可執行文件、目標文件、靜態庫和動態庫等都是mach-o格式,因此須要filetype來講明mach-o文件是屬於哪一種文件。
  4. ncms:加載命令的條數 (加載命令緊跟Header以後)
  5. sizeofcmds:加載命令的大小
  6. 動態鏈接器(dyld)的標誌,能夠先無論
  7. reserved:保留字段

其中filetype常取字段有:

#define MH_OBJECT 0x1 目標文件 
#define MH_EXECUTE 0x2 可執行文件 
#define MH_DYLIB 0x6 動態庫 
#define MH_DYLINKER 0x7 動態鏈接器 
#define MH_DSYM 0xa 存儲二進制文件符號信息,用於Debug分析
複製代碼

加載命令

進程是特殊文件在內存中加載獲得的結果。那這種文件的格式必須是系統內核能夠理解的,系統內核才能正確解析。 --本文最開始

上面介紹了Mach-O有不一樣類型的「段」,且系統內核(或連接器)須要不一樣的加載方式來加載對應的段,而加載命令就是指導系統內核如何加載,因此有了不一樣的加載命令。

爲了講清楚Mach-O格式,我僅講一個最普通且有表明意義的加載命令:段加載命令(LC_SEGMENT_64),其它加載命令,後面篇章用到時候,再具體講解。

LC_SEGMENT_64

// 定義在<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 */
};
複製代碼
  1. cmd表示加載命令類型,cmdsize表示加載命令大小(還包括了緊跟其後的nsects個section的大小);須要知道的是,雖然不一樣加載命令的結構體不一樣,可是全部結構體的前兩個字段都是cmd和cmdsize。這樣系統在迭代全部加載命令時候,能夠準確找到每一個加載命令。
  2. segname:加載命令名字
  3. 從fileoff(偏移)處,取filesize字節的二進制數據,放到內存的vmaddr處的vmsize字節。(fileoff處到filesize字節的二進制數據,就是「段」)
  4. 每個段的權限相同(或者說,編譯時候,編譯器把相同權限的數據放在一塊兒,成爲段),其權限根據initprot初始化,initprot指定了如何經過讀/寫/執行位初始化頁面的保護級別。段的保護設置能夠動態改變,可是不能超過maxprot中指定的值(在iOS中,+x和+w是互斥的)。
  5. nsects:段中section數量

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 */
};
複製代碼

加載命令若是有section,後面會緊跟nsects個section。section的header結構體是同樣的。

爲何要同時存在segment和section

真的要講清這個,須要理解虛擬內存。我這裏拋磚引玉,但願讀者在看Mach-O文件結構時候,也能想下爲何這麼設計。

其實從連接的角度來看,Mach-O文件是按照section來存儲文件的,segment只不過是把多個section打包放在一塊兒而已;可是從Mach-O文件裝載到內存的角度來看,Mach-O文件是按照segment(編譯時候,編譯器把相同權限的數據放在一塊兒,成爲segment)來存儲的,即便一個segment裏的內容小於1頁空間的內存,可是仍是會佔用一頁空間的內存,因此segment裏不只有filesize,也有vmsize,而section不須要有vmsize。

這樣作,是爲了節約內存,減小頁面內部碎片。

查看Mach-O文件格式

  1. 命令: otool -l mach-o文件
  2. MachOView

Mach-O格式圖

經過上面分析,最後給出Mach-O格式圖,若是你對這個格式圖有不理解地方,再回過頭看看上面對應地方的分析~

引用

  1. 《程序員的自我修養-連接、裝載與庫》
  2. 《深刻解析Mac OS X & iOS操做系統》
相關文章
相關標籤/搜索