# 動(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)。
前者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ù)的角色,還提供了dlopen
、dlsym
等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();
......
在上面的doImageInit
、doModInitFunctions
函數(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ù)
值得說(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 作用順序的概括:
- 從 kernel 留下的原始調(diào)用棧引導(dǎo)和啟動(dòng)自己
- 將程序依賴的動(dòng)態(tài)鏈接庫(kù)遞歸加載進(jìn)內(nèi)存,當(dāng)然這里有緩存機(jī)制
- non-lazy 符號(hào)立即 link 到可執(zhí)行文件,lazy 的存表里
- Runs static initializers for the executable
- 找到可執(zhí)行文件的 main 函數(shù),準(zhǔn)備參數(shù)并調(diào)用
- 程序執(zhí)行中負(fù)責(zé)綁定 lazy 符號(hào)、提供 runtime dynamic loading services、提供調(diào)試器接口
- 程序main函數(shù) return 后執(zhí)行 static terminator
- 某些場(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();
關(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é)。