Mach-O文件

通用二進制(Universal binary)文件

macOS系統一路走來,支持的CPU及硬件平臺都有了很大的變化,從早期的PowerPC平臺,到後來的x86,再到如今主流的arm、x86-64平臺。軟件開發人員爲了作到不一樣硬件平臺的兼容性,若是須要爲每個平臺編譯一個可執行文件,這將是很是繁瑣的。爲了解決軟件在多個硬件平臺上的兼容性問題,蘋果開發了一個通用的二進制文件格式(Universal Binary)。 又稱爲胖二進制(Fat Binary),通用二進制文件中將多個支持不一樣CPU架構的二進制文件打包成一個文件,系統在加載運行該程序時,會根據通用二進制文件中提供的多個架構來與當前系統平臺作匹配,運行適合當前系統的那個版本。有人或許會好奇,不是講Mach-O文件嗎?怎麼開始講通用二進制文件,不要着急,看下面file命令查看dyld的打印,universal binary前面不就是Mach-O嗎html

蘋果自家系統中存在着不少通用二進制文件。好比/usr/lib/dyld,在終端中執行file命令能夠查看它的信息:git

$ file /usr/lib/dyld
/usr/lib/dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
/usr/lib/dyld (for architecture x86_64):	Mach-O 64-bit dynamic linker x86_64
/usr/lib/dyld (for architecture i386):	Mach-O dynamic linker i386
複製代碼

咱們在Xcode中能夠經過設置Build Settings中的Architectures來生成兼容各類架構的APP image.png 編譯以後,使用file命令查看生成的ipa包裏的可執行文件 image.pnggithub

系統提供了一個命令行工具lipo來操做通用二進制文件。它能夠添加、提取、刪除以及替換通用二進制文件中特定架構的二進制版本。數組

查看通用二進制文件信息:lipo -info testruby

提取test中armv7版本的二進制文件能夠執行:lipo test -extract armv7 -output test_armv7markdown

提取test中arm64版本的二進制文件能夠執行:lipo test -extract arm64 -output test_arm64架構

合併test_armv7和test_arm64:lipo -create test_armv7 test_arm64 -output test0app

刪除test中armv7s版本的二進制文件能夠執行:lipo test -remove armv7s -output test1socket

通用二進制的"通用"不止針對能夠直接運行的能夠執行文件,系統中的動態庫.dylib文件,靜態庫.a文件以及Framework等均可以是通用二進制文件,對它們一樣也可使用lipo命令來進行管理;ide

接下來打開咱們的Xcode,按command + shift + o輸入mach-o/fat.h就能夠看到對通用二進制文件格式的聲明,從文件的命名和聲明來看,將通用二進制叫做胖二進制或許更合適;胖二進制的頭部定義以下: Snip20210627_130.png magic字段被定義爲常量FAT_MAGIC,它的取值是固定的0xcafebabe,表示這是一個通用二進制文件;這裏要說一下字節序,計算機硬件有兩種儲存數據的方式,分別爲大端字節序,和小端字節序,大端字節序:高位字節在前,低位字節在後,這是人類讀寫數值的方法。小端字節序:低位字節在前,高位字節在後,是大多數機器讀取數據的方式,以下圖所示
bg2016112201.gif
nfat_arch字段表示後面的Mach-O文件的數量
每一個通用二進制架構信息都使用fat_arch結構體表示,在fat_header結構體以後,緊接着的是一個或多個連續的fat_arch結構體,它的定義以下: image.png cputype字段是cpu說明符,類型是cpu_type_t,定義在<mach/machine.h>文件,使用一樣的command + shift + o而後輸入頭文件的方法能夠打開<mach/machine.h>文件,在macOS上取值通常爲CPU_TYPE_I386CPU_TYPE_X86_64,在iOS平臺上通常是CPU_TYPE_ARMCPU_TYPE_ARM64
cpu_subtype字段是機器說明符,類型是cpu_subtype_t,一樣定義在<mach/machine.h>文件,macOS上通常是CPU_SUBTYPE_I386_ALL,CPU_SUBTYPE_X86_64_ALL,在iOS上通常則是CPU_SUBTYPE_ARM64_ALL,CPU_SUBTYPE_ARM_V7
offset字段指明瞭當前Mach-O數據相對於當前文件開頭的偏移值
size字段指明瞭數據的大小
align字段指明瞭數據的內存對齊邊界,取值必須是2的n次方,它確保了當前cpu架構的目標文件加載到內存中時,數據是通過內存優化對齊的

使用MachOView能夠十分清楚的看到這些信息 image.png 在fat_arch結構體往下就是具體的Mach-O格式文件了,它的內容複雜得多,將在下一小節進行討論。

Mach-O文件

簡介

到底什麼是Mach-O文件我翻閱了網上無數的文章,幾乎沒有人給出明確的答案,我這裏給出我本身的理解,只要是符合某種特定結構或者說格式的二進制文件均可以稱之爲Mach-O文件,也能夠說Mach-O文件是一種有着特定結構的二進制文件,這個特定的結構咱們後面會講到,熟悉Mach-O文件格式,有助於瞭解蘋果軟件底層運行機制,更好的掌握dyld加載Mach-O的步驟,爲本身動手開發Mach-O相關的加解密工具打下基礎

  1. MacOS上的可執行文件是一種Mach-O文件(好比ruby,phtyon...),但不是全部可執行文件都是Mach-O文件
  2. 庫文件是一種Mach-O文件,動態庫.dylib,靜態庫.a,還有Framework都是一種Mach-O文件
  3. .o文件(clang編譯c源文件獲得的)是一種Mach-O文件
  4. .dsym文件(符號表)也是一種Mach-O文件
  5. dyld也是一種Mach-O文件

以上這些都屬於Mach-O文件,固然除了以上這五種,還有其餘類型的Mach-O文件,只是這五種比較常見...其餘還有八種,其餘八種會在下面對Mach-O文件結構的介紹中提到

從上面MachOView的截圖中能夠看到,test文件內有4種不一樣架構的文件,每種架構的文件均可以稱它爲一個Mach-O文件,而剛剛所講的通用二進制文件就是一個文件若是包含了1種以上的Mach-O文件,那麼他就是通用二進制文件

咱們知道了Mach-O文件就是一堆有着特定結構的二進制數據,那麼咱們如何從這一堆的二進制裏獲取咱們所須要的數據?若是作過股票行情APP,IM通信底層SDK或者說使用過socket長鏈接對二進制數據進行過處理,發送,接收的同窗,必定會知道對一堆的二進制如何有效的處理,提取咱們想要的數據的;以我曾經作過的一款股票行情軟件爲例,裏面就定義了大量的結構體類型,用結構體來對二進制數據進行解析,獲得咱們想要的數據,那麼這個Mach-O文件的解析有沒有對應的結構體呢?固然有,在Xcode中使用command + shift + o搜索mach-o/loader.h就會發現一堆的結構體,這些結構體都是系統用來解析Mach-O文件的,咱們也能從中獲取到很多的信息

結構

一個典型的Mach-O文件結構以下圖所示: v2-35f7008ce676b29129f9ec8bed3c464f_r.png 從圖中能夠了解到一個Mach-O文件的結構包括Header,Load commands和Data

  • Header: 描述了Mach-O的cpu架構、文件類型以及加載命令等信息。
  • Load commands: 描述了文件中數據的具體組織結構,不一樣的數據類型使用不一樣的加載命令表示
  • Data: 每一個段(segment)都有一個或多個Section,它們存放了具體的數據與代碼。

Header

可使用otool命令來查看Mach-O文件的頭部信息 image.png 這個部分的定義,能夠經過在Xcode中,按command + shift + o輸入mach-o/loader.h的方式找到 image.png

  • magic在截圖中都能看到的宏定義,對32位架構的程序來講,它的值就是0xfeedface,可使用MH_MAGIC宏代替;對64位架構的程序來講,它的值就是0xfeedfacf,對應的宏MH_MAGIC_64
  • cputype和上一節中所講的fat_header結構體的含義徹底相同
  • cpusubtype同上
  • filetype表示Mach-O文件的具體類型,值有下圖所示的12種,常見的有MH_EXECUTE(可執行文件),MH_DYLIB(動態庫),MH_DYLINKER(動態鏈接器),MH_DSYM(符號表文件)

image.png

  • ncmdsload commands的數量
  • sizeofcmds全部load commands的佔的字節數
  • flags標記,值比較多,最好去頭文件中查看詳細說明
#define	MH_NOUNDEFS	0x1		/* the object file has no undefined
					   references */
#define	MH_INCRLINK	0x2		/* the object file is the output of an
					   incremental link against a base file
					   and can't be link edited again */
