哈羅,大家好好久沒(méi)有更新iOS專欄的內(nèi)容了,今天想與大家聊一聊iOS的dyld的內(nèi)容,想用雜談的形式與大家侃侃分享一下dyld的步驟,讓我們愉快的開(kāi)始吧:
其實(shí)當(dāng)初我是想從頭到尾開(kāi)始寫一篇關(guān)于dyld內(nèi)容的帖子的,但是當(dāng)我查看網(wǎng)絡(luò)上有很多文章已經(jīng)寫得很好了,自己再寫的話已經(jīng)無(wú)法站在別人的高度來(lái)再進(jìn)行創(chuàng)作了,所以今天這一篇我將借用網(wǎng)絡(luò)上我認(rèn)為寫得比較不錯(cuò)的一篇文章,我再來(lái)基于該文來(lái)進(jìn)行下總結(jié),這個(gè)也是為什么我們要用雜談的方式進(jìn)行的原因了
參考文獻(xiàn)如下:
iOS dyld詳解
該文把dyld分為了以下10個(gè)步驟:
- 第一步:dyldbootstrap::start()函數(shù)
- 第二步:設(shè)置運(yùn)行環(huán)境。
- 第三步:加載共享緩存。
- 第四步:實(shí)例化主程序。
- 第五步:加載插入的動(dòng)態(tài)庫(kù)。
- 第六步:鏈接主程序。
- 第七步:鏈接插入的動(dòng)態(tài)庫(kù)。
- 第八步:執(zhí)行弱符號(hào)綁定
- 第九步:執(zhí)行初始化方法。
- 第十步:查找入口點(diǎn)并返回。
我們?cè)谶@10個(gè)步驟基于一些重要的點(diǎn)再談?wù)勎易约旱目捶?,也為算是幫助大家在這個(gè)基礎(chǔ)上提煉一下更重要的過(guò)程,大家也可以在面試中簡(jiǎn)約的作為dyld過(guò)程的表達(dá):
首先是第一步運(yùn)行dyldbootstrap::start()函數(shù):
首先要知道Rebasing是什么:
在過(guò)去會(huì)把 dylib 加載到指定地址,所有指針和數(shù)據(jù)對(duì)于代碼來(lái)說(shuō)都是對(duì)的,dyld 就無(wú)需做任何 fix-up 了。如今用了 ASLR 后會(huì)將 dylib 加載到新的隨機(jī)地址(actual_address),這個(gè)隨機(jī)的地址跟代碼和數(shù)據(jù)指向的舊地址(preferred_address)會(huì)有偏差,dyld 需要修正這個(gè)偏差(slide),做法就是將 dylib 內(nèi)部的指針地址都加上這個(gè)偏移量,偏移量的計(jì)算方法如下:
Slide = actual_address - preferred_address
為什么要進(jìn)行dyld Rebasing呢,首先大家要知道的是dyld本身就是一個(gè)動(dòng)態(tài)的庫(kù),他自己也是需要運(yùn)行的,所以他就自己也需要Rebasing才能運(yùn)行
第二步:設(shè)置運(yùn)行環(huán)境:
這一步主要是設(shè)置運(yùn)行參數(shù)、環(huán)境變量等,這一步不作為重點(diǎn)講述
第三步:加載共享緩存:
首先我們要知道什么是共享緩存:
在iOS系統(tǒng)中,每個(gè)程序依賴的動(dòng)態(tài)庫(kù)都需要通過(guò)dyld(位于/usr/lib/dyld)一個(gè)一個(gè)加載到內(nèi)存,然而很多系統(tǒng)庫(kù)幾乎是每個(gè)程序都會(huì)用到的,如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次,勢(shì)必造成運(yùn)行緩慢,為了優(yōu)化啟動(dòng)速度和提高程序性能,共享緩存機(jī)制就應(yīng)運(yùn)而生。所有默認(rèn)的動(dòng)態(tài)鏈接庫(kù)被合并成一個(gè)大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構(gòu)保存分別保存著
也就是說(shuō)共享緩存是一些動(dòng)態(tài)庫(kù)的集合,基本上就是系統(tǒng)的動(dòng)態(tài)庫(kù),這里做的重要的事情就是一次性把所有系統(tǒng)動(dòng)態(tài)庫(kù)加載到內(nèi)存中,如果共享緩存已加載或者被別人加載了(因?yàn)閯?dòng)態(tài)庫(kù)本身就是共用的,共享緩存也就是動(dòng)態(tài)庫(kù)的集合,不需要每個(gè)進(jìn)程都加載一遍),則不做任何處理(我是這么理解的)
第四步:實(shí)例化主程序:
這一步將主程序的Mach-O加載進(jìn)內(nèi)存,并實(shí)例化一個(gè)ImageLoader指向了這個(gè)主程序的image(鏡像),這里又涉及到了一個(gè)概念,image也就是鏡像指的是什么呢:
其實(shí)很簡(jiǎn)單他就是指可執(zhí)行的Mach-O文件,并且Mach-O文件不緊緊指的是最后生成的可直接運(yùn)行App程序,還包括所有的動(dòng)態(tài),靜態(tài)庫(kù)這些都是數(shù)據(jù)可執(zhí)行的二進(jìn)制Mach-O文件,也就是我們說(shuō)的鏡像
第五步:加載插入的動(dòng)態(tài)庫(kù):
這一步主要是是加載環(huán)境變量DYLD_INSERT_LIBRARIES中配置的動(dòng)態(tài)庫(kù),為什么又要加載動(dòng)態(tài)庫(kù)呢,因?yàn)榈谌渴羌虞d系統(tǒng)的動(dòng)態(tài)庫(kù),但是你的程序會(huì)使用到一些第三方的動(dòng)態(tài)庫(kù)的話,那么也是需要load進(jìn)行內(nèi)存才能使用的,加載每一個(gè)鏡像就需要初始化一個(gè)ImageLoader指向加載的鏡像
第六步:鏈接主程序:
鏈接主程序最主要的步驟就是 recursiveRebase,recursiveBind也就是循環(huán)的Rebase,Bind(循環(huán)就是這個(gè)庫(kù)依賴了別的庫(kù),被依賴的庫(kù)也需要Rebasing,Binding)
- Rebasing :
上面已經(jīng)解釋過(guò)了,就是加上一個(gè)偏移
- Binding :
網(wǎng)絡(luò)上關(guān)于Binding是如下講解的: Binding是處理那些指向 dylib 外部的指針,它們實(shí)際上被符號(hào)(symbol)名稱綁定,也就是個(gè)字符串。之前提到 __LINKEDIT 段中也存儲(chǔ)了需要 bind 的指針,以及指針需要指向的符號(hào)。dyld 需要找到 symbol 對(duì)應(yīng)的實(shí)現(xiàn),這需要很多計(jì)算,去符號(hào)表里查找。找到后會(huì)將內(nèi)容存儲(chǔ)到 __DATA 段中的那個(gè)指針中。
我覺(jué)網(wǎng)絡(luò)上的講解太過(guò)于籠統(tǒng)了,我可以說(shuō)一下我的理解,我以前做Linux的時(shí)候閱讀過(guò)一些關(guān)于編譯,連接相關(guān)的書(shū)籍,這里我可以用我的知識(shí)來(lái)幫助大家講解下:
首先大家要知道動(dòng)態(tài)庫(kù)應(yīng)該是代碼與地址無(wú)關(guān)的也就是PIC的(例如我們用GCC -fPIC可以在Linux下面編譯出位置無(wú)關(guān)的.so動(dòng)態(tài)庫(kù)),但是你的程序或者說(shuō)一個(gè)進(jìn)程需要用到這些動(dòng)態(tài)庫(kù)的時(shí)候就需要精確到地址(這里的地址指的是虛擬地址,你的進(jìn)程肯定是運(yùn)行在一個(gè)固定的虛擬地址下的),那么這些這些地址無(wú)關(guān)的代碼肯定是不能直接運(yùn)行的(很簡(jiǎn)單,你試想一下如果你調(diào)用一個(gè)函數(shù)或者變量如果調(diào)用地址都不確定的話那么肯定運(yùn)行不了)所以要對(duì)這些動(dòng)態(tài)庫(kù)進(jìn)行重定位(Linux下重定位這個(gè)概念應(yīng)該符合這個(gè)Binding的過(guò)程),重定位的大致過(guò)程就是對(duì)于動(dòng)態(tài)庫(kù)的got段里面的地址進(jìn)行修改達(dá)到可以找到目標(biāo)符號(hào)的目的
再順便幫助大家提一點(diǎn),大家想過(guò)一個(gè)問(wèn)題沒(méi)有,為什么dyld本身不需要Binding呢,大家可以想象一點(diǎn),dyld雖然是動(dòng)態(tài)庫(kù)但是本身不是動(dòng)態(tài)鏈接的,言外之意就是他本身不會(huì)再依賴別的動(dòng)態(tài)庫(kù)了,因?yàn)樗旧砭褪怯脕?lái)加載鏈接其他庫(kù)的如果他再依賴別的庫(kù)那么,那么誰(shuí)來(lái)幫他加載鏈接呢,所以dyld與Linux下的動(dòng)態(tài)鏈接器ld.so是一樣的他們是靜態(tài)鏈接的,所以運(yùn)行的時(shí)候不需要鏈接其他的庫(kù)重定位(bingding)這個(gè)步驟了
第七步:鏈接插入的動(dòng)態(tài)庫(kù):
這一步最重要的內(nèi)容就是對(duì)于動(dòng)態(tài)庫(kù)進(jìn)行recursiveRebase,recursiveBind,與上面的類似
第八步:執(zhí)行弱符號(hào)綁定:
首先要知道什么是弱符號(hào):
函數(shù)和全局變量編譯后需要有唯一的符號(hào)名(簡(jiǎn)單的可以理解為變量名或者函數(shù)名之類的),在鏈接時(shí)才不會(huì)混淆。程序員所寫代碼中的變量名會(huì)經(jīng)過(guò)修飾后作為符號(hào)名,比如 C 中fun會(huì)被修飾為_(kāi)fun,而符號(hào)分為弱符號(hào)與強(qiáng)符號(hào),對(duì)于 C/C++ 來(lái)說(shuō),編譯器默認(rèn)函數(shù)和已初始化的全局變量為強(qiáng)符號(hào)(包括顯示初始化為0),未初始化的全局變量為弱符號(hào),另外你也可以使用
__attribute__ ((weak))
來(lái)定義一個(gè)弱符號(hào),編譯器決議符號(hào)時(shí)有如下規(guī)則:
不允許強(qiáng)符號(hào)被多次定義,否則會(huì)報(bào)錯(cuò)。
多個(gè)符號(hào)名重復(fù)且只有一個(gè)強(qiáng)符號(hào)時(shí),選擇強(qiáng)符號(hào)。
多個(gè)符號(hào)名重復(fù)且都是弱符號(hào)時(shí),選擇占用空間最大的一個(gè)。
值得注意的是這里說(shuō)的強(qiáng)弱符號(hào)只是針對(duì)于跨目標(biāo)文件符號(hào)的定義,例如你的本目標(biāo)文件的局部變量則沒(méi)有該強(qiáng)弱的區(qū)分
第八步這一步做的大概就是確定弱符號(hào)的數(shù)據(jù)偏移與大小,最終決定最合適的弱符號(hào)綁定到可運(yùn)行地址上面(我是這么理解的)
第九步:執(zhí)行初始化方法:
這一步最重要的就是注冊(cè)的init回調(diào)函數(shù)(load_images)回調(diào)里面調(diào)用了call_load_methods()來(lái)執(zhí)行所有的+ load方法;以及調(diào)用了全局C++對(duì)象的構(gòu)造函數(shù)以及所有帶
__attribute__((constructor)
的C函數(shù),值得一提的就是,這個(gè)語(yǔ)法也是GNU C 的一大特色,后面括號(hào)里面的內(nèi)容不是隨便加的,這個(gè)語(yǔ)法可以定一個(gè)段segment,鏈接器在遇到這種固定的段的時(shí)候會(huì)有一些特殊的處理,在這里就是這類的函數(shù)在某個(gè)時(shí)間段(main函數(shù)之前)統(tǒng)一運(yùn)行,類似的語(yǔ)法還有
__attribute__((destructor))
這個(gè)可以保證這類的函數(shù)在main函數(shù)調(diào)用后調(diào)用這個(gè)方法
第十步:查找入口點(diǎn)并返回:
這一步就可以跳轉(zhuǎn)到我們自己運(yùn)行的main函數(shù)了
好了,十個(gè)步驟我們已經(jīng)走完了,這里面借用了一篇大師的文章和我的一些個(gè)人理解去幫助大家去閱讀,如果有任何疑問(wèn)或者想法可以給我留言,另外希望大家關(guān)注我,您的持續(xù)關(guān)注是我持續(xù)寫作的動(dòng)力,那我們下期見(jiàn)了···