Mach-o文件
Mach-O 是iOS/macOS系統(tǒng)上應(yīng)用程序的格式
通用二進制文件(胖二進制文件)
因為MacOSX最早運行于PowerPC(PPC)架構(gòu),后來移植到Intel,通用二進制的文件可以在PPC和X86兩種處理器上執(zhí)行,所以蘋果提出了通用二進制這個概念,iOS應(yīng)用需要支持不同的CPU架構(gòu)(armv7,arm64),所以默認打包出來的是一個Universal Binary(通用二進制)格式文件,使用file命令或者lipo命令查看文件信息


查看抖音APP的可執(zhí)行文件

積目現(xiàn)在只支持arm64 CPU架構(gòu),所以打出的包只有arm64架構(gòu)格式。
胖二進制文件只是將不同架構(gòu)的Mach-O文件打包在一起,再在文件起始位置加上fat_header結(jié)構(gòu)來說明包含的Mach-O文件支持架構(gòu)和偏移地址信息

Fat Header 可以在XNU源碼mach-o/fat.h中看到定義

Mach-O文件結(jié)構(gòu)
fat_arch數(shù)據(jù)之后就是每一個Mach-O文件的分布了,Mach-O文件是由3部分組成:Mach-O頭部(Header), 加載命令(Load Commands), 數(shù)據(jù)塊(Data)

Mach-O頭部(Header)
Mach-O頭部(Header)保存了CPU架構(gòu),大小端序,文件類型,加載命令數(shù)量等一些基本信息,通過頭部就能按順序向下解析Match-O文件
Mach-O頭部定義
頭部信息定義在XNU源碼中mach-o/loader.h中,32位架構(gòu)的CPU使用mach_header結(jié)構(gòu)體來描述頭部信息,64位架構(gòu)的CPU使用mach_header_64結(jié)構(gòu)體描述頭部信息

filetype文件類型也定義在XNU源碼中mach-o/loader.h中,列舉了一些常見的文件類型

Mach-O文件的標志信息也定義在XNU源碼中mach-o/loader.h中
標志信息中的 MH_PIE 只在MH_EXECUTE中使用,表示啟用ASLR地址空間布局隨機化來增加程序的安全性

除了使用otool工具在命令行查看Mach-O文件之外還可以使用MachOView可視化工具直接打開Mach-O文件,
使用MachOView打開dyld

使用MachOView打開UIKit

Mach-O加載命令
通過加載命令(Load Commands)告訴加載器如何處理二進制數(shù)據(jù),有些命令是內(nèi)核處理的,有些是動態(tài)鏈接器處理的。
加載命令的結(jié)構(gòu)體定義

在XNU源碼—加過注釋mach-o/loader.h中可以看到LoadCommands命令的定義
使用MachOView打開的Mach-O文件中LoadCommands部分結(jié)構(gòu)圖

重要的一些命令
LC_SEGMENT 和 LC_SEGMENT_64
這兩個都是段的加載命令,每個段定義了一個虛擬內(nèi)存區(qū)域,動態(tài)鏈接器負責把這個區(qū)域映射到進程地址空間 LC_SEGMENT使用 segment_command 結(jié)構(gòu)體標識, LC_SEGMENT_64使用 segment_command_64結(jié)構(gòu)體表示
segment_command_64在XNU源碼中的定義

按照LC_SEGMENT_64加載Segment到內(nèi)存中:系統(tǒng)從文件偏移量為fileoff處加載filesize字節(jié)內(nèi)容到虛擬內(nèi)存地址vmaddr處的vmsize字節(jié)。每個段的頁面都根據(jù)initprot進行初始化,initprot指定了如何通過讀,寫,執(zhí)行位初始化頁面的保護級別,段的保護設(shè)置可以動態(tài)改變,但是不能超過maxprot中指定的值(在iOS中 +X和+W是互斥的)
在MachOView中看segment_command_64的內(nèi)容

_PAGEZERO
靜態(tài)鏈接器創(chuàng)建了_PAGEZERO,作為可執(zhí)行文件的第一個段,這個段在虛擬內(nèi)存中的位置和大小都是0,不能讀寫,不能執(zhí)行
_TEXT
包含了可執(zhí)行的一些代碼合其他一些只讀數(shù)據(jù),靜態(tài)鏈接器設(shè)置該段的虛擬內(nèi)存的權(quán)限為可讀,可執(zhí)行,這些代碼可以被進程執(zhí)行,但是不能修改。
在MachOView中查看_TEXT段的section定義:

