(六) Mach-O 文件的動(dòng)態(tài)鏈接、庫(kù)、Dyld(含dlopen)

# 動(dòng)態(tài)鏈接
# 庫(kù):靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)
  ## 靜態(tài)庫(kù)
  ## 動(dòng)態(tài)庫(kù)
  ## 非常重要的LibSystem庫(kù)
  ## 補(bǔ)充兩個(gè)概念:程序模塊、映像image
  ## .a/.dylib與.framework的區(qū)別
# Mach-O 文件的動(dòng)態(tài)鏈接 —— dyld引入
# dyld工作流程詳解
  ## _dyld_start
  ## dyldbootstrap::start()
  ## dyld::_main()
# 小結(jié)
# 加載動(dòng)態(tài)庫(kù)的另一種方式:顯式運(yùn)行時(shí)鏈接dlopen、dlsym
# 參考鏈接

# 動(dòng)態(tài)鏈接

動(dòng)態(tài)鏈接的基本思想是把程序按照模塊拆分成各個(gè)相對(duì)獨(dú)立部分,在程序運(yùn)行時(shí)才將它們鏈接在一起形成一個(gè)完整的程序,而不是像靜態(tài)鏈接一樣把所有的程序模塊都鏈接成一個(gè)個(gè)單獨(dú)的可執(zhí)行文件。

動(dòng)態(tài)鏈接涉及運(yùn)行時(shí)的鏈接及多個(gè)文件的裝載,必需要有操作系統(tǒng)的支持,因?yàn)閯?dòng)態(tài)鏈接的情況下,進(jìn)程的虛擬地址空間的分布會(huì)比靜態(tài)鏈接情況下更為復(fù)雜,還有一些存儲(chǔ)管理、內(nèi)存共享、進(jìn)程線程等機(jī)制在動(dòng)態(tài)鏈接下也會(huì)有一些微妙的變化。目前主流的操作系統(tǒng)幾乎都支持動(dòng)態(tài)鏈接這種方式。

# 庫(kù):靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)

庫(kù)(Library),是我們?cè)陂_(kāi)發(fā)中的重要角色,庫(kù)的作用在于代碼共享、模塊分割以及提升良好的工程管理實(shí)踐。說(shuō)白了就是一段編譯好的二進(jìn)制代碼,加上頭文件就可以供別人使用。

為什么要用庫(kù)?一種情況是某些代碼需要給別人使用,但是我們不希望別人看到源碼,就需要以庫(kù)的形式進(jìn)行封裝,只暴露出頭文件(靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)的共同點(diǎn)就是不會(huì)暴露內(nèi)部具體的代碼信息)。另外一種情況是,對(duì)于某些不會(huì)進(jìn)行大的改動(dòng)的代碼,我們想減少編譯的時(shí)間,就可以把它打包成庫(kù),因?yàn)閹?kù)是已經(jīng)編譯好的二進(jìn)制了,編譯的時(shí)候只需要 Link 一下,不會(huì)浪費(fèi)編譯時(shí)間。

根據(jù)庫(kù)在使用的時(shí)候 Link 時(shí)機(jī)或者說(shuō)方式(靜態(tài)鏈接、動(dòng)態(tài)鏈接),庫(kù)分為靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)。

## 靜態(tài)庫(kù)

靜態(tài)庫(kù)即靜態(tài)鏈接庫(kù)(Windows 下的 .lib,linux 下的.a,Mac 下的 .a .framework)。之所以叫做靜態(tài),是因?yàn)殪o態(tài)庫(kù)在鏈接時(shí)會(huì)被完整地拷貝一份到可執(zhí)行文件中(會(huì)使最終的可執(zhí)行文件體積增大)。被多個(gè)程序使用就會(huì)有多份冗余拷貝。如果更新靜態(tài)庫(kù),需要重新編譯一次可執(zhí)行文件,重新鏈接新的靜態(tài)庫(kù)。

## 動(dòng)態(tài)庫(kù)

動(dòng)態(tài)庫(kù)即動(dòng)態(tài)鏈接庫(kù)。與靜態(tài)庫(kù)相反,動(dòng)態(tài)庫(kù)在編譯時(shí)并不會(huì)被拷貝到可執(zhí)行文件中,可執(zhí)行文件中只會(huì)存儲(chǔ)指向動(dòng)態(tài)庫(kù)的引用(使用了動(dòng)態(tài)庫(kù)的符號(hào)、及對(duì)應(yīng)庫(kù)的路徑等)。等到程序運(yùn)行時(shí),動(dòng)態(tài)庫(kù)才會(huì)被真正加載進(jìn)來(lái),此時(shí),先根據(jù)記錄的庫(kù)路徑找到對(duì)應(yīng)的庫(kù),再通過(guò)記錄的名字符號(hào)找到綁定的地址。

動(dòng)態(tài)庫(kù)的優(yōu)點(diǎn)是:

  • 減少可執(zhí)行文件體積:相比靜態(tài)鏈接,動(dòng)態(tài)鏈接在編譯時(shí)不需要打進(jìn)去(不需要拷貝到每個(gè)可執(zhí)行文件中),所以可執(zhí)行文件的體積要小很多。
  • 代碼共用:很多程序都動(dòng)態(tài)鏈接了這些 lib,但它們?cè)趦?nèi)存和磁盤(pán)中中只有一份(因?yàn)檫@個(gè)原因,動(dòng)態(tài)庫(kù)也被稱作共享庫(kù))。
  • 易于維護(hù):使用動(dòng)態(tài)庫(kù),可以不重新編譯連接可執(zhí)行程序的前提下,更新動(dòng)態(tài)庫(kù)文件達(dá)到更新應(yīng)用程序的目的。

常見(jiàn)的可執(zhí)行文件的形式:

  • Linux系統(tǒng)中,ELF動(dòng)態(tài)鏈接文件被稱為動(dòng)態(tài)共享對(duì)象(DSO,Dynamic SharedObjects),簡(jiǎn)稱共享對(duì)象,一般都是以 .so 為擴(kuò)展名的一些文件;
  • Windows系統(tǒng)中,動(dòng)態(tài)鏈接文件被稱為動(dòng)態(tài)鏈接庫(kù)(Dynamical Linking Library),通常就是我們平時(shí)很常見(jiàn)的以 .dll 為擴(kuò)展名的文件;
  • OS X 和其他 UN*X 不同,它的庫(kù)不是“共享對(duì)象(.so)”,因?yàn)?OS X 和 ELF 不兼容,而且這個(gè)概念在 Mach-O 中不存在。OS 中的動(dòng)態(tài)鏈接文件一般稱為動(dòng)態(tài)庫(kù)文件,帶有 .dylib.framework及鏈接符號(hào).tbd。可以在 /usr/lib 目錄下找到(這一點(diǎn)和其他所有的 UN*X 一樣,不過(guò)在OS X 和 iOS 中沒(méi)有/lib目錄)
  • OS X 與其他 UN*X 另一點(diǎn)不同是:沒(méi)有libc。開(kāi)發(fā)者可能熟悉其他 UN*X 上的C運(yùn)行時(shí)庫(kù)(或Windows上的MSVCRT) 。但是在 OS X 上對(duì)應(yīng)的庫(kù)/usr/lib/libc.dylib只不過(guò)是指向libSystem.B.dylib的符號(hào)鏈接。
  • 以C語(yǔ)言運(yùn)行庫(kù)為例,補(bǔ)充一下運(yùn)行庫(kù)的概念:任何一個(gè)C程序,它的背后都有一套龐大的代碼來(lái)進(jìn)行支撐,以使得該程序能夠正常運(yùn)行。這套代碼至少包括入口函數(shù),及其所依賴的函數(shù)所構(gòu)成的函數(shù)集合。當(dāng)然,它還理應(yīng)包括各種標(biāo)準(zhǔn)庫(kù)函數(shù)的實(shí)現(xiàn)。這樣的一個(gè)代碼集合稱之為運(yùn)行時(shí)庫(kù)(Runtime Library)。而C語(yǔ)言的運(yùn)行庫(kù),即被稱為C運(yùn)行庫(kù)(CRT)。運(yùn)行庫(kù)顧名思義是讓程序能正常運(yùn)行的一個(gè)庫(kù)。

