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