#define MH_DYLDLINK	0x4		/* the object file is input for the
					   dynamic linker and can't be staticly
					   link edited again */
#define MH_BINDATLOAD	0x8		/* the object file's undefined
					   references are bound by the dynamic
					   linker when loaded. */
#define MH_PREBOUND	0x10		/* the file has its dynamic undefined
					   references prebound. */
......
複製代碼
  • reserved這個字段只在64位架構的Mach-O文件中才有,目前它的取值系統保留

使用MachOView查看Header的信息 image.png

Load Commands

Load Commands描述的是文件的加載信息,加載信息有不少,加載的段、符號表、動態庫信息等都在Commands中取到。這個部分信息仍是比較有用的,咱們能夠從這裏獲取到符號表和字符串表的偏移量,下文中會有詳細的解釋。

Load Commands加載命令緊跟在Header以後,全部加載命令的前兩個字段必須是cmd和cmdsize,cmd字段用該命令類型的常量填充,頭文件中定義了許多的宏用於該字段,每一個命令類型都有一個特定的結構;cmdsize字段是以字節爲單位的特定加載命令結構的大小,再加上它後面做爲加載命令一部分的任何內容(即節結構、字符串等)要前進到下一個加載命令,能夠將cmdsize加上當前加載命令的偏移量 image.png cmd字段的取值有目前有50多種,太多了就不所有粘貼出來了...

#define LC_REQ_DYLD 0x80000000

/* Constants for the cmd field of all load commands, the type */
#define	LC_SEGMENT	0x1	/* segment of this file to be mapped */
#define	LC_SYMTAB	0x2	/* link-edit stab symbol table info */
#define	LC_SYMSEG	0x3	/* link-edit gdb symbol table info (obsolete) */
#define	LC_THREAD	0x4	/* thread */
#define	LC_UNIXTHREAD	0x5	/* unix thread (includes a stack) */
#define	LC_LOADFVMLIB	0x6	/* load a specified fixed VM shared library */
#define	LC_IDFVMLIB	0x7	/* fixed VM shared library identification */
#define	LC_IDENT	0x8	/* object identification info (obsolete) */
......
複製代碼

全部的這些加載命令由系統內核加載器直接使用,或由動態連接器處理。其中幾個常見的加載命令有LC_LOAD_DYLIBLC_SEGMENTLC_MAINLC_CODE_SIGNATURELC_ENCRYPTION_INFO等,下面介紹其中的幾個

LC_LOAD_DYLIB

LC_LOAD_DYLIB:表示這是一個須要動態加載的連接庫。它使用dylib_command結構體表示。定義以下:

struct dylib_command {
	uint32_t	cmd;		/* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
					   LC_REEXPORT_DYLIB */
	uint32_t	cmdsize;	/* includes pathname string */
	struct dylib	dylib;		/* the library identification */
};
複製代碼

當cmd類型是LC_ID_DYLIB,LC_LOAD_DYLIB,LC_LOAD_WEAK_DYLIB,LC_REEXPORT_DYLIB時,都使用dylib_command結構體表示;其中dylib結構體存儲要加載的動態庫的具體信息以下

struct dylib {
    union lc_str  name;			/* library's path name */
    uint32_t timestamp;			/* library's build time stamp */
    uint32_t current_version;		/* library's current version number */
    uint32_t compatibility_version;	/* library's compatibility vers number*/
};
複製代碼

name字段是連接庫的完整路徑,動態連接器在加載庫時,通用此路徑來進行加載它。
timestamp字段描述了庫構建時的時間戳。
current_versioncompatibility_version指明瞭前當版本與兼容的版本號
若是你看了個人上一篇文章代碼注入裏面提到了yololib,這個工具的原理基本就是利用這條LC_LOAD_DYLIB加載命令的相關信息實現的

LC_MAIN

LC_MAIN: 此加載命令記錄了可執行文件的主函數main()的位置。它使用entry_point_command結構體表示。定義以下:

struct entry_point_command {
    uint32_t  cmd;	/* LC_MAIN only used in MH_EXECUTE filetypes */
    uint32_t  cmdsize;	/* 24 */
    uint64_t  entryoff;	/* file (__TEXT) offset of main() */
    uint64_t  stacksize;/* if not zero, initial stack size */
};
複製代碼

