Mach-O
【Mach-O】 為 Mach Object 文件格式的縮寫,是 iOS 系統(tǒng)不同運(yùn)行時(shí)期 可執(zhí)行文件 的文件類型統(tǒng)稱。它是一種用于 可執(zhí)行文件、目標(biāo)代碼、動(dòng)態(tài)庫(kù)、內(nèi)核轉(zhuǎn)儲(chǔ)的文件格式。
【Mach-O】 的三種文件類型:Executable、Dylib、Bundle
Executable 是 app 的二進(jìn)制主文件。
Dylib 是動(dòng)態(tài)庫(kù),動(dòng)態(tài)庫(kù)分為 動(dòng)態(tài)鏈接庫(kù) 和 動(dòng)態(tài)加載庫(kù)。
動(dòng)態(tài)鏈接庫(kù):在沒(méi)有被加載到內(nèi)存的前提下,當(dāng)可執(zhí)行文件被加載,動(dòng)態(tài)庫(kù)也隨著被加載到內(nèi)存中?!倦S著程序啟動(dòng)而啟動(dòng)】
動(dòng)態(tài)加載庫(kù):當(dāng)需要的時(shí)候再使用 dlopen 等通過(guò)代碼或者命令的方式加載?!境绦騿?dòng)之后】
Bundle 是一種特殊類型的Dylib,你無(wú)法對(duì)其進(jìn)行鏈接。所能做的是在Runtime運(yùn)行時(shí)通過(guò)dlopen來(lái)加載它,它可以在macOS 上用于插件。
Image (鏡像文件)包含了上述的三種類型。
Framework 可以理解為動(dòng)態(tài)庫(kù)。
Mach-O的結(jié)構(gòu)
Header:保存【Mach-O】的一些基本信息,包括運(yùn)行平臺(tái)、文件類型、LoadCommands指令的個(gè)數(shù)、指令總大小,dyld標(biāo)記Flags等等。
Load Commands:緊跟Header,這些加載指令清晰地告訴加載器如何處理二進(jìn)制數(shù)據(jù),有些命令是由內(nèi)核處理的,有些是由動(dòng)態(tài)鏈接器處理的。加載【Mach-O】文件時(shí)會(huì)使用這部分?jǐn)?shù)據(jù)確定內(nèi)存分布以及相關(guān)的加載命令,對(duì)系統(tǒng)內(nèi)核加載器和動(dòng)態(tài)連接器起指導(dǎo)作用。比如我們的main()函數(shù)的加載地址、程序所需的dyld的文件路徑、以及相關(guān)依賴庫(kù)的文件路徑。
Data:每個(gè)segment的具體數(shù)據(jù)保存在這里,包含具體的代碼、數(shù)據(jù)等等。
segment:【Mach-O】 鏡像文件 是由 segments 段組成的。段的名稱為大寫格式。所有的段都是 page size 的倍數(shù),在arm64上為 16kB,其它架構(gòu)為 4KB。
常見(jiàn)的segments:
__TEXT:代碼段,包含頭文件、代碼和只讀常量。只讀不可修改
__DATA:數(shù)據(jù)段,包含全局變量,靜態(tài)變量等。可讀可寫
_LINKEDIT:如何加載程序,包含了方法和變量的元數(shù)據(jù)(位置,偏移量),以及代碼簽名等信息。只讀不可修改。
有兩種主要的技術(shù)來(lái)保證應(yīng)用的安全:ASLR 和 Code Sign
【ASLR】的全稱是Address space layout randomization,翻譯過(guò)來(lái)就是“地址空間布局隨機(jī)化”。App被啟動(dòng)的時(shí)候,程序會(huì)被映射到邏輯的地址空間,這個(gè)邏輯的地址空間有一個(gè)起始地址,而【ASLR】技術(shù)使得這個(gè)起始地址是隨機(jī)的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函數(shù)的地址。
【Code Sign】相信大多數(shù)開(kāi)發(fā)者都知曉,這里要提一點(diǎn)的是,為了在運(yùn)行時(shí) 驗(yàn)證【Mach-O】 文件的簽名,在進(jìn)行【Code Sign】的時(shí)候,加密哈希不是針對(duì)于整個(gè)文件,而是針對(duì)于每一個(gè)Page的。并存儲(chǔ)在 __LINKEDIT 中。這就保證了在dyld進(jìn)行加載的時(shí)候,可以對(duì)每一個(gè)page進(jìn)行獨(dú)立的驗(yàn)證。
dyld
當(dāng)內(nèi)核完成映射進(jìn)程的工作后,會(huì)將名字為 dyld 的 Mach-O 文件映射到進(jìn)程中的隨機(jī)地址,它將PC 寄存器設(shè)為 dyld 的地址并運(yùn)行。dyld 在應(yīng)用進(jìn)程中運(yùn)行的工作是加載應(yīng)用依賴的所有動(dòng)態(tài)鏈接庫(kù),準(zhǔn)備好運(yùn) 行所需的一切,它擁有的權(quán)限跟應(yīng)用程序一樣。
dyld(the dynamic link editor),【動(dòng)態(tài)鏈接器】是蘋果操作系統(tǒng)一個(gè)重要部分,在 iOS / macOS 系統(tǒng)中,僅有很少的進(jìn)程只需內(nèi)核就可以完成加載,基本上所有的進(jìn)程都是動(dòng)態(tài)鏈接的,所以 Mach-O 鏡像文件中會(huì)有很多對(duì)外部的庫(kù)和符號(hào)的引用,但是這些引用并不能直接用,在啟動(dòng)時(shí)還必須要通過(guò)這些引用進(jìn)行內(nèi)容填充,這個(gè)填充的工作就是由 dyld 來(lái)完成的。
【動(dòng)態(tài)鏈接加載器】在系統(tǒng)中以一個(gè)用戶態(tài)的可執(zhí)行文件形式存在,一般應(yīng)用程序會(huì)在Mach-O文件部分指定一個(gè) LC_LOAD_DYLINKER 的加載命令,此加載命令指定了dyld的路徑,通常它的默認(rèn)值是“/usr/lib/dyld”。系統(tǒng)內(nèi)核在加載Mach-O文件時(shí),會(huì)使用該路徑指定的程序作為動(dòng)態(tài)庫(kù)的加載器來(lái)加載dylib。
dyld 流程
Load dylibs -> Rebase -> Bind -> ObjC ->Initializers
Load dylibs:
從主執(zhí)行文件header獲取到需要加載的所依賴的動(dòng)態(tài)庫(kù)列表,而header早就被內(nèi)核映射過(guò)。然后它需要找到每個(gè)dylib,然后打開(kāi)文件,讀取文件起始位置,確保它是Mach-O文件。接著會(huì)找到代碼簽名并將其注冊(cè)到內(nèi)核。然后在dylib文件的每個(gè)segment上調(diào)用mmap()。應(yīng)用所依賴的dylib文件可能會(huì)再依賴其他dylib,所以dyld所需要加載的是動(dòng)態(tài)庫(kù)列表一個(gè)遞歸依賴的集合。一般應(yīng)用會(huì)加載100到400 個(gè)dylib文件,但大部分都是系統(tǒng)的dylib,它們會(huì)被預(yù)先計(jì)算和緩存起來(lái),加載速度很快。
Fix-ups:
在加載所有的動(dòng)態(tài)鏈接庫(kù)之后,它們只是處在相互獨(dú)立的狀態(tài),需要將它們綁定起來(lái),這就是Fix-ups。代碼簽名使得我們不能修改指令,那樣就不能讓一個(gè)dylib 調(diào)用另一個(gè) dylib,這是就需要很多間接層。
Mach-O中有很多符號(hào),有指向當(dāng)前 Mach-O 的,也有指向其他 dylib 的,比如printf。那么,在運(yùn)行時(shí),代碼如何準(zhǔn)確的找到printf的地址呢?
Mach-O中采用了PIC技術(shù),全稱是Position Independ code。意味著代碼可以被加載到間接的地址上。當(dāng)你的程序要調(diào)用printf的時(shí)候,會(huì)先在 __DATA 段中建立一個(gè)指針指向printf,在通過(guò)這個(gè)指針實(shí)現(xiàn)間接調(diào)用。dyld這時(shí)候需要做一些fix-up工作,即幫助應(yīng)用程序找到這些符號(hào)的實(shí)際地址。主要包括兩部分:rebasing和binding。
Rebasing:在鏡像內(nèi)部調(diào)整指針的指向。
Binding: 將指針指向鏡像外部的內(nèi)容。
之所以需要Rebase,是因?yàn)閯倓偺岬降?ASLR 使得地址隨機(jī)化,導(dǎo)致起始地址不固定,另外由于 Code Sign,導(dǎo)致不能直接修改 Image。Rebase的時(shí)候只需要增加對(duì)應(yīng)的偏移量即可。(待Rebase的數(shù)據(jù)都存放在__LINKEDIT中,可以通過(guò)MachOView查看:Dynamic Loader Info -> Rebase Info)
Binding就是將這個(gè)二進(jìn)制調(diào)用的外部符號(hào)進(jìn)行綁定的過(guò)程。 比如我們objc代碼中需要使用到NSObject, 即符號(hào)OBJC_CLASS$_NSObject,但是這個(gè)符號(hào)又不在我們的二進(jìn)制中,在系統(tǒng)庫(kù) Foundation.framework中,因此就需要Binding這個(gè)操作將對(duì)應(yīng)關(guān)系綁定到一起。
Rebase解決了內(nèi)部的符號(hào)引用問(wèn)題,而外部的符號(hào)引用則是由Bind解決。在解決Bind的時(shí)候,是根據(jù)字符串匹配的方式查找符號(hào)表,所以這個(gè)過(guò)程相對(duì)于Rebase來(lái)說(shuō)是略慢的。
dyld 2 和 dyld 3
在 iOS 13之前,所有的第三方App都是通過(guò)dyld 2來(lái)啟動(dòng) App 的,主要過(guò)程如下:
解析 Mach-O的Header 和 Load Commands,找到其依賴的庫(kù),并遞歸找到所有依賴的庫(kù)
加載Mach-O文件
進(jìn)行符號(hào)查找
綁定和變基
運(yùn)行初始化程序
dyld 3被分為了三個(gè)組件:
一個(gè)進(jìn)程外的Mach-O 解析器
預(yù)先處理了所有可能影響啟動(dòng)速度的search path、@rpaths和環(huán)境變量
然后分析Mach-O的Header和依賴,并完成了所有符號(hào)查找的工作
最后將這些結(jié)果創(chuàng)建成一個(gè)啟動(dòng)閉包
這是一個(gè)普通的daemon進(jìn)程,可以使用通常的測(cè)試架構(gòu)
一個(gè)進(jìn)程內(nèi)的引擎,用來(lái)運(yùn)行啟動(dòng)閉包
這部分在進(jìn)程中處理
驗(yàn)證啟動(dòng)閉包的安全性,然后映射到dylib之中,再跳轉(zhuǎn)到main函數(shù)
不需要解析Mach-O的 Header 和依賴,也不需要符號(hào)查找。
一個(gè)啟動(dòng)閉包緩存服務(wù)
系統(tǒng)App的啟動(dòng)閉包被構(gòu)建在一個(gè)Shared Cache 中,我們甚至不需要打開(kāi)一個(gè)單獨(dú)的文件
對(duì)于第三方的App,我們會(huì)在App安裝或者升級(jí)的時(shí)候構(gòu)建這個(gè)啟動(dòng)閉包。
在iOS、tvOS、watchOS中,這一切都是App啟動(dòng)之前完成的。在macOS上,由于有Side Load App,進(jìn)程內(nèi)引擎會(huì)在首次啟動(dòng)的時(shí)候啟動(dòng)一個(gè)daemon進(jìn)程,之后就可以使用啟動(dòng)閉包啟動(dòng)了。
dyld 3 把很多耗時(shí)的查找、計(jì)算和I/O 的事件都預(yù)先處理好,這使得啟動(dòng)速度有了很大的提升。
App加載流程
編譯過(guò)程
其中編譯過(guò)程如下圖所示,主要分為以下幾步:
源文件:載入.h、.m、.cpp等文件
預(yù)處理:替換宏,刪除注釋,展開(kāi)頭文件,產(chǎn)生.i文件
編譯:將.i文件轉(zhuǎn)換為匯編語(yǔ)言,產(chǎn)生.s文件
匯編:將匯編文件轉(zhuǎn)換為機(jī)器碼文件,產(chǎn)生.o文件
dyld加載流程分析
根據(jù)dyld源碼,以及l(fā)ibobjc、libSystem、libdispatch源碼協(xié)同分析
在load方法處加一個(gè)斷點(diǎn),通過(guò)bt堆棧信息查看app啟動(dòng)是從哪里開(kāi)始的
+ (void)load{
NSLog(@"%s",__func__); //此處加斷點(diǎn)
}
控制臺(tái)數(shù)據(jù)結(jié)果:
/*
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x000000010532ee17 002-應(yīng)用程加載分析`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
frame #1: 0x00007fff201805e3 libobjc.A.dylib`load_images + 1442
frame #2: 0x0000000105342e54 dyld_sim`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 425
frame #3: 0x0000000105351887 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 437
frame #4: 0x000000010534fbb0 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #5: 0x000000010534fc50 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #6: 0x00000001053432a9 dyld_sim`dyld::initializeMainExecutable() + 199
frame #7: 0x0000000105347d50 dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4431
frame #8: 0x00000001053421c7 dyld_sim`start_sim + 122
frame #9: 0x000000010bea485c dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2308
frame #10: 0x000000010bea24f4 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 837
frame #11: 0x000000010be9d227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
frame #12: 0x000000010be9d025 dyld`_dyld_start + 37
(lldb)
*/
【app啟動(dòng)起點(diǎn)】:通過(guò)程序運(yùn)行發(fā)現(xiàn),是從dyld
中的_dyld_start
開(kāi)始的,所以需要去OpenSource下載一份dyld的源碼來(lái)進(jìn)行分析
參考資料:
[iOS-底層原理 15:dyld加載流程](http://www.lxweimin.com/p/db765ff4e36a)
[iOS 應(yīng)用程序加載](http://www.lxweimin.com/p/bffb5bdb4f13)