## 兩個(gè)非常重要的庫(kù):LibSystem、libobjc

libSystem 提供了 LibC(運(yùn)行庫(kù)) 的功能,還包含了在其他 UN*X 上原本由其他一些庫(kù)提供的功能,列幾個(gè)熟知的:

  • GCD libdispatch
  • C語(yǔ)言庫(kù) libsystem_c
  • Block libsystem_blocks
  • 加密庫(kù)(比如常見(jiàn)的md5函數(shù)) libcommonCrypto

還有些庫(kù)(如數(shù)學(xué)庫(kù) libm、線程庫(kù) libpthread)雖然在/usr/lib中看到雖然有這些庫(kù)的文件,但都是libSystem.B.dylib的替身/快捷方式,即都是指向libSystem的符號(hào)鏈接。

libSystem 庫(kù)是系統(tǒng)上所有二進(jìn)制代碼的絕對(duì)先決條件,即所有的二進(jìn)制文件都依賴這個(gè)庫(kù),不論是C、C++還是Objective-C的程序。這是因?yàn)檫@個(gè)庫(kù)是對(duì)底層系統(tǒng)調(diào)用和內(nèi)核服務(wù)的接口,如果沒(méi)有這些接口就什么事也干不了。這個(gè)庫(kù)還是/usr/ib/system目錄下一些庫(kù)的保護(hù)傘庫(kù)(通過(guò)LC_REEXPORT_LIB加載命令重新導(dǎo)出了符號(hào)) 。

總結(jié)來(lái)說(shuō):libSystem在運(yùn)行庫(kù)的基礎(chǔ)上,增加了一些對(duì)底層系統(tǒng)調(diào)用和內(nèi)核服務(wù)的抽象接口。所以在下面的流程中,會(huì)發(fā)現(xiàn)libSystem是先于其他動(dòng)態(tài)庫(kù)初始化的。

libobjc與libsystem一樣,都是默認(rèn)添加的lib,包含iOS開(kāi)發(fā)天天接觸的objc runtime.

## 補(bǔ)充兩個(gè)概念

  • 程序模塊:從本質(zhì)上講,普通可執(zhí)行程序和動(dòng)態(tài)庫(kù)中都包含指令和數(shù)據(jù),這一點(diǎn)沒(méi)有區(qū)別。在使用動(dòng)態(tài)庫(kù)的情況下,程序本身被分為了程序主要模塊(Program1)和動(dòng)態(tài)鏈接文件(Lib.so Lib.dylib Lib.dll),但實(shí)際上它們都可以看作是整個(gè)程序的一個(gè)模塊,所以當(dāng)我們提到程序模塊時(shí)可以指程序主模塊也可以指動(dòng)態(tài)鏈接庫(kù)。
  • 映像(image) ,通常也是指這兩者。可執(zhí)行文件/動(dòng)態(tài)鏈接文件,在裝載時(shí)被直接映射到進(jìn)程的虛擬地址空間中運(yùn)行,它是進(jìn)程的虛擬空間的映像,所以很多時(shí)候,也被叫做映像/鏡像文件(Image File)。

## .a/.dylib與.framework的區(qū)別

前者是純二進(jìn)制文件,文件不能直接使用,需要有.h文件的配合(我們?cè)谑褂孟到y(tǒng)的.dylib動(dòng)態(tài)庫(kù)時(shí),經(jīng)常發(fā)現(xiàn)沒(méi)有頭文件,其實(shí)這些庫(kù)的頭文件都位于一個(gè)已知位置,如usr/include(新系統(tǒng)中這個(gè)文件夾由SDK附帶了,見(jiàn) [/usr/include missing on macOS Catalina (with Xcode 11)] ),庫(kù)文件位于usr/lib,使得這些庫(kù)全局可用),后者除了二進(jìn)制文件、頭文件還有資源文件,代碼可以直接導(dǎo)入使用(.a + .h + sourceFile = .framework)。

Framework 是蘋(píng)果公司的 Cocoa/Cocoa Touch 程序中使用的一種資源打包方式,可以將代碼文件、頭文件、資源文件(nib/xib、圖片、國(guó)際化文本)、說(shuō)明文檔等集中在一起,方便開(kāi)發(fā)者使用。Framework 其實(shí)是資源打包的方式,和靜態(tài)庫(kù)動(dòng)態(tài)庫(kù)的本質(zhì)是沒(méi)有什么關(guān)系(所以framework文件可以是靜態(tài)庫(kù)也可以是動(dòng)態(tài)庫(kù),iOS 中用到的所有系統(tǒng) framework 都是動(dòng)態(tài)鏈接的)

在其它大部分平臺(tái)上,動(dòng)態(tài)庫(kù)都可以用于不同應(yīng)用間共享, 共享可執(zhí)行文件,這就大大節(jié)省了內(nèi)存。但是iOS平臺(tái)在 iOS 8 之前,蘋(píng)果不允許第三方框架使用動(dòng)態(tài)方式加載,開(kāi)發(fā)者可以使用的動(dòng)態(tài) Framework 只有蘋(píng)果系統(tǒng)提供的 UIKit.Framework,F(xiàn)oundation.Framework 等。開(kāi)發(fā)者要進(jìn)行模塊化,只能打包成靜態(tài)庫(kù)文件:.a + 頭文件.framework(這時(shí)候的 Framework 只支持打包成靜態(tài)庫(kù)的 Framework),前種方式打包不夠方便,使用時(shí)也比較麻煩,沒(méi)有后者的便捷性。

iOS 8/Xcode 6 推出之后,允許開(kāi)發(fā)者有條件地創(chuàng)建和使用動(dòng)態(tài)庫(kù),支持了動(dòng)態(tài) Framework。開(kāi)發(fā)者打包的動(dòng)態(tài) Framework 和系統(tǒng)的 UIKit.Framework 還是有很大區(qū)別。后者不需要拷貝到目標(biāo)程序中,是一個(gè)鏈接。而前者在打包和提交 app 時(shí)會(huì)被放到 app main bundle 的根目錄中,運(yùn)行在沙盒里,而不是系統(tǒng)中。也就是說(shuō),不同的 app 就算使用了同樣的 framework,但還是會(huì)有多份的框架被分別簽名,打包和加載,因此蘋(píng)果又把這種 Framework 稱為 Embedded Framework(可植入性 Framework)。

不過(guò) iOS8 上開(kāi)放了 App Extension 功能,可以為一個(gè)應(yīng)用創(chuàng)建插件,這樣主app和插件之間共享動(dòng)態(tài)庫(kù)還是可行的。

數(shù)量上,蘋(píng)果公司建議最多使用6個(gè)非系統(tǒng)動(dòng)態(tài)庫(kù)。

然后就是,在上傳App Store打包的時(shí)候,蘋(píng)果會(huì)對(duì)我們的代碼進(jìn)行一次 Code Singing,包括 app 可執(zhí)行文件和所有Embedded 的動(dòng)態(tài)庫(kù),所以如果是動(dòng)態(tài)從服務(wù)器更新的動(dòng)態(tài)庫(kù),是簽名不了的,sandbox驗(yàn)證動(dòng)態(tài)庫(kù)的簽名非法時(shí),就會(huì)造成crash。因此應(yīng)用插件化、軟件版本實(shí)時(shí)模塊升級(jí)等功能在iOS上無(wú)法實(shí)現(xiàn)。不過(guò)在 in house(企業(yè)發(fā)布) 包和develop 包中可以使用。

# Mach-O 文件的動(dòng)態(tài)鏈接 —— dyld引入

Mach-O 文件的裝載完成,即內(nèi)核加載器做完相關(guān)的工作后,對(duì)于需要?jiǎng)討B(tài)鏈接(使用了動(dòng)態(tài)庫(kù))的可執(zhí)行文件(大部分可執(zhí)行文件都是動(dòng)態(tài)鏈接的)來(lái)說(shuō),控制權(quán)會(huì)轉(zhuǎn)交給鏈接器,鏈接器進(jìn)而接著處理文件頭中的其他加載命令。真正的庫(kù)加載和符號(hào)解析的工作都是通過(guò)LC_LOAD_DYLINKER加載命令指定的動(dòng)態(tài)鏈接器在用戶態(tài)完成的。通常情況下,使用的是 /usr/lib/dyld 作為動(dòng)態(tài)鏈接器,不過(guò)這條加載命令可以指定任何程序作為參數(shù)。

鏈接器接管剛創(chuàng)建的進(jìn)程的控制權(quán),因?yàn)閮?nèi)核將進(jìn)程的入口點(diǎn)設(shè)置為鏈接器的入口點(diǎn)。

dyld是一個(gè)用戶態(tài)的進(jìn)程。dyld不屬于內(nèi)核的一部分,而是作為一個(gè)單獨(dú)的開(kāi)源項(xiàng)目由蘋(píng)果進(jìn)行維護(hù)的(當(dāng)然也屬于Darwin的一部分) ,點(diǎn)擊查看項(xiàng)目網(wǎng)址。從內(nèi)核的角度看,dyld是一個(gè)可插入的組件,可以替換為第三方的鏈接器。dyld對(duì)應(yīng)的二進(jìn)制文件有兩個(gè),分別是/usr/lib/dyld/urs/lib/system/libdyld.dylib,前者通用二進(jìn)制格式(FAT),filetype為MH_DYLINKER,后者是普通的動(dòng)態(tài)鏈接庫(kù)格式(Mach-O)。

從調(diào)用堆棧上看dyld、libdyld.dylib的作用

前者dyld一段可執(zhí)行的程序,內(nèi)核將其映射至進(jìn)程地址空間,將控制權(quán)交給它進(jìn)行執(zhí)行,遞歸加載所需的動(dòng)態(tài)庫(kù),其中也會(huì)將動(dòng)態(tài)鏈接器的另一種形式的libdyld.dylib加載,因?yàn)閯?dòng)態(tài)鏈接器dyld其不但在應(yīng)用的裝載階段起作用,在主程序運(yùn)行的時(shí)候,其充當(dāng)一個(gè)庫(kù)的角色,還提供了dlopendlsym等api,可以讓主程序顯式運(yùn)行時(shí)鏈接(見(jiàn)下文)。(關(guān)于這一點(diǎn),沒(méi)有找到明確的文檔說(shuō)明。如果有人有正確的理解,請(qǐng)一定要評(píng)論區(qū)告訴我一下,感激不盡)

Linux中,動(dòng)態(tài)鏈接庫(kù)的存在形式稍有不同,Linux動(dòng)態(tài)鏈接器本身是一個(gè)共享對(duì)象(動(dòng)態(tài)庫(kù)),它的路徑是/lib/ld-linux.so.2,這實(shí)際上是個(gè)軟鏈接,它指向/lib/ld-x.y.z.so, 這個(gè)才是真正的動(dòng)態(tài)連接器文件。共享對(duì)象其實(shí)也是ELF文件,它也有跟可執(zhí)行文件一樣的ELF文件頭(包括e_entry、段表等)。動(dòng)態(tài)鏈接器是個(gè)非常特殊的共享對(duì)象,它不僅是個(gè)共享對(duì)象,還是個(gè)可執(zhí)行的程序,可以直接在命令行下面運(yùn)行。因?yàn)閘d.so是共享對(duì)象,又是動(dòng)態(tài)鏈接器,所以本來(lái)應(yīng)由動(dòng)態(tài)鏈接器進(jìn)行的共享對(duì)象的重定位,就要靠自己來(lái),又稱“自舉”。自舉完成后ld.so以一個(gè)共享對(duì)象的角色,來(lái)實(shí)現(xiàn)動(dòng)態(tài)鏈接庫(kù)的功能。

我們需要了解一下LC_LOAD_DYLIB這個(gè)加載命令,這個(gè)命令會(huì)告訴鏈接器在哪里可以找到這些符號(hào),即動(dòng)態(tài)庫(kù)的相關(guān)信息(ID、時(shí)間戳、版本號(hào)、兼容版本號(hào)等)。鏈接器要加載每一個(gè)指定的庫(kù),并且搜尋匹配的符號(hào)。每個(gè)被鏈接的庫(kù)(Mach-O格式)都有一個(gè)符號(hào)表,符號(hào)表將符號(hào)名稱和地址關(guān)聯(lián)起來(lái)。符號(hào)表在Mach-O目標(biāo)文件中的地址可以通過(guò)LC_SYMTAB加載命令指定的 symoff 找到。對(duì)應(yīng)的符號(hào)名稱在 stroff, 總共有 nsyms 條符號(hào)信息。

下面是LC_SYMTAB的load_command:

struct dylib {
    union lc_str  name;             / library's path name /
    uint32_t timestamp;             / library's build time stamp /
    uint32_t current_version;       / library's current version number /
    uint32_t compatibility_version; / library's compatibility vers number /
};

struct dylib_command {
    uint32_t    cmd;        /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB, LC_REEXPORT_DYLIB */
    uint32_t    cmdsize;    /* includes pathname string */
    struct dylib    dylib;      /* the library identification */
};

在 <mach-o/dyld.h> 動(dòng)態(tài)庫(kù)頭文件中,也為我們提供了查詢所有動(dòng)態(tài)庫(kù) image 的方法(也可以使用otool -L 文件路徑命令來(lái)查看,但看著沒(méi)代碼全):

#include <mach-o/dyld.h>
#include <stdio.h>

void listImages(){
    uint32_t i;
    uint32_t ic = _dyld_image_count();

    printf("Got %d images\n", ic);
    for (i = 0; i < ic; ++ i) {
        printf("%d: %p\t%s\t(slide: %p)\n",
               i,
               _dyld_get_image_header(i),
               _dyld_get_image_name(i),
               _dyld_get_image_vmaddr_slide(i));
    }
}

listImages();  //調(diào)用方法

log: 
  ...
  45: 0x1ab331000   /usr/lib/libobjc.A.dylib    (slide: 0x2b1b8000)
  46: 0x1e1767000   /usr/lib/libSystem.B.dylib  (slide: 0x2b1b8000)
  ...
  70: 0x107220000   /usr/lib/system/introspection/libdispatch.dylib (slide: 0x107220000)
  71: 0x1ab412000   /usr/lib/system/libdyld.dylib   (slide: 0x2b1b8000)
  ...

# dyld工作流程詳解

通過(guò)源碼來(lái)看一下dyld的工作流程,只是部分片段,詳細(xì)的可以下載源碼。

## __dyld_start

下面的匯編代碼很簡(jiǎn)單,如果不清楚,可以看一下這篇匯編入門(mén)文章iOS需要了解的ARM64匯編

#if __arm64__
    .text
    .align 2
    .globl __dyld_start
__dyld_start:
; 操作fp棧幀寄存器,sp棧指針寄存器,配置函數(shù)棧幀
    mov     x28, sp
    and     sp, x28, #~15       // force 16-byte alignment of stack
    mov x0, #0
    mov x1, #0
    stp x1, x0, [sp, #-16]! // make aligned terminating frame
    mov fp, sp          // set up fp to point to terminating frame
    sub sp, sp, #16             // make room for local variables
; L(long 64位) P(point),在前面的匯編一文中,我們已經(jīng)知道:r0 - r30 是31個(gè)通用整形寄存器。每個(gè)寄存器可以存取一個(gè)64位大小的數(shù)。 
; 當(dāng)使用 x0 - x30訪問(wèn)時(shí),它就是一個(gè)64位的數(shù)。
; 當(dāng)使用 w0 - w30訪問(wèn)時(shí),訪問(wèn)的是這些寄存器的低32位
#if __LP64__       
    ldr     x0, [x28]               // get app's mh into x0
    ldr     x1, [x28, #8]           // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
    add     x2, x28, #16            // get argv into x2
#else
    ldr     w0, [x28]               // get app's mh into x0
    ldr     w1, [x28, #4]           // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
    add     w2, w28, #8             // get argv into x2
#endif
    adrp    x3,___dso_handle@page
    add     x3,x3,___dso_handle@pageoff // get dyld's mh in to x4
    mov x4,sp                   // x5 has &startGlue
; 從上面的匯編代碼可以看到,主要是在設(shè)置dyldbootstrap::start函數(shù)調(diào)用棧的配置,在前面的匯編一文中,我們已經(jīng)知道函數(shù)的參數(shù),主要通過(guò)x0-x7幾個(gè)寄存器來(lái)傳遞
; 可以看到函數(shù)需要的幾個(gè)參數(shù)app_mh,argc,argv,dyld_mh,&startGlue分別被放置到了x0 x1 x2 x4 x5寄存器上
    ; call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
    bl  __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
    mov x16,x0                  // save entry point address in x16

## dyldbootstrap::start()

//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//  主要做的是dyld的引導(dǎo)工作,一般這個(gè)工作通常由 dyld 和 crt(C運(yùn)行時(shí)庫(kù) C Run-Time Libray )來(lái)完成。但dyld自身加載的時(shí)候,只能由自己來(lái)做。
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);
    // 如果有slide,那么需要重定位,必須在使用任何全局變量之前,進(jìn)行該操作
    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;
    // 為stack canary設(shè)置一個(gè)隨機(jī)值
    // stack canary:棧的警惕標(biāo)志(stack canary),得名于煤礦里的金絲雀,用于探測(cè)該災(zāi)難的發(fā)生。具體辦法是在棧的返回地址的存儲(chǔ)位置之前放置一個(gè)整形值,該值在裝入程序時(shí)隨機(jī)確定。棧緩沖區(qū)攻擊時(shí)從低地址向高地址覆蓋棧空間,因此會(huì)在覆蓋返回地址之前就覆蓋了警惕標(biāo)志。返回返回前會(huì)檢查該警惕標(biāo)志是否被篡改。
    __guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
    // 執(zhí)行 dyld 中所有的C++初始化函數(shù)。run all C++ initializers inside dyld
    runDyldInitializers(argc, argv, envp, apple);
#endif
    // 完成所有引導(dǎo)工作,調(diào)用dyld::main(). 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);
}

## dyld::_main()

dyld也是Mach-O文件格式的,文件頭中的 filetype 字段為MH_DYLINKER,區(qū)別與可執(zhí)行文件的 MH_EXECUTE,所以dyld也是有main()函數(shù)的(默認(rèn)名稱是mian(),也可以自己修改入口地址的)。

因?yàn)檫@個(gè)函數(shù)太長(zhǎng),寫(xiě)在一起不好閱讀,所以按照流程功能點(diǎn),自上而下分為一個(gè)個(gè)代碼片段。關(guān)鍵的函數(shù)會(huì)在代碼中注釋說(shuō)明

### 方法名及說(shuō)明

// dyld的入口指針,內(nèi)核加載dyld,跳轉(zhuǎn)到__dyld_start函數(shù):進(jìn)行了一些寄存器設(shè)置,然后就調(diào)用了該函數(shù)。Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which sets up some registers and call this function.
// 返回主程序模塊的mian()函數(shù)地址,__dyld_start中會(huì)跳到該地址。Returns address of main() in target program which __dyld_start jumps to
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è)置運(yùn)行環(huán)境,處理環(huán)境變量

    #pragma mark -- 第一步,設(shè)置運(yùn)行環(huán)境
    // Grab the cdHash of the main executable from the environment
    uint8_t mainExecutableCDHashBuffer[20];
    const uint8_t* mainExecutableCDHash = nullptr;
    if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
        // 獲取主程序的hash
        mainExecutableCDHash = mainExecutableCDHashBuffer;

#if !TARGET_OS_SIMULATOR
    // Trace dyld's load
    notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
    // Trace the main executable's load
    notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif

    uintptr_t result = 0;
    // 獲取主程序的macho_header結(jié)構(gòu)
    sMainExecutableMachHeader = mainExecutableMH;
    // 獲取主程序的slide值
    sMainExecutableSlide = mainExecutableSlide;
    ......
    CRSetCrashLogMessage("dyld: launch started");
    // 傳入Mach-O頭部以及一些參數(shù)設(shè)置上下文信息
    setContext(mainExecutableMH, argc, argv, envp, apple);

    // Pickup the pointer to the exec path.
    // 獲取主程序路徑
    sExecPath = _simple_getenv(apple, "executable_path");

    // <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
    if (!sExecPath) sExecPath = apple[0];
    ......
    if ( sExecPath[0] != '/' ) {
        // have relative path, use cwd to make absolute
        char cwdbuff[MAXPATHLEN];
        if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
            // maybe use static buffer to avoid calling malloc so early...
            char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
            strcpy(s, cwdbuff);
            strcat(s, "/");
            strcat(s, sExecPath);
            sExecPath = s;
        }
    }

    // Remember short name of process for later logging
    // 獲取進(jìn)程名稱
    sExecShortName = ::strrchr(sExecPath, '/');
    if ( sExecShortName != NULL )
        ++sExecShortName;
    else
        sExecShortName = sExecPath;

    // 配置進(jìn)程受限模式
    configureProcessRestrictions(mainExecutableMH, envp);
    ......
    // 檢測(cè)環(huán)境變量
    checkEnvironmentVariables(envp);
    // 在DYLD_FALLBACK為空時(shí)設(shè)置默認(rèn)值
    defaultUninitializedFallbackPaths(envp);
    ......
    // 如果設(shè)置了DYLD_PRINT_OPTS則調(diào)用printOptions()打印參數(shù)
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    // 如果設(shè)置了DYLD_PRINT_ENV則調(diào)用printEnvironmentVariables()打印環(huán)境變量
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    ......
    // 獲取當(dāng)前程序架構(gòu)
    getHostInfo(mainExecutableMH, mainExecutableSlide);

### 第二步 加載共享緩存

在iOS系統(tǒng)中,UIKit,F(xiàn)oundation等基礎(chǔ)庫(kù)是每個(gè)程序都依賴的,需要通過(guò)dyld(位于/usr/lib/dyld)一個(gè)一個(gè)加載到內(nèi)存,然而如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次,勢(shì)必造成運(yùn)行緩慢,為了優(yōu)化啟動(dòng)速度和提高程序性能,共享緩存機(jī)制就應(yīng)運(yùn)而生。iOS的dyld采用了一個(gè)共享庫(kù)預(yù)鏈接緩存,蘋(píng)果從iOS 3.0開(kāi)始將所有的基礎(chǔ)庫(kù)都移到了這個(gè)緩存中,合并成一個(gè)大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下(OS X中是在/private/var/db/dyld目錄),按不同的架構(gòu)保存分別保存著,如dyld_shared_cache_armv7。而且在OS X中還有一個(gè)輔助的.map文件,而iOS中沒(méi)有。

如果在iOS上搜索大部分常見(jiàn)的庫(kù),比如所有二進(jìn)制文件都依賴的libSystem,是搜索不到的,這個(gè)庫(kù)的文件不在文件系統(tǒng)中,而是被緩存文件包含。關(guān)于如何從共享緩存中提取我們想看的庫(kù),可以參考鏈接dyld詳解第一部分

    #pragma mark -- 第二步,加載共享緩存 // load shared cache
    // 檢查共享緩存是否開(kāi)啟,iOS必須開(kāi)啟
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
      /*
       * mapSharedCache加載共享緩存庫(kù),其中調(diào)用loadDyldCache函數(shù),展開(kāi)loadDyldCache,有這么幾種情況:
         * 僅加載到當(dāng)前進(jìn)程mapCachePrivate(模擬器僅支持加載到當(dāng)前進(jìn)程)
         * 共享緩存是第一次被加載,就去做加載操作mapCacheSystemWide
         * 共享緩存不是第一次被加載,那么就不做任何處理
       */
      mapSharedCache();
    }
    ......

    try {
        // add dyld itself to UUID list
        addDyldImageToUUIDList();

### 第三步 實(shí)例化主程序

ImageLoader:前面已經(jīng)提到image(映像文件)常見(jiàn)的有可執(zhí)行文件、動(dòng)態(tài)鏈接庫(kù)。ImageLoader 作用是將這些文件加載進(jìn)內(nèi)存,且每一個(gè)文件對(duì)應(yīng)一個(gè)ImageLoader實(shí)例來(lái)負(fù)責(zé)加載。

從下面可以看到大概的順序:先將動(dòng)態(tài)鏈接的 image 遞歸加載,再依次進(jìn)行可執(zhí)行文件的鏈接。

        #pragma mark -- 第三步 實(shí)例化主程序,會(huì)實(shí)例化一個(gè)主程序ImageLoader
        // instantiate ImageLoader for main executable
        /*
         * 展開(kāi) instantiateFromLoadedImage 函數(shù), 可以看到主要分三步:
         *  isCompatibleMachO():檢查mach-o的subtype是否是當(dāng)前cpu可以支持;
         *  instantiateMainExecutable(): 就是實(shí)例化可執(zhí)行文件,這個(gè)期間會(huì)解析LoadCommand,這個(gè)之后會(huì)發(fā)送 dyld_image_state_mapped 通知;
         *  addImage(): 添加到 allImages中
         */
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

        // Now that shared cache is loaded, setup an versioned dylib overrides
    #if SUPPORT_VERSIONED_PATHS
        checkVersionedPaths();
    #endif

        // dyld_all_image_infos image list does not contain dyld
        // add it as dyldPath field in dyld_all_image_infos
        // for simulator, dyld_sim is in image list, need host dyld added
#if TARGET_OS_SIMULATOR
        // get path of host dyld from table of syscall vectors in host dyld
        void* addressInDyld = gSyscallHelpers;
#else
        // get path of dyld itself
        void*  addressInDyld = (void*)&__dso_handle;
#endif
        char dyldPathBuffer[MAXPATHLEN+1];
        int len = proc_regionfilename(getpid(), (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
        if ( len > 0 ) {
            dyldPathBuffer[len] = '\0'; // proc_regionfilename() does not zero terminate returned string
            if ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath) != 0 )
                gProcessInfo->dyldPath = strdup(dyldPathBuffer);
        }

### 第四步 加載插入的動(dòng)態(tài)庫(kù)

通過(guò)遍歷 DYLD_INSERT_LIBRARIES 環(huán)境變量,調(diào)用 loadInsertedDylib 加載。

在三方App的Mach-O文件中通過(guò)修改DYLD_INSERT_LIBRARIES的值來(lái)加入我們自己的動(dòng)態(tài)庫(kù),從而注入代碼,hook別人的App。

        #pragma mark -- 第四步 加載插入的動(dòng)態(tài)庫(kù)
        // load any inserted libraries
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
        // record count of inserted libraries so that a flat search will look at 
        // inserted libraries, then main, then others.
        // 記錄插入的動(dòng)態(tài)庫(kù)數(shù)量
        sInsertedDylibCount = sAllImages.size()-1;

### 第五步 鏈接主程序

實(shí)例化之后就是動(dòng)態(tài)鏈接的過(guò)程。link 這個(gè)過(guò)程就是將加載進(jìn)來(lái)的二進(jìn)制變?yōu)榭捎脿顟B(tài)的過(guò)程。簡(jiǎn)單來(lái)說(shuō)就是:rebase => binding

  • rebase:就是針對(duì) “mach-o在加載到虛擬內(nèi)存中不是固定的首地址” 這一現(xiàn)象做數(shù)據(jù)修正的過(guò)程。一般可執(zhí)行文件在沒(méi)有ASLR造成的首地址不固定的情況下, 裝載進(jìn)虛擬地址中的首地址都是固定的, 比如:Linux下一般都是0x08040000,Windows下一般都是0x0040000,Mach-O的TEXT地址在__PageZero之后的0x100000000地址.
  • 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)系綁定到一起。
  • lazyBinding:就是在加載動(dòng)態(tài)庫(kù)的時(shí)候不會(huì)立即binding, 當(dāng)時(shí)當(dāng)?shù)谝淮握{(diào)用這個(gè)方法的時(shí)候再實(shí)施binding。 做到的方法也很簡(jiǎn)單: 通過(guò)dyld_stub_binder這個(gè)符號(hào)來(lái)做。 lazy binding的方法第一次會(huì)調(diào)用到dyld_stub_binder, 然后dyld_stub_binder負(fù)責(zé)找到真實(shí)的方法,并且將地址bind到樁上,下一次就不用再bind了。
  • weakBinding:下方還有一步weakBinding
        #pragma mark -- 第五步 鏈接主程序
        // link main executable
        gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
        if ( mainExcutableAlreadyRebased ) {
            // previous link() on main executable has already adjusted its internal pointers for ASLR 
            // work around that by rebasing by inverse amount
            sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
        }
#endif
        /*
        link() 函數(shù)的遞歸調(diào)用函數(shù)堆棧形式
          ▼ ImageLoader::link() //啟動(dòng)主程序的連接進(jìn)程   —— ImageLoader.cpp,ImageLoader類(lèi)中可以發(fā)現(xiàn)很多由dyld調(diào)用來(lái)實(shí)現(xiàn)二進(jìn)制加載邏輯的函數(shù)。
            ▼ recursiveLoadLibraries() //進(jìn)行所有需求動(dòng)態(tài)庫(kù)的加載
              ?? //確定所有需要的庫(kù)
              ▼ context.loadLibrary() //來(lái)逐個(gè)加載。context對(duì)象是一個(gè)簡(jiǎn)單的結(jié)構(gòu)體,包含了在方法和函數(shù)之間傳遞的函數(shù)指針。這個(gè)結(jié)構(gòu)體的loadLibrary成員在libraryLocator()函數(shù)(dyld.cpp)中初始化,它完成的功能也只是簡(jiǎn)單的調(diào)用load()函數(shù)。
                ▼ load() // 源碼在dyld.cpp,會(huì)調(diào)用各種幫助函數(shù)。
                  ?? loadPhase0() → loadPhase1() → ... → loadPhase5() → loadPhase5load() → loadPhase5open() → loadPhase6() 遞歸調(diào)用  //每一個(gè)函數(shù)都負(fù)責(zé)加載進(jìn)程工作的一個(gè)具體任務(wù)。比如,解析路徑或者處理會(huì)影響加載進(jìn)程的環(huán)境變量。
                  ▼ loadPhase6() // 該函數(shù)從文件系統(tǒng)加載需求的dylib到內(nèi)存中。然后調(diào)用一個(gè)ImageLoaderMachO類(lèi)的實(shí)例對(duì)象。來(lái)完成每個(gè)dylib對(duì)象Mach-O文件具體的加載和連接邏輯。
         */
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        sMainExecutable->setNeverUnloadRecursive();
        if ( sMainExecutable->forceFlat() ) {
            gLinkContext.bindFlat = true;
            gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
        }

### 第六步 鏈接插入的動(dòng)態(tài)庫(kù)

        #pragma mark -- 第六步 鏈接插入的動(dòng)態(tài)庫(kù)
        // link any inserted libraries
        // do this after linking main executable so that any dylibs pulled in by inserted 
        // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
        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();
            }
            // 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);
            }
        }

        // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
        for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
            ImageLoader* image = sAllImages[i];
            if ( image->inSharedCache() )
                continue;
            image->registerInterposing(gLinkContext);
        }
        ......

        // apply interposing to initial set of images
        for(int i=0; i < sImageRoots.size(); ++i) {
            sImageRoots[i]->applyInterposing(gLinkContext);
        }
        gLinkContext.notifyBatch(dyld_image_state_bound, false);

        // Bind and notify for the inserted images now interposing has been registered
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
            }
        }

### 第七步 弱符號(hào)綁定

        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        #pragma mark -- 第七步 執(zhí)行弱符號(hào)綁定。weakBind: 從代碼中可以看出這一步會(huì)對(duì)所有含有弱符號(hào)的鏡像合并排序進(jìn)行bind。OC中沒(méi)發(fā)現(xiàn)應(yīng)用場(chǎng)景,可能是C++的吧
        sMainExecutable->weakBind(gLinkContext);
        gLinkContext.linkingMainExecutable = false;

        sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);

        CRSetCrashLogMessage("dyld: launch, running initializers");
        ......  

### 第八步 執(zhí)行初始化方法

dyld會(huì)優(yōu)先初始化動(dòng)態(tài)庫(kù),然后初始化主程序。

        #pragma mark -- 第八步 執(zhí)行初始化方法initialize() 
        // run all initializers
        //attribute((constructor)) 修飾的函數(shù)就是在這一步執(zhí)行的, 即在主程序的main()函數(shù)之前。__DATA中有個(gè)Section __mod_init_func就是記錄這些函數(shù)的。
        //與之對(duì)應(yīng)的是attribute((destructor))修飾的函數(shù), 是主程序 main() 執(zhí)行之后的一些全局函數(shù)析構(gòu)操作, 也是記錄在一個(gè)Section __mod_term_func中.
        /*
        initializeMainExecutable()函數(shù)的遞歸調(diào)用函數(shù)堆棧形式:
          ?? 先初始化動(dòng)態(tài)庫(kù),for(size_t i=1; i < rootCount; ++i) { sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]); }  // run initialzers for any inserted dylibs
          ▼ 再初始化可執(zhí)行文件 sMainExecutable->runInitializers()  // run initializers for main executable and everything it brings up 
            ▼ ImageLoader::processInitializers()
              ▼ ImageLoader::recursiveInitialization()  // 循環(huán)遍歷images list中所有的imageloader,recursive(遞歸)初始化。Calling recursive init on all images in images list
                ▼ ImageLoaderMachO::doInitialization()  // 初始化這個(gè)image. initialize this image
                  ▼ ImageLoaderMachO::doImageInit()  //解析LC_ROUTINES_COMMAND 這個(gè)加載命令,可以參考loader.h中該命令的說(shuō)明,這個(gè)命令包含了動(dòng)態(tài)共享庫(kù)初始化函數(shù)的地址,該函數(shù)必須在庫(kù)中任意模塊初始化函數(shù)(如C++ 靜態(tài)構(gòu)造函數(shù)等)之前調(diào)用
                  ▼ ImageLoaderMachO::doModInitFunctions()  // 內(nèi)部會(huì)調(diào)用C++全局對(duì)象的構(gòu)造函數(shù)、__attribute__((constructor))修飾的C函數(shù)
                  // 以上兩個(gè)函數(shù)中,libSystem相關(guān)的都是要首先執(zhí)行的,而且在上述遞歸加載動(dòng)態(tài)庫(kù)過(guò)程,libSystem是默認(rèn)引入的,所以棧中會(huì)出現(xiàn)libSystem_initializer的初始化方法
          ?? (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
        */
        initializeMainExecutable(); 

        // 通知所有的監(jiān)視進(jìn)程,本進(jìn)程要進(jìn)入main()函數(shù)了。 notify any montoring proccesses that this process is about to enter main()
        notifyMonitoringDyldMain();
        ......

在上面的doImageInitdoModInitFunctions函數(shù)中,會(huì)發(fā)現(xiàn)都有判斷libSystem庫(kù)是否已加載的代碼,即libSystem要首先加載、初始化。在上文中,我們已經(jīng)強(qiáng)調(diào)了這個(gè)庫(kù)的重要性。之所以在這里又提到,是因?yàn)檫@個(gè)庫(kù)也起到了將dyld與objc關(guān)聯(lián)起來(lái)的作用:

可以從上面的調(diào)用堆棧中看到,從dyld到objc的流程,下面來(lái)插一段objc的源碼objc-os.mm_object_init函數(shù)的實(shí)現(xiàn):

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // 各種初始化
    environ_init();
    tls_init();
    static_init();
    lock_init();
    // 看了一下exception_init是空實(shí)現(xiàn)!!就是說(shuō)objc的異常是完全采用c++那一套的。
    exception_init();
   // 注冊(cè)dyld事件的監(jiān)聽(tīng),該方法是dyld提供的,內(nèi)部調(diào)用了dyld::registerObjCNotifiers這個(gè)方法,記錄了這三個(gè)分別對(duì)應(yīng)map,init,unmap事件的回調(diào)函數(shù),會(huì)在相應(yīng)時(shí)機(jī)觸發(fā)
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

這三個(gè)函數(shù)就很熟悉了,位于objc-runtime-new.mm中,objc運(yùn)行時(shí)老生常談的幾個(gè)方法(關(guān)于OBJC的部分,內(nèi)容太多,這里簡(jiǎn)單介紹,下篇細(xì)談),每次有新的鏡像加載時(shí)都會(huì)在指定時(shí)機(jī)觸發(fā)這幾個(gè)方法:

  • map_images : 每當(dāng) dyld 將一個(gè) image 加載進(jìn)內(nèi)存時(shí) , 會(huì)觸發(fā)該函數(shù)進(jìn)行image的一些處理:如果是首次,初始化執(zhí)行環(huán)境等,之后_read_images進(jìn)行讀取,進(jìn)行類(lèi)、元類(lèi)、方法、協(xié)議、分類(lèi)的一些加載。
  • load_images : 每當(dāng) dyld 初始化一個(gè) image 會(huì)觸發(fā)該方法,會(huì)對(duì)該 image 進(jìn)行+load的調(diào)用
  • unmap_image : 每當(dāng) dyld 將一個(gè) image 移除時(shí) , 會(huì)觸發(fā)該函數(shù)
引圖自 https://juejin.im/post/6844904068867948552#heading-0

值得說(shuō)明的是,這個(gè)初始化的過(guò)程遠(yuǎn)比寫(xiě)出來(lái)的要復(fù)雜,這里只提到了 runtime 這個(gè)分支,還有像 GCD、XPC 等重頭的系統(tǒng)庫(kù)初始化分支沒(méi)有提及(當(dāng)然,有緩存機(jī)制在,也不會(huì)重復(fù)初始化),總結(jié)起來(lái)就是 main 函數(shù)執(zhí)行之前,系統(tǒng)做了非常多的加載和初始化工作,但都被很好的隱藏了,我們無(wú)需關(guān)心。

然后,從上面最后的代碼(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL); 以及注釋register cxa_atexit() handler to run static terminators in all loaded images when this process exits可以看出注冊(cè)了cxa_atexit()函數(shù),當(dāng)此進(jìn)程退出時(shí),該處理程序會(huì)運(yùn)行所有加載的image中的靜態(tài)終止程序(static terminators)。

### 第九步 查找主程序入口點(diǎn)并返回,__dyld_start會(huì)跳轉(zhuǎn)進(jìn)入

        #pragma mark -- 第九步 查找入口點(diǎn) main() 并返回,調(diào)用 getEntryFromLC_MAIN,從 Load Command 讀取LC_MAIN入口,如果沒(méi)有LC_MAIN入口,就讀取LC_UNIXTHREAD,然后跳到主程序的入口處執(zhí)行
        // find entry point for main executable
        result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
        if ( result != 0 ) {
            // main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
            if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
                *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
            else
                halt("libdyld.dylib support not present for LC_MAIN");
        }
        else {
            // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
            result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
            *startGlue = 0;
        }
    ......

    catch(const char* message) {
        syncAllImages();
        halt(message);
    }
    catch(...) {
        dyld::log("dyld: launch failed\n");
    }
    ......
    return result;
}

# 小結(jié)

引自iOS 程序 main 函數(shù)之前發(fā)生了什么一文中的片段,《 Mike Ash 這篇 blog 》對(duì) dyld 作用順序的概括:

  1. 從 kernel 留下的原始調(diào)用棧引導(dǎo)和啟動(dòng)自己
  2. 將程序依賴的動(dòng)態(tài)鏈接庫(kù)遞歸加載進(jìn)內(nèi)存,當(dāng)然這里有緩存機(jī)制
  3. non-lazy 符號(hào)立即 link 到可執(zhí)行文件,lazy 的存表里
  4. Runs static initializers for the executable
  5. 找到可執(zhí)行文件的 main 函數(shù),準(zhǔn)備參數(shù)并調(diào)用
  6. 程序執(zhí)行中負(fù)責(zé)綁定 lazy 符號(hào)、提供 runtime dynamic loading services、提供調(diào)試器接口
  7. 程序main函數(shù) return 后執(zhí)行 static terminator
  8. 某些場(chǎng)景下 main 函數(shù)結(jié)束后調(diào) libSystem 的 _exit 函數(shù)

然后,使用調(diào)用堆棧,來(lái)看下dyld的工作流程,只注釋了認(rèn)為重要的部分。

