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 編譯以後,使用file
命令查看生成的ipa包裏的可執行文件 github
系統提供了一個命令行工具lipo
來操做通用二進制文件。它能夠添加、提取、刪除以及替換通用二進制文件中特定架構的二進制版本。數組
查看通用二進制文件信息:lipo -info test
ruby
提取test中armv7版本的二進制文件能夠執行:lipo test -extract armv7 -output test_armv7
markdown
提取test中arm64版本的二進制文件能夠執行:lipo test -extract arm64 -output test_arm64
架構
合併test_armv7和test_arm64:lipo -create test_armv7 test_arm64 -output test0
app
刪除test中armv7s版本的二進制文件能夠執行:lipo test -remove armv7s -output test1
socket
通用二進制的"通用"不止針對能夠直接運行的能夠執行文件,系統中的動態庫.dylib文件,靜態庫.a文件以及Framework等均可以是通用二進制文件,對它們一樣也可使用lipo
命令來進行管理;ide
接下來打開咱們的Xcode,按command + shift + o輸入mach-o/fat.h就能夠看到對通用二進制文件格式的聲明,從文件的命名和聲明來看,將通用二進制叫做胖二進制或許更合適;胖二進制的頭部定義以下: magic
字段被定義爲常量FAT_MAGIC,它的取值是固定的0xcafebabe,表示這是一個通用二進制文件;這裏要說一下字節序,計算機硬件有兩種儲存數據的方式,分別爲大端字節序,和小端字節序,大端字節序:高位字節在前,低位字節在後,這是人類讀寫數值的方法。小端字節序:低位字節在前,高位字節在後,是大多數機器讀取數據的方式,以下圖所示
nfat_arch
字段表示後面的Mach-O文件的數量
每一個通用二進制架構信息都使用fat_arch結構體表示,在fat_header結構體以後,緊接着的是一個或多個連續的fat_arch結構體,它的定義以下: cputype
字段是cpu說明符,類型是cpu_type_t
,定義在<mach/machine.h>文件,使用一樣的command + shift + o而後輸入頭文件的方法能夠打開<mach/machine.h>文件,在macOS上取值通常爲CPU_TYPE_I386
或CPU_TYPE_X86_64
,在iOS平臺上通常是CPU_TYPE_ARM
或CPU_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能夠十分清楚的看到這些信息 在fat_arch結構體往下就是具體的Mach-O格式文件了,它的內容複雜得多,將在下一小節進行討論。
到底什麼是Mach-O文件我翻閱了網上無數的文章,幾乎沒有人給出明確的答案,我這裏給出我本身的理解,只要是符合某種特定結構或者說格式的二進制文件均可以稱之爲Mach-O文件,也能夠說Mach-O文件是一種有着特定結構的二進制文件,這個特定的結構咱們後面會講到,熟悉Mach-O文件格式,有助於瞭解蘋果軟件底層運行機制,更好的掌握dyld加載Mach-O的步驟,爲本身動手開發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文件結構以下圖所示: 從圖中能夠了解到一個Mach-O文件的結構包括Header,Load commands和Data
可使用otool
命令來查看Mach-O文件的頭部信息 這個部分的定義,能夠經過在Xcode中,按command + shift + o輸入mach-o/loader.h的方式找到
magic
在截圖中都能看到的宏定義,對32位架構的程序來講,它的值就是0xfeedface,可使用MH_MAGIC宏代替;對64位架構的程序來講,它的值就是0xfeedfacf,對應的宏MH_MAGIC_64cputype
和上一節中所講的fat_header結構體的含義徹底相同cpusubtype
同上filetype
表示Mach-O文件的具體類型,值有下圖所示的12種,常見的有MH_EXECUTE(可執行文件),MH_DYLIB(動態庫),MH_DYLINKER(動態鏈接器),MH_DSYM(符號表文件)ncmds
load 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的信息
Load Commands描述的是文件的加載信息,加載信息有不少,加載的段、符號表、動態庫信息等都在Commands中取到。這個部分信息仍是比較有用的,咱們能夠從這裏獲取到符號表和字符串表的偏移量,下文中會有詳細的解釋。
Load Commands加載命令緊跟在Header以後,全部加載命令的前兩個字段必須是cmd和cmdsize,cmd字段用該命令類型的常量填充,頭文件中定義了許多的宏用於該字段,每一個命令類型都有一個特定的結構;cmdsize字段是以字節爲單位的特定加載命令結構的大小,再加上它後面做爲加載命令一部分的任何內容(即節結構、字符串等)要前進到下一個加載命令,能夠將cmdsize加上當前加載命令的偏移量 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_DYLIB
、LC_SEGMENT
、LC_MAIN
、LC_CODE_SIGNATURE
、LC_ENCRYPTION_INFO
等,下面介紹其中的幾個
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_version
與compatibility_version
指明瞭前當版本與兼容的版本號
若是你看了個人上一篇文章代碼注入裏面提到了yololib
,這個工具的原理基本就是利用這條LC_LOAD_DYLIB
加載命令的相關信息實現的
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
:段加載命令,描述了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加載命令,這裏就不一一介紹了...貼一個圖大概瞭解下
使用MachOView查看Load Commands的內容
數據區,除了Header和Load Commands外全部的原始數據。Load Commands是對數據的彙總提示,而數據區則是真實的數據。Load Commands與數據區的關係就像書的目錄與章節的關係,如圖所示,Segment爲__TEXT的段裏,顯示有8個section,每一個section具體的內容就在Data區裏了 接下里介紹幾個比較重要的section
__TEXT
,__text
)這裏存放的是彙編後的代碼,當咱們進行編譯時,每一個.m文件會通過預編譯->編譯->彙編造成.o文件,稱之爲目標文件。彙編後,全部的代碼會造成彙編指令存儲在.o文件的(__TEXT,__text
)區((__DATA,__data
)也是相似)。連接後,全部的.o文件會合併成一個文件,全部.o文件的(__TEXT,__text
)數據都會按連接順序存放到應用文件的(__TEXT,__text
)中。
__TEXT,__objc_methname
)這裏存放了項目裏,全部咱們用Objective-C寫的方法名
__TEXT,__objc_classname
)這裏存放了項目裏全部Objective-C類的名字 class-dump工具可以解析出每一個類的方法,屬性,成員變量,應該就是來自上面兩個section的數據了,固然這只是個人猜想,具體怎麼實現的就要去看class-dump的源碼了
符號表,這個是重點中的重點,符號表是將地址和符號聯繫起來的橋樑。符號表並不能直接存儲符號,而是存儲符號位於字符串表的位置。
字符串表全部的變量名、函數名等,都以字符串的形式存儲在字符串表中。
動態符號表存儲的是動態庫函數位於符號表的偏移信息。(__DATA
,__la_symbol_ptr
) section 能夠從動態符號表中獲取到該section位於符號表的索引數組。動態符號表並不存儲符號信息,而是存儲其位於符號表的偏移信息。Fishhook源碼看起來比較複雜主要是由於hook的是動態連接的函數,索引和連接關係比較繞。可是咱們本身編寫的C函數不是動態連接的,而是在編譯連接後代碼指令就存儲在文件內部的函數,所以不會用到動態符號表。
固然,關於Mach-O文件的知識遠不止這麼點,可是要徹底講清楚裏面的全部內容,那估計不是這麼一篇文章可以講的清楚的,至少也得是一本書了,我也只是網上收集到的一些資料,本身寫了篇總結而已
另外這篇文章借鑑和參考瞭如下這兩篇文章: