iOS App 加載流程知識

文集: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 XiOS 來說 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

Executableapp 的二進制主文件,我們可以在 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:代碼段,包含頭文件、代碼和只讀常量。只讀不可修改
image.png
  • __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)用的安全:ASLRCode 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位進程則至少是4GBNULL指針引用和指針截斷誤差都是會被它捕獲,這個范圍也叫做 PAGEZERO。

1.8 dyld

內(nèi)核完成映射進程的工作后,會將名字為 dyldMach-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)用程序找到這些符號的實際地址。主要包括兩部分:rebasingbinding。

  • 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

image.png

iOS 13之前,所有的第三方App都是通過dyld 2來啟動 App 的,主要過程如下:

  • 解析 Mach-OHeaderLoad Commands,找到其依賴的庫,并遞歸找到所有依賴的庫
  • 加載Mach-O文件
  • 進行符號查找
  • 綁定和變基
  • 運行初始化程序

dyld 3被分為了三個組件:

  • 一個進程外的Mach-O 解析器 預先處理了所有可能影響啟動速度的search path、@rpaths和環(huán)境變量 然后分析Mach-OHeader和依賴,并完成了所有符號查找的工作 最后將這些結(jié)果創(chuàng)建成一個啟動閉包 這是一個普通的daemon進程,可以使用通常的測試架構(gòu)

  • 一個進程內(nèi)的引擎,用來運行啟動閉包 這部分在進程中處理 驗證啟動閉包的安全性,然后映射到dylib之中,再跳轉(zhuǎn)到main函數(shù) 不需要解析Mach-OHeader和依賴,也不需要符號查找。

  • 一個啟動閉包緩存服務(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-Oheader 得到偏移量 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)用所有imageInitalizer方法進行初始化。先為所有插入并鏈接完成的動態(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ù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容