#pragma mark -- 內(nèi)核XNU加載Mach-O
#pragma mark -- 從 XNU內(nèi)核態(tài) 將控制權(quán)轉(zhuǎn)移到 dyld用戶態(tài)
▼ dyld
  ▼ __dyld_start   // 源碼在dyldStartup.s這個(gè)文件,用匯編實(shí)現(xiàn)
    ▼ dyldbootstrap::start()   //dyldInitialization.cpp,負(fù)責(zé)dyld的引導(dǎo)工作
      ▼ dyld::_main()   // dyld.cpp
        ?? // 第一步,設(shè)置運(yùn)行環(huán)境
        ?? // 第二步,加載共享緩存
        ?? // 第三步 實(shí)例化主程序,會(huì)實(shí)例化一個(gè)主程序ImageLoader
        ▼ instantiateFromLoadedImage()  
          ?? isCompatibleMachO()  // 檢查mach-o的subtype是否是當(dāng)前cpu可以支持;
          ?? instantiateMainExecutable()  // 實(shí)例化可執(zhí)行文件,這個(gè)期間會(huì)解析LoadCommand,這個(gè)之后會(huì)發(fā)送 dyld_image_state_mapped 通知;
          ?? addImage()  // 將可執(zhí)行文件這個(gè)image,添加到 allImages中
        ?? // 第四步,循環(huán)調(diào)用該函數(shù),加載插入的動(dòng)態(tài)庫(kù)
        ?? loadInsertedDylib()  
        ?? // 第五步,調(diào)用link()函數(shù),鏈接主程序
        ▼ link()  
          ▼ ImageLoader::link() //啟動(dòng)主程序的連接進(jìn)程   —— ImageLoader.cpp,ImageLoader類(lèi)中可以發(fā)現(xiàn)很多由dyld調(diào)用來(lái)實(shí)現(xiàn)二進(jìn)制加載邏輯的函數(shù)。
            ▼ recursiveLoadLibraries() //進(jìn)行所有需求動(dòng)態(tài)庫(kù)的加載
              ?? //確定所有需要的庫(kù)
              ▼ context.loadLibrary() //來(lái)逐個(gè)加載。context對(duì)象是一個(gè)簡(jiǎn)單的結(jié)構(gòu)體,包含了在方法和函數(shù)之間傳遞的函數(shù)指針。這個(gè)結(jié)構(gòu)體的loadLibrary成員在libraryLocator()函數(shù)(dyld.cpp)中初始化,它完成的功能也只是簡(jiǎn)單的調(diào)用load()函數(shù)。
                ▼ load() // 源碼在dyld.cpp,會(huì)調(diào)用各種幫助函數(shù)。
                  ?? loadPhase0() → loadPhase1() → ... → loadPhase5() → loadPhase5load() → loadPhase5open() → loadPhase6() 遞歸調(diào)用  //每一個(gè)函數(shù)都負(fù)責(zé)加載進(jìn)程工作的一個(gè)具體任務(wù)。比如,解析路徑或者處理會(huì)影響加載進(jìn)程的環(huán)境變量。
                  ▼ loadPhase6() // 該函數(shù)從文件系統(tǒng)加載需求的dylib到內(nèi)存中。然后調(diào)用一個(gè)ImageLoaderMachO類(lèi)的實(shí)例對(duì)象。來(lái)完成每個(gè)dylib對(duì)象Mach-O文件具體的加載和連接邏輯。
        ?? // 第六步,調(diào)用link()函數(shù),鏈接插入的動(dòng)態(tài)庫(kù)
        ?? // 第七步,對(duì)主程序進(jìn)行弱符號(hào)綁定weakBind
        ?? sMainExecutable->weakBind(gLinkContext);
        ?? // 第八步,執(zhí)行初始化方法 initialize。attribute((constructor)) 修飾的函數(shù)就是在這一步執(zhí)行的, 即在主程序的main()函數(shù)之前。__DATA中有個(gè)Section __mod_init_func就是記錄這些函數(shù)的。
        ▼ initializeMainExecutable()  // dyld會(huì)優(yōu)先初始化動(dòng)態(tài)庫(kù),然后初始化主程序。
          ▼ sMainExecutable->runInitializersrunInitializers()  // run initializers for main executable and everything it brings up 
            ▼ ImageLoader::processInitializers()
              ▼ ImageLoader::recursiveInitialization()  // 循環(huán)遍歷images list中所有的imageloader,recursive(遞歸)初始化。Calling recursive init on all images in images list
                ▼ ImageLoaderMachO::doInitialization()  // 初始化這個(gè)image. initialize this image
                  ▼ ImageLoaderMachO::doImageInit()  //解析LC_ROUTINES_COMMAND 這個(gè)加載命令,可以參考loader.h中該命令的說(shuō)明,這個(gè)命令包含了動(dòng)態(tài)共享庫(kù)初始化函數(shù)的地址,該函數(shù)必須在庫(kù)中任意模塊初始化函數(shù)(如C++ 靜態(tài)構(gòu)造函數(shù)等)之前調(diào)用
                  ▼ ImageLoaderMachO::doModInitFunctions()  // 內(nèi)部會(huì)調(diào)用C++全局對(duì)象的構(gòu)造函數(shù)、__attribute__((constructor))修飾的C函數(shù)
                  // 以上兩個(gè)函數(shù)中,libSystem相關(guān)的都是要首先執(zhí)行的,而且在上述遞歸加載動(dòng)態(tài)庫(kù)過(guò)程,libSystem是默認(rèn)引入的,所以棧中會(huì)出現(xiàn)libSystem_initializer的初始化方法
          ?? (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
        ?? // 第九步,查找入口點(diǎn) main() 并返回,調(diào)用 getEntryFromLC_MAIN,從 Load Command 讀取LC_MAIN入口,如果沒(méi)有LC_MAIN入口,就讀取LC_UNIXTHREAD,然后跳到主程序的入口處執(zhí)行
        ?? (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
網(wǎng)絡(luò)引圖,勘誤:右下角libSystemInitialized是在doModInitFunctions中調(diào)用

關(guān)于更多的理論知識(shí),可以閱讀下iOS程序員的自我修養(yǎng)-MachO文件動(dòng)態(tài)鏈接(四)實(shí)踐篇—fishhook原理(:程序運(yùn)行期間通過(guò)修改符號(hào)表(nl_symbol_ptr和la_symbol_ptr),來(lái)替換要hook的符號(hào)對(duì)應(yīng)的地址),將《程序員的自我修養(yǎng)》中的理論結(jié)合iOS系統(tǒng)中的實(shí)現(xiàn)機(jī)制做了個(gè)對(duì)比介紹。

# 加載動(dòng)態(tài)庫(kù)的另一種方式:顯式運(yùn)行時(shí)鏈接dlopen

上面的這種動(dòng)態(tài)鏈接,其實(shí)還可以稱為裝載時(shí)鏈接,與靜態(tài)鏈接相比,其實(shí)都是屬于在程序運(yùn)行之前進(jìn)行的鏈接。還有另一種動(dòng)態(tài)鏈接稱為顯式運(yùn)行時(shí)鏈接(Explicit Runtime Linking)

裝載時(shí)鏈接:是在程序開(kāi)始運(yùn)行時(shí)(前)通過(guò)dyld動(dòng)態(tài)加載。通過(guò)dyld加載的動(dòng)態(tài)庫(kù)需要在編譯時(shí)進(jìn)行鏈接,鏈接時(shí)會(huì)做標(biāo)記,綁定的地址在加載后再?zèng)Q定。

顯式運(yùn)行時(shí)鏈接:即在運(yùn)行時(shí)通過(guò)動(dòng)態(tài)鏈接器dyld提供的API dlopen 和 dlsym 來(lái)加載。這種方式,在編譯時(shí)是不需要參與鏈接的。

  • dlopen會(huì)把共享庫(kù)載入運(yùn)行進(jìn)程的地址空間,載入的共享庫(kù)也會(huì)有未定義的符號(hào),這樣會(huì)觸發(fā)更多的共享庫(kù)被載入。
  • dlopen也可以選擇是立刻解析所有引用還是滯后去做。
  • dlopen打開(kāi)動(dòng)態(tài)庫(kù)后返回的是模塊的指針(句柄/文件描述符(FD))
  • dlsym的作用就是通過(guò)dlopen返回的動(dòng)態(tài)庫(kù)指針和函數(shù)的符號(hào),得到函數(shù)的地址然后使用。

不過(guò),通過(guò)這種運(yùn)行時(shí)加載遠(yuǎn)程動(dòng)態(tài)庫(kù)的 App,蘋(píng)果公司是不允許上線 App Store 的,所以只能用于線下調(diào)試環(huán)節(jié)。

# 參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,506評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,614評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,402評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,934評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,168評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,596評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,027評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,404評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,662評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,398評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,743評(píng)論 2 370