文集:iOS 知識補充
前言
- 這篇主要內(nèi)容記錄iOS 應(yīng)用程序加載的學習,用于在OC底層學習探索做下筆記,學習參考轉(zhuǎn)載:賣饃工程師
1. 理論基礎(chǔ)速成
1.1 靜態(tài)庫與動態(tài)庫
庫是已寫好的、供使用的 可復用代碼,每個程序都要依賴很多基礎(chǔ)的底層庫。
從本質(zhì)上,庫是一種可執(zhí)行代碼的二進制形式??梢员?code>操作系統(tǒng)載入內(nèi)存執(zhí)行。庫分為兩種:靜態(tài)庫(.a .lib)和 動態(tài)庫 (framework .so .dll)。
所謂的靜態(tài)、動態(tài)指的是 鏈接的過程。
將一個程序編譯成可執(zhí)行程序的步驟如下:

1.1.1 靜態(tài)庫
之所以稱之為【靜態(tài)庫】,是因為在鏈接階段,會將匯編生成的目標文件.o 與 引用的庫一起鏈接到可執(zhí)行文件中。對應(yīng)的鏈接方式稱為 靜態(tài)鏈接。

如果多個進程需要引用到【靜態(tài)庫】,在內(nèi)存中就會存在多份拷貝,如上圖中進程1 用到了靜態(tài)庫1、5,進程2也用到了進程1、5,那么靜態(tài)庫1、5在編譯期就分別被鏈接到了進程1和進程2中,假設(shè)靜態(tài)庫1占用2M內(nèi)存,如果有20個這樣的進程需要用到靜態(tài)庫1,將占用40M的空間。

【靜態(tài)庫】的特點如下:
- 靜態(tài)庫對函數(shù)庫的鏈接是在
編譯期完成的。執(zhí)行期間代碼裝載速度快。 - 使可執(zhí)行文件變大,浪費空間和資源(
占空間)。 - 對程序的更新、部署與發(fā)布不方便,需要
全量更新。如果 某一個靜態(tài)庫更新了,所有使用它的應(yīng)用程序都需要重新編譯、發(fā)布給用戶。
1.1.2 動態(tài)庫
【動態(tài)庫】在程序編譯時并不會鏈接到目標代碼中,而是在運行時才被載入。不同的應(yīng)用程序如果調(diào)用相同的庫,那么在內(nèi)存中只需要有一份該共享庫的實例,避免了空間浪費問題。同時也解決了靜態(tài)庫對程序的更新的依賴,用戶只需更新動態(tài)庫即可。

【動態(tài)庫】在內(nèi)存中只存在一份拷貝,如果某一進程需要用到動態(tài)庫,只需在運行時動態(tài)載入即可。

【動態(tài)庫】的特點:
動態(tài)庫把對一些庫函數(shù)的鏈接載入推遲到程序運行時期(占時間)。
可以實現(xiàn)進程之間的資源共享。(因此動態(tài)庫也稱為共享庫)
將一些程序升級變得簡單,不需要重新編譯,屬于增量更新。
1.2 Mach-O

程序想要運行起來,它的可執(zhí)行文件格式就要被操作系統(tǒng)所理解,比如 ELF(Executable and Linking Format) 是 Linux 下可執(zhí)行文件的格式,PE32/PE32+(Portable Executable) 是 windows 的可執(zhí)行文件的格式,那么對于 OS X和 iOS 來說 Mach-O 是其可執(zhí)行文件的格式。
【Mach-O】 為 Mach Object 文件格式的縮寫,是 iOS 系統(tǒng)不同運行時期 可執(zhí)行文件 的文件類型統(tǒng)稱。它是一種用于 可執(zhí)行文件、目標代碼、動態(tài)庫、內(nèi)核轉(zhuǎn)儲的文件格式。
【Mach-O】 的三種文件類型:Executable、Dylib、Bundle
- Executable
Executable 是 app 的二進制主文件,我們可以在 Xcode 項目中的 products 文件中找到它:

- Dylib
Dylib 是動態(tài)庫,動態(tài)庫分為 動態(tài)鏈接庫 和 動態(tài)加載庫。
動態(tài)鏈接庫:在沒有被加載到內(nèi)存的前提下,當可執(zhí)行文件被加載,動態(tài)庫也隨著被加載到內(nèi)存中。【隨著程序啟動而啟動】動態(tài)加載庫:當需要的時候再使用dlopen等通過代碼或者命令的方式加載。【程序啟動之后】
- Bundle
Bundle 是一種特殊類型的Dylib,你無法對其進行鏈接。所能做的是在Runtime運行時通過dlopen來加載它,它可以在macOS 上用于插件。
- Image 和 Framework
Image (鏡像文件)包含了上述的三種類型 Framework 可以理解為動態(tài)庫。
1.2.1 Mach-O的結(jié)構(gòu)

【Mach-O】是一個以數(shù)據(jù)塊分組的二進制字節(jié)流,每個【Mach-O】文件包括一個Mach-O頭,然后是一系列的載入命令,再是一個或多個段,每個段包括0到255個塊。
Header結(jié)構(gòu)
保存【Mach-O】的一些基本信息,包括運行平臺、文件類型、LoadCommands指令的個數(shù)、指令總大小,dyld標記Flags等等。
Load Commands
緊跟Header,這些加載指令清晰地告訴加載器如何處理二進制數(shù)據(jù),有些命令是由內(nèi)核處理的,有些是由動態(tài)鏈接器處理的。加載【Mach-O】文件時會使用這部分數(shù)據(jù)確定內(nèi)存分布以及相關(guān)的加載命令,對系統(tǒng)內(nèi)核加載器和動態(tài)連接器起指導作用。比如我們的main()函數(shù)的加載地址、程序所需的dyld的文件路徑、以及相關(guān)依賴庫的文件路徑。
Data
每個segment的具體數(shù)據(jù)保存在這里,包含具體的代碼、數(shù)據(jù)等等。
1.2.1.1 segment段:

【Mach-O】 鏡像文件 是由 segments 段組成的。
段的名稱為大寫格式
所有的段都是 page size 的倍數(shù)。
在arm64上為 16kB
其它架構(gòu)為 4KB
這里在普及一下 虛擬內(nèi)存 和 內(nèi)存頁 的知識:
具有 VM 機制的操作系統(tǒng),會對每個運行的進程創(chuàng)建一個邏輯地址空間 logical address space 或者叫 虛擬地址空間 virtual address space;該空間的大小由操作系統(tǒng)位數(shù)決定。

虛擬地址空間 會被分為相同大小的塊,這些塊被稱為內(nèi)存頁(page)。計算機處理器和它的內(nèi)存管理單元(MMU - memory management unit)維護著一張將程序的 虛擬地址空間 映射到 物理地址 上的分頁表 page table 。
在 macOS 和早版本的 iOS 中,分頁大小為 4kb。在之后的基于A7 和 A8 的系統(tǒng)中,虛擬內(nèi)存(64位的地址空間)地址空間的分頁大小變?yōu)榱?6kb,而物理RAM上的內(nèi)存分頁大小仍然維持在 4kb;基于 A9 及以后的系統(tǒng),虛擬內(nèi)存和物理內(nèi)存的分頁都是16kb。
1.2.1.2 section

在 segment 段內(nèi)部還有許多的 section 區(qū)。section 名稱為小寫格式。 section節(jié) 實際上只是一個 segment 段的子范圍,它們沒有頁面大小的任何限制,但是它們是不重疊的。
1.2.1.3 常見的segments
- __TEXT:
代碼段,包含頭文件、代碼和只讀常量。只讀不可修改

- __DATA:
數(shù)據(jù)段,包含全局變量,靜態(tài)變量等。可讀可寫

- __LINKEDIT:
如何加載程序,包含了方法和變量的元數(shù)據(jù)(位置,偏移量),以及代碼簽名等信息。只讀不可修改。
1.2.2 Mach-O Universal Files

【Mach-O】 通用文件,將多種架構(gòu)的 Mach-O 文件合并而成。它通過 header 來記錄不同架構(gòu)在文件中的偏移量,segment 占多個分頁,header 占一頁的空間。header 單獨占一頁 有利于 虛擬內(nèi)存 的實現(xiàn)。
1.3 虛擬內(nèi)存

虛擬內(nèi)存是一層 間接尋址 。
【虛擬內(nèi)存】是在物理內(nèi)存上建立的一個邏輯地址空間。建立在進程和物理內(nèi)存之間的中間層,它向上(應(yīng)用)提供了一個連續(xù)的邏輯地址空間,向下隱藏了物理內(nèi)存的細節(jié)。
虛擬內(nèi)存被劃分為一個個大小相同的Page(64位系統(tǒng)上是16KB),提高管理和讀寫的效率。 Page又分為只讀和讀寫的Page。
虛擬內(nèi)存解決的是管理所有進程使用 物理RAM 的問題。通過添加間接層來讓每個進程使用 邏輯地址空間,它可以映射到RAM 上的某個物理頁上。這種映射 不是一對一的,邏輯地址可能映射不到 RAM 上,也有可能有多個邏輯地址映射到同一個物理RAM 上。
虛擬內(nèi)存使得邏輯地址可以沒有實際的物理地址,也可以讓多個邏輯地址對應(yīng)到一個物理地址。
- 針對第一種情況(邏輯地址可能映射不到 RAM ):在應(yīng)用執(zhí)行的時候,它被分配的邏輯地址空間都是可以訪問的,當應(yīng)用訪問一個邏輯Page,而在對應(yīng)的物理內(nèi)存中并不存在的時候,這時候就發(fā)生了一次
Page fault。當Page fault發(fā)生的時候,會中斷當前的程序,在物理內(nèi)存中尋找一個可用的Page,然后從磁盤中讀取數(shù)據(jù)到物理內(nèi)存,接著繼續(xù)執(zhí)行當前程序。- 而第二種情況(多個邏輯地址映射到同一個物理RAM 上)就是
多進程共享內(nèi)存。
對于文件可以不用一次性讀入整個文件,可以使用分頁映射 mmap()的方式獲取。也就是把文件 某個片段 映射到進程邏輯內(nèi)存的 某個頁 上。當某個想要讀取的頁沒有在內(nèi)存中,就會觸發(fā) page fault,內(nèi)核只會讀入那一頁,實現(xiàn)文件的 懶加載。也就是說 【Mach-O】 文件中的 __TEXT 段可以映射到多個進程,并可以懶加載,且進程之間 共享內(nèi)存。
__DATA 段是可讀寫的。這里使用到了Copy-On-Write技術(shù),簡稱【COW】。 也就是多個進程共享一頁內(nèi)存空間時,一旦有進程要做寫操作,它會先將這頁內(nèi)存內(nèi)容復制一份出來,然后重新映射邏輯地址到新的RAM 頁上。也就是這個進程自己擁有了那頁內(nèi)存的拷貝。這就涉及到了clean/dirty page 的概念。dirty page 含有進程自己的信息,而clean page 可以被內(nèi)核重新生成(重新讀磁盤)。多以 dirty page 的代價大于 clean page。
1.4 多進程加載Mach-O 鏡像

- 所以在多個進程加載【Mach-O】鏡像時,__TEXT 和 __LINKEDIT 因為是只讀的,都是可以
共享內(nèi)存的,讀取速度就會很快。 - 而 __DATA 因為是可讀寫的,就有可能產(chǎn)生
dirty page,如果檢測有 clean page 就可以直接使用,反之就需要重新讀取 DATA page。一旦產(chǎn)生了 dirty page,當dyld執(zhí)行結(jié)束后,__LINKEDIT 需要通知內(nèi)核當前頁面不再需要了,當別人需要使用的時候就可以重新 clean 這些頁面。

1.5 ASLR
有兩種主要的技術(shù)來保證應(yīng)用的安全:ASLR 和 Code Sign。
【ASLR】的全稱是Address space layout randomization,翻譯過來就是“地址空間布局隨機化”。App被啟動的時候,程序會被映射到邏輯的地址空間,這個邏輯的地址空間有一個起始地址,而【ASLR】技術(shù)使得這個起始地址是隨機的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函數(shù)的地址。
1.6 Code Signing
【Code Sign】相信大多數(shù)開發(fā)者都知曉,這里要提一點的是,為了在運行時 驗證【Mach-O】 文件的簽名,在進行【Code Sign】的時候,加密哈希不是針對于整個文件,而是針對于每一個Page的。并存儲在 __LINKEDIT 中。這就保證了在dyld進行加載的時候,可以對每一個page進行獨立的驗證。
1.7 exec()

exec()是一個系統(tǒng)調(diào)用。系統(tǒng)內(nèi)核把應(yīng)用程序映射到新的地址空間,且每次起始位置都是隨機的(因為ASLR)。并將起始位置到0x000000 這段范圍的進程權(quán)限都標記為不可讀寫不可執(zhí)行。如果是32 位進程,這個范圍至少是4kb ;如果是64位進程則至少是4GB。NULL指針引用和指針截斷誤差都是會被它捕獲,這個范圍也叫做 PAGEZERO。
1.8 dyld

當內(nèi)核完成映射進程的工作后,會將名字為 dyld 的 Mach-O 文件映射到進程中的隨機地址,它將PC 寄存器設(shè)為 dyld 的地址并運行。dyld 在應(yīng)用進程中運行的工作是加載應(yīng)用依賴的所有動態(tài)鏈接庫,準備好運行所需的一切,它擁有的權(quán)限跟應(yīng)用程序一樣。
dyld(the dynamic link editor),【動態(tài)鏈接器】是蘋果操作系統(tǒng)一個重要部分,在 iOS / macOS 系統(tǒng)中,僅有很少的進程只需內(nèi)核就可以完成加載,基本上所有的進程都是動態(tài)鏈接的,所以 Mach-O 鏡像文件中會有很多對外部的庫和符號的引用,但是這些引用并不能直接用,在啟動時還必須要通過這些引用進行內(nèi)容填充,這個填充的工作就是由 dyld 來完成的。
【動態(tài)鏈接加載器】在系統(tǒng)中以一個用戶態(tài)的可執(zhí)行文件形式存在,一般應(yīng)用程序會在Mach-O文件部分指定一個 LC_LOAD_DYLINKER 的加載命令,此加載命令指定了dyld的路徑,通常它的默認值是“/usr/lib/dyld”。系統(tǒng)內(nèi)核在加載Mach-O文件時,會使用該路徑指定的程序作為動態(tài)庫的加載器來加載dylib。
1.9 共享緩存
dyld加載時,為了優(yōu)化程序啟動,啟用了共享緩存(shared cache)技術(shù)。共享緩存會在進程啟動時被dyld映射到內(nèi)存中,之后,當任何Mach-O鏡像加載時,dyld首先會檢查該Mach-O鏡像與所需的動態(tài)庫是否在共享緩存中,如果存在,則直接將它在共享內(nèi)存中的內(nèi)存地址映射到進程的內(nèi)存地址空間。在程序依賴的系統(tǒng)動態(tài)庫很多的情況下,這種做法對程序啟動性能是有明顯提升的。
1.10 dyld 流程

- Load dylibs
從主執(zhí)行文件header獲取到需要加載的所依賴的動態(tài)庫列表,而header早就被內(nèi)核映射過。然后它需要找到每個dylib,然后打開文件,讀取文件起始位置,確保它是Mach-O文件。接著會找到代碼簽名并將其注冊到內(nèi)核。然后在dylib文件的每個segment上調(diào)用mmap()。應(yīng)用所依賴的dylib文件可能會再依賴其他dylib,所以dyld所需要加載的是動態(tài)庫列表一個遞歸依賴的集合。一般應(yīng)用會加載100到400 個dylib文件,但大部分都是系統(tǒng)的dylib,它們會被預先計算和緩存起來,加載速度很快。
- Fix-ups
在加載所有的動態(tài)鏈接庫之后,它們只是處在相互獨立的狀態(tài),需要將它們綁定起來,這就是Fix-ups。代碼簽名使得我們不能修改指令,那樣就不能讓一個dylib 調(diào)用另一個 dylib,這是就需要很多間接層。
Mach-O中有很多符號,有指向當前 Mach-O 的,也有指向其他 dylib 的,比如printf。那么,在運行時,代碼如何準確的找到printf的地址呢?
Mach-O中采用了PIC技術(shù),全稱是Position Independ code。意味著代碼可以被加載到間接的地址上。當你的程序要調(diào)用printf的時候,會先在 __DATA 段中建立一個指針指向printf,在通過這個指針實現(xiàn)間接調(diào)用。dyld這時候需要做一些fix-up工作,即幫助應(yīng)用程序找到這些符號的實際地址。主要包括兩部分:rebasing和binding。
- Rebasing 和 Binding

Rebasing:在鏡像內(nèi)部調(diào)整指針的指向。 Binding: 將指針指向鏡像外部的內(nèi)容。
之所以需要Rebase,是因為剛剛提到的 ASLR 使得地址隨機化,導致起始地址不固定,另外由于 Code Sign,導致不能直接修改 Image。Rebase的時候只需要增加對應(yīng)的偏移量即可。(待Rebase的數(shù)據(jù)都存放在__LINKEDIT中,可以通過MachOView查看:Dynamic Loader Info -> Rebase Info)
Binding就是將這個二進制調(diào)用的外部符號進行綁定的過程。 比如我們objc代碼中需要使用到NSObject, 即符號OBJC_CLASS$_NSObject,但是這個符號又不在我們的二進制中,在系統(tǒng)庫 Foundation.framework中,因此就需要Binding這個操作將對應(yīng)關(guān)系綁定到一起。
Rebase解決了內(nèi)部的符號引用問題,而外部的符號引用則是由Bind解決。在解決Bind的時候,是根據(jù)字符串匹配的方式查找符號表,所以這個過程相對于Rebase來說是略慢的。
1.11 dyld 2 和 dyld 3

在 iOS 13之前,所有的第三方App都是通過dyld 2來啟動 App 的,主要過程如下:
- 解析
Mach-O的Header和Load Commands,找到其依賴的庫,并遞歸找到所有依賴的庫 - 加載Mach-O文件
- 進行符號查找
- 綁定和變基
- 運行初始化程序
dyld 3被分為了三個組件:
一個進程外的
Mach-O解析器 預先處理了所有可能影響啟動速度的search path、@rpaths和環(huán)境變量 然后分析Mach-O的Header和依賴,并完成了所有符號查找的工作 最后將這些結(jié)果創(chuàng)建成一個啟動閉包 這是一個普通的daemon進程,可以使用通常的測試架構(gòu)一個進程內(nèi)的引擎,用來運行啟動閉包 這部分在進程中處理 驗證啟動閉包的安全性,然后映射到dylib之中,再跳轉(zhuǎn)到main函數(shù) 不需要解析
Mach-O的Header和依賴,也不需要符號查找。一個啟動閉包緩存服務(wù) 系統(tǒng)App的啟動閉包被構(gòu)建在一個
Shared Cache中,我們甚至不需要打開一個單獨的文件 對于第三方的App,我們會在App安裝或者升級的時候構(gòu)建這個啟動閉包。 在iOS、tvOS、watchOS中,這一切都是App啟動之前完成的。在macOS上,由于有Side Load App,進程內(nèi)引擎會在首次啟動的時候啟動一個daemon進程,之后就可以使用啟動閉包啟動了。
dyld 3 把很多耗時的查找、計算和I/O 的事件都預先處理好,這使得啟動速度有了很大的提升。
2、App 加載流程
有了前面的知識儲備,接下來將探索app的加載流程。
在應(yīng)用程序的入口 main()函數(shù)之前斷點,查看堆棧信息

可以看到,先于main函數(shù)調(diào)用的是 start,同時,這一流程是由libdyld.dylib庫執(zhí)行的。dyld 是開源庫,可以下載源碼探索。點擊下載dyld 源碼
為了看到更詳細的調(diào)用過程,我們在項目中的 ViewController 的 + (void) load方法打斷點。詳細堆棧信息如下

2.1 _dyld_start
可見,調(diào)用流程是從 _dyld_start 開始的,我們在下載好的源碼中搜索 _dyld_start 。在 dyldStartup.s 文件中找到了入口,這里是用匯編實現(xiàn)的,盡管在不同架構(gòu)下有所區(qū)別,但都是會調(diào)用 dyldbootstrap命名空間下的start 方法,這和上面的堆棧順序也是相同的。
call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
復制代碼
2.2 dyldbootstrap::start
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
rebaseDyld(dyldsMachHeader);
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
復制代碼
dyldbootstrap::start中,主要過程為:
①使用全局變量之前,對dyld進行rebase操作,以修復為 real pointer來運行;
②設(shè)置參數(shù)和環(huán)境變量;
③讀取 app二進制文件 Mach-O 的header 得到偏移量 appSlide,然后調(diào)用dyld 命名空間下的_main 方法。
2.3 dyld::_main
這里是dyld的入口。內(nèi)核加載了dyld然后跳轉(zhuǎn)到 _dyld_start來設(shè)置一些寄存器的值之后 進入這個方法。返回_dyld_start所跳轉(zhuǎn)到的目標程序的main函數(shù)地址。精簡的代碼如下:
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
......
// 設(shè)置運行環(huán)境,可執(zhí)行文件準備工作
......
// load shared cache 加載共享緩存
mapSharedCache();
......
reloadAllImages:
......
// instantiate ImageLoader for main executable 加載可執(zhí)行文件并生成一個ImageLoader實例對象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
......
// load any inserted libraries 加載插入的動態(tài)庫
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// link main executable 鏈接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
......
// link any inserted libraries 鏈接所有插入的動態(tài)庫
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// 注冊符號插入
image->registerInterposing(gLinkContext);
}
}
}
......
//弱符號綁定
sMainExecutable->weakBind(gLinkContext);
sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);
......
// run all initializers 執(zhí)行初始化方法
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
return result;
}
復制代碼
主要過程:
①第一步: 設(shè)置運行環(huán)境,為可執(zhí)行文件的加載做準備工作;
②第二步: 映射共享緩存到當前進程的邏輯內(nèi)存空間;
③第三步: 實例化主程序;
④第四步: 加載插入的動態(tài)庫;
⑤第五步: 鏈接主程序;
⑥第六步: 鏈接插入的動態(tài)庫;
⑦第七步: 執(zhí)行弱符號綁定(weakBind);
⑧第八步: 執(zhí)行初始化方法;
⑨第九步: 查找程序入口并返回main( ).
- 注1: sMainExecutable = instantiateFromLoadedImage(....) 與 loadInsertedDylib(...)
這一步 dyld 將我們可執(zhí)行文件以及插入的 lib 加載進內(nèi)存,生成對應(yīng)的image。sMainExecutable 對應(yīng)著我們的可執(zhí)行文件,里面包含了我們項目中所有新建的類。 InsertDylib 一些插入的庫,他們配置在全局的環(huán)境變量 sEnv 中,我們可以在項目中設(shè)置環(huán)境變量 DYLD_PRINT_ENV 為1來打印該 sEnv 的值。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
復制代碼
isCompatibleMachO 是檢查Mach-O的subtype是否是當前cpu可以支持; 內(nèi)核會映射到主可執(zhí)行文件中,我們需要為映射到主可執(zhí)行文件的文件,創(chuàng)建ImageLoader。
instantiateMainExecutable 就是實例化可執(zhí)行文件, 這個期間會解析LoadCommand, 這個之后會發(fā)送 dyld_image_state_mapped 通知; 在此方法中,讀取image,然后addImage() 到鏡像列表。
- 注2: link(sMainExecutable,...) 和 link(image,....)
對上面生成的 Image 進行鏈接。這個過程就是將加載進來的二進制變?yōu)榭捎脿顟B(tài)的過程。其主要做的事有對image進行 load(加載),rebase(基地址復位),bind(外部符號綁定),我們可以查看源碼:
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
......
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
......
this->recursiveRebaseWithAccounting(context);
......
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
}
復制代碼
注2.1: recursiveLoadLibraries(context, preflightOnly, loaderRPaths)
遞歸加載所有依賴庫進內(nèi)存。注2.2:recursiveRebase(context)
遞歸對自己以及依賴庫進行rebase操作。在以前,程序每次加載其在內(nèi)存中的堆棧基地址都是一樣的,這意味著你的方法,變量等地址每次都一樣的,這使得程序很不安全,后面就出現(xiàn) ASLR(Address space layout randomization,地址空間布局隨機化),程序每次啟動后地址都會隨機變化,這樣程序里所有的代碼地址都是錯的,需要重新對代碼地址進行計算修復才能正常訪問。注2.3:recursiveBindWithAccounting(context, forceLazysBound, neverUnload); 對庫中所有nolazy的符號進行
bind,一般的情況下多數(shù)符號都是lazybind的,他們在第一次使用的時候才進行bind。
2.4 initializeMainExecutable()
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
復制代碼
這一步主要是調(diào)用所有image的Initalizer方法進行初始化。先為所有插入并鏈接完成的動態(tài)庫執(zhí)行初始化操作
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
復制代碼
再為主程序可執(zhí)行文件執(zhí)行初始化操作
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
復制代碼
具體流程為: ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization
詳細代碼如下:
2.5 ImageLoader::runInitializers
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.imagesAndPaths[0] = { this, this->getPath() };
// 重點
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}
復制代碼
調(diào)用 processInitializers
2.6 ImageLoader::processInitializers
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
for (uintptr_t i=0; i < images.count; ++i) {
// 重點
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
復制代碼
在這里,對鏡像表中的所有鏡像執(zhí)行recursiveInitialization ,創(chuàng)建一個未初始化的向上依賴新表。如果依賴中未初始化完畢,則繼續(xù)執(zhí)行processInitializers,直到全部初始化完畢。
2.7 ImageLoader::recursiveInitialization
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order
if ( this->needsTermination() )
context.terminationRecorder(this);
// 重點 1: let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// 重點 2: initialize this image
bool hasInitializers = this->doInitialization(context);
// 重點 3: let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock();
}
復制代碼
在 recursiveInitialization 函數(shù)中,我們重點關(guān)注
- context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);,
- doInitialization(context)
- context.notifySingle(dyld_image_state_initialized, this, NULL);
- context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
通知objc我們要初始化這個鏡像,這里 通過 notifySingle 函數(shù)對sNotifyObjCInit 進行函數(shù)調(diào)用。
2.7.1 context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo)
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
......
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
......
}
復制代碼
獲取鏡像文件的真實地址 【*sNotifyObjCInit)(image->getRealPath(), image->machHeader() 】,而 sNotifyObjCInit是 通過 registerObjCNotifiers中傳遞的參數(shù)(_dyld_objc_notify_init)進行賦值的。
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
......
}
復制代碼
繼而找到,registerObjCNotifiers 的 拉起函數(shù) _dyld_objc_notify_register.
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
復制代碼
_dyld_objc_notify_register函數(shù)是供 objc runtime 使用的,當objc鏡像被映射,取消映射,和初始化時 被調(diào)用的注冊處理器。我們可以在 libobjc.A.dylib 庫里,_objc_init函數(shù)中找到其調(diào)用。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init(); // 環(huán)境變量
tls_init();
static_init(); // C++
runtime_init(); // runtime 初始化
exception_init(); // 異常初始化
cache_init(); // 緩存初始化
_imp_implementationWithBlock_init(); //
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
復制代碼
runtime初始化后,在_objc_init中注冊了幾個通知,從dyld這里接手了幾個活,其中包括負責初始化相應(yīng)依賴庫里的類結(jié)構(gòu),調(diào)用依賴庫里所有的load方法等。
就拿sMainExcuatable來說,它的initializer方法是最后調(diào)用的,當initializer方法被調(diào)用前dyld會通知runtime進行類結(jié)構(gòu)初始化,然后再通知調(diào)用load方法,這些目前還發(fā)生在main函數(shù)前,但由于lazy bind機制,依賴庫多數(shù)都是在使用時才進行bind,所以這些依賴庫的類結(jié)構(gòu)初始化都是發(fā)生在程序里第一次使用到該依賴庫時才進行的。
當所有的依賴庫的lnitializer都調(diào)用完后,dyld::main 函數(shù)會返回程序的main()函數(shù)地址,main函數(shù)被調(diào)用,從而代碼來到了我們熟悉的程序入口。
那么 _objc_init又是如何被調(diào)用的呢?

看調(diào)用堆棧,在 ImageLoader::recursiveInitialization 函數(shù)中,我們之前關(guān)注的重點2: doInitialization
- this->doInitialization(context);
// 重點 2: initialize this image
bool hasInitializers = this->doInitialization(context);
復制代碼
2.7.2 ImageLoaderMachO::doInitialization
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
復制代碼
在 doModInitFunctions之后 會 先執(zhí)行 libSystem_initializer,保證系統(tǒng)庫優(yōu)先初始化完畢,在這里初始化 libdispatch_init,進而在_os_object_init 中 調(diào)用 _objc_init。
由于 runtime 向 dyld 綁定了回調(diào),當 image 加載到內(nèi)存后,dyld 會通知 runtime 進行處理
runtime 接手后調(diào)用 map_images 做解析和處理,接下來load_images 中調(diào)用 call_load_methods 方法,遍歷所有加載進來的 Class,按繼承層級依次調(diào)用 Class 的 +load 方法和其 Category 的 +load 方法。
至此,可執(zhí)行文件和動態(tài)庫中所有的符號(Class,Protocol,Selector,IMP,…)都已經(jīng)按格式成功加載到內(nèi)存中,被 runtime 所管理,在這之后,runtime 的那些方法(動態(tài)添加 Class、swizzle 等等才能生效)
總結(jié):

APP是由內(nèi)核引導啟動的,kernel內(nèi)核做好所有準備工作后會得到線程入口及main入口,但是線程不會馬上進入main入口,因為還要加載動態(tài)鏈接器(dyld),dyld會將入口點保存下來,等dyld加載完所有動態(tài)鏈接庫等工作之后,再開始執(zhí)行main函數(shù)。
系統(tǒng)kernel做好啟動程序的初始準備后,交給dyld負責。
dyld接手后,系統(tǒng)先讀取 App 的可執(zhí)行文件(Mach-O文件),從里面獲取dyld的路徑,然后加載dyld,dyld去初始化運行環(huán)境,開啟緩存策略,配合 ImageLoader 將二進制文件按格式加載到內(nèi)存,加載程序相關(guān)依賴庫(其中也包含我們的可執(zhí)行文件),并對這些庫進行鏈接,最后調(diào)用每個依賴庫的初始化方法,在這一步,runtime被初始化。當所有依賴庫初始化后,輪到最后一位(程序可執(zhí)行文件)進行初始化,在這時runtime會對項目中所有類進行類結(jié)構(gòu)初始化,然后調(diào)用所有的load方法。最后dyld返回main()函數(shù)地址,main()函數(shù)被調(diào)用。
這個過程遠比寫出來的要復雜,這里只提到了 runtime 這個分支,還有像 GCD、XPC 等重頭的系統(tǒng)庫初始化分支沒有提及(當然,有緩存機制在,它們也不會玩命初始化),總結(jié)起來就是 main 函數(shù)執(zhí)行之前,系統(tǒng)做了茫茫多的加載和初始化工作,最終引入那個熟悉的main函數(shù)。