entryoff字段中就指定了main()函數的文件偏移。stacksize指定了初始的堆棧大小。

LC_SEGMENT/LC_SEGMENT_64

LC_SEGMENT/LC_SEGMENT_64:段加載命令,描述了32位或64位Mach-O文件的段的信息,,常見的段有__PAGEZERO,__TEXT,__DATA,__LINKEDIT,__PAGEZERO是一個空段,它位於文件起始段的位置,__TEXT__DATA分別是文本段和數據段,分別存儲了代碼信息和數據信息,__LINKEDIT是連接信息段;段(segment)又能夠細分爲section,每一個段(segment)能夠包含多個section

段使用segment_command結構體來表示,它的定義以下:

struct segment_command { /* for 32-bit architectures */
	uint32_t	cmd;		/* LC_SEGMENT */
	uint32_t	cmdsize;	/* includes sizeof section structs */
	char		segname[16];	/* segment name */
	uint32_t	vmaddr;		/* memory address of this segment */
	uint32_t	vmsize;		/* memory size of this segment */
	uint32_t	fileoff;	/* file offset of this segment */
	uint32_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 */
};
複製代碼

segname字段是一個16字節大小的空間,用來存儲段的名稱,好比__TEXT...
vmaddr字段指明瞭段要加載的虛擬內存地址
vmsize字段指明瞭段所佔的虛擬內存的大小
fileoff字段指明瞭段數據所在文件中偏移地址
filesize字段指明瞭段數據實際的大小
maxprot字段指明瞭頁面所須要的最高內存保護
initprot字段指明瞭頁面初始的內存保護
nsects字段指明瞭段所包含的節區(section)
flags字段指明瞭段的標誌信息
還有不少Load Commands加載命令,這裏就不一一介紹了...貼一個圖大概瞭解下 image.png

使用MachOView查看Load Commands的內容 image.png

Data

數據區,除了Header和Load Commands外全部的原始數據。Load Commands是對數據的彙總提示,而數據區則是真實的數據。Load Commands與數據區的關係就像書的目錄與章節的關係,如圖所示,Segment爲__TEXT的段裏,顯示有8個section,每一個section具體的內容就在Data區裏了 image.png 接下里介紹幾個比較重要的section

(__TEXT,__text)

這裏存放的是彙編後的代碼,當咱們進行編譯時,每一個.m文件會通過預編譯->編譯->彙編造成.o文件,稱之爲目標文件。彙編後,全部的代碼會造成彙編指令存儲在.o文件的(__TEXT,__text)區((__DATA,__data)也是相似)。連接後,全部的.o文件會合併成一個文件,全部.o文件的(__TEXT,__text)數據都會按連接順序存放到應用文件的(__TEXT,__text)中。 image.png

(__TEXT,__objc_methname)

這裏存放了項目裏,全部咱們用Objective-C寫的方法名 image.png

(__TEXT,__objc_classname)

這裏存放了項目裏全部Objective-C類的名字 image.png class-dump工具可以解析出每一個類的方法,屬性,成員變量,應該就是來自上面兩個section的數據了,固然這只是個人猜想,具體怎麼實現的就要去看class-dump的源碼了

Symbol Table

符號表,這個是重點中的重點,符號表是將地址和符號聯繫起來的橋樑。符號表並不能直接存儲符號,而是存儲符號位於字符串表的位置。 image.png

String Table

字符串表全部的變量名、函數名等,都以字符串的形式存儲在字符串表中。 image.png

Dynamic Symbol Table

動態符號表存儲的是動態庫函數位於符號表的偏移信息。(__DATA,__la_symbol_ptr) section 能夠從動態符號表中獲取到該section位於符號表的索引數組。動態符號表並不存儲符號信息,而是存儲其位於符號表的偏移信息。Fishhook源碼看起來比較複雜主要是由於hook的是動態連接的函數,索引和連接關係比較繞。可是咱們本身編寫的C函數不是動態連接的,而是在編譯連接後代碼指令就存儲在文件內部的函數,所以不會用到動態符號表。 image.png

固然,關於Mach-O文件的知識遠不止這麼點,可是要徹底講清楚裏面的全部內容,那估計不是這麼一篇文章可以講的清楚的,至少也得是一本書了,我也只是網上收集到的一些資料,本身寫了篇總結而已
另外這篇文章借鑑和參考瞭如下這兩篇文章:

相關文章
相關標籤/搜索