_DATA
包含了將會被更改的數(shù)據(jù),靜態(tài)鏈接器設(shè)置該段的虛擬內(nèi)存權(quán)限為可讀,可寫
在MachOView中查看_DATA段的section定義:

非懶綁定:在動態(tài)鏈接器加載程序的時候就會綁定真實的調(diào)用地址,之后直接使用即可??梢岳斫鉃橹鲃咏壎?br> 懶綁定:只有在方法被調(diào)用的時候才會去尋找對應(yīng)的調(diào)用地址,然后再執(zhí)行,可以理解為被動綁定
一個段可以包含0個或者多個Section,32位和64位分別用section和section_64結(jié)構(gòu)體表示

段和section的命名規(guī)則:大寫代表的是段,小寫代表的是section
LC_LOAD_DYLIB 和 LC_LOAD_WEAK_DYLIB
指向的都是程序依賴的加載庫的信息使用MachOView查看LC_LOAD_DYLIB加載命令可以看到當前Mach-O文件依賴的程序庫

LC_LOAD_DYLIB在XNU源碼中是使用dylib_command 結(jié)構(gòu)體定義的


dylib_command對應(yīng)的結(jié)構(gòu)也可能是LC_LOAD_WEAK_DYLIB, LC_LOAD_DYLIB 和 LC_LOAD_WEAK_DYLIB它們都表示需要加載一個動態(tài)庫,通過LC_LOAD_WEAK_DYLIB聲明的依賴庫是可選的,如果加載的過程中缺少這些動態(tài)庫主程序會繼續(xù)執(zhí)行,不會有什么影響,通過LC_LOAD_DYLIB加載動態(tài)庫,依賴庫若是沒有找到,加載器會放棄并結(jié)束該進程
加載路徑可以是/System/Library/ 和 、usr/lib/ 這種系統(tǒng)路徑,還可以通過@rpath,@executable_path 來指定路徑
@rpath是LC_RPATH 加載命令指定路徑的,在iOS上存放應(yīng)用自己的Framework文件默認路徑為@executable_path/Framework, @executable_path是當前可執(zhí)行文件的目錄,在MacOS上可以通過 install_name_tool工具修改依賴庫的路徑
LC_CODE_SIGNATURE
LC_CODE_SIGNATURE是代碼簽名加載命令,通常位于最后一個段中,描述了Mach-O文件的代碼簽名信息,在iOS中如果簽名校驗不通過,進程會立刻被內(nèi)核用SIGKILL命令殺死,在XNU源碼中使用linkedit_data_command結(jié)構(gòu)體表示
其他命令
- LC_DYLD_INFO_ONLY
記錄了動態(tài)鏈接的重要信息,動態(tài)鏈接器要根據(jù)它來進行地址重定向 - LC_SYMTAB
文件所使用的符號表 - LC_DYSYMTAB
動態(tài)鏈接器所使用的符號表,可以獲取到間接符號表的偏移量 - LC_LOAD_DYLINKER
默認的動態(tài)鏈接器路徑 /usr/bin/dyld - LC_UUID
Mach-O文件的唯一標識。DSYM文件和崩潰堆棧中都存在這個值,用來分析對應(yīng)的崩潰位置 - LC_VERSION_MIN_IPHONEOS
mach-O文件要求的最低系統(tǒng)版本和Xcode中配置的target有關(guān)系 - LC_SOURCE_VERSION
構(gòu)建二進制文件的源代碼版本 - LC_MAIN
程序的入口(main函數(shù)地址)。動態(tài)鏈接器獲取到這個地址,然后開始執(zhí)行代碼 - LC_ENCRYPTION_INFO_64
文件加密信息,包括加密標記,加密數(shù)據(jù)的偏移大小 - LC_RPATH
@path的路徑,指定動態(tài)鏈接器搜索路徑列表 - LC_FUNCTION_STARTS
函數(shù)的起始地址表,調(diào)試器可以判斷一個地址是否在這個表的范圍內(nèi) - LC_DATA_IN_CODE
定義在代碼段內(nèi)的非指令表