本文分為理論【1-4】和實(shí)踐【5-6】?jī)刹糠郑?/p>
-
main()函數(shù)之前發(fā)生了什么 -
Mach-O格式 - 虛擬內(nèi)存基礎(chǔ)知識(shí)
- 如何加載和準(zhǔn)備
Mach-O二進(jìn)制文件 - 如何測(cè)量啟動(dòng)時(shí)間
- 優(yōu)化啟動(dòng)時(shí)間
一、Mach-O文件
Mach-O是運(yùn)行時(shí)可執(zhí)行文件的文件類型。
(一)Mach-O的文件類型
- 可執(zhí)行文件:它是應(yīng)用程序中最重要的二進(jìn)制文件,也是應(yīng)用擴(kuò)展文件的主二進(jìn)制文件。
- 動(dòng)態(tài)庫(kù)【Dylib】:它是一個(gè)動(dòng)態(tài)庫(kù),在其他平臺(tái)上又稱為DSO或DLL,
- 捆綁包【Bundle】:它是一種特殊的動(dòng)態(tài)庫(kù),無(wú)法進(jìn)行鏈接,只能在運(yùn)行時(shí)使用
dlopen()函數(shù)打開(kāi)它。Mac OS的插件會(huì)用到。
圖像【Image】:它是指可執(zhí)行文件,動(dòng)態(tài)庫(kù)或捆綁包的任意一種類型;
框架【Framework】:它是一種帶有資源和標(biāo)頭目錄的動(dòng)態(tài)庫(kù),存儲(chǔ)該動(dòng)態(tài)庫(kù)需要的文件。
函數(shù)定義:
void * dlopen( const char * pathname, int mode );
函數(shù)描述:
dlopen函數(shù)以指定模式打開(kāi)指定的動(dòng)態(tài)連接庫(kù)文件,并返回一個(gè)句柄給調(diào)用進(jìn)程。使用dlclose()來(lái)卸載打開(kāi)的庫(kù)。
mode:分為這兩種
RTLD_LAZY 暫緩決定,等有需要時(shí)再解出符號(hào)
RTLD_NOW 立即決定,返回前解除所有未決定的符號(hào)。
RTLD_LOCAL
RTLD_GLOBAL 允許導(dǎo)出符號(hào)
RTLD_GROUP
RTLD_WORLD
返回值:
打開(kāi)錯(cuò)誤返回NULL
成功,返回庫(kù)引用
編譯時(shí)候要加入 -ldl (指定dl庫(kù))
(二)Mach-O圖像格式
- 段
-
Mach-O圖像被分成數(shù)段; - 所有的段名都由大寫(xiě)字母組成;
- 每一段都是頁(yè)面大小的倍數(shù),而頁(yè)面大小由硬件決定,
arm64處理器的頁(yè)面大小是16KB,其他都是4KB;
下例中TEXT段大小是3頁(yè),DATA和LINKEDIT段大小都是1頁(yè)。

最常見(jiàn)的段名是TEXT,DATA和LINKEDIT。實(shí)際上幾乎每一個(gè)二進(jìn)制文件都包含這三段,你可以添加自定義段,但一般不會(huì)給它賦值。
TEXT,DATA和LINKEDIT段的作用:
-
TEXT:它是文件的開(kāi)頭,包含了Mach的頭文件,任何機(jī)器指令以及任何只讀常量,比如C字符串。 -
DATA:它是重寫(xiě)段,它包含了所有的全局變量。 -
LINKEDIT:它不包含全局變量的函數(shù),它包含變量函數(shù)信息,比如名稱和地址
- 分區(qū)
- 分區(qū)是段的子范圍;
- 分區(qū)不用遵循頁(yè)面的大??;
- 分區(qū)的名稱都用小寫(xiě)字母表示;

(三)Mach-O通用文件
假設(shè)我們生成一個(gè)64位的iOS應(yīng)用,現(xiàn)在我們有一個(gè)Mach-O文件。當(dāng)我們也想讓它在32位的設(shè)備上運(yùn)行,Xcode中會(huì)發(fā)生什么變化呢?
當(dāng)我們重新生成時(shí),Xcode會(huì)生成另一個(gè)單獨(dú)的Mach-O文件,這個(gè)是為32位生成的armv7。然后這兩個(gè)文件合并成第三個(gè)文件,這個(gè)文件叫作Mach-O通用文件。它前端有一個(gè)頭文件,所有的頭文件都有一個(gè)所有體系結(jié)構(gòu)的列表,它們的偏移值也在文件里。該頭文件也是一個(gè)頁(yè)面的大小。

通過(guò)上面我們知道,Mach-O圖像的每段都是頁(yè)面大小的倍數(shù),而且頭文件也需要一個(gè)頁(yè)面的大小,這樣會(huì)浪費(fèi)很多空間。那為何還要這樣做呢?這就涉及到虛擬內(nèi)存。
二、虛擬內(nèi)存
在軟件工程里有句格言,任何問(wèn)題都可以通過(guò)添加一個(gè)間接層加以解決。而虛擬內(nèi)存所解決的問(wèn)題就是,所有這些進(jìn)程存在時(shí)該如何管理所有的物理內(nèi)存。為了解決這個(gè)問(wèn)題,添加了一個(gè)小的間接層,每個(gè)進(jìn)程都是一個(gè)邏輯地址空間,映射到RAM的某個(gè)物理頁(yè)面,這種映射不一定是一對(duì)一的。邏輯地址可以不對(duì)應(yīng)任何物理RAM,也可以多個(gè)邏輯地址對(duì)應(yīng)同一個(gè)物理RAM,這樣帶來(lái)很多中可能。那能利用虛擬內(nèi)存做什么呢?
首先如果有一個(gè)邏輯地址不映射任何物理RAM,當(dāng)進(jìn)程要訪問(wèn)該地址時(shí)就會(huì)產(chǎn)生頁(yè)面錯(cuò)誤,內(nèi)核將停止該線程,并試圖找出解決方案。
下一點(diǎn)是如果有兩個(gè)進(jìn)程,對(duì)應(yīng)兩個(gè)邏輯地址,這兩個(gè)邏輯地址映射同一個(gè)物理頁(yè)面,這兩個(gè)進(jìn)程共享相同的RAM位,進(jìn)程之間開(kāi)始共享。
另一個(gè)有趣的功能是基于文件的映射,不用把整個(gè)文件讀入RAM,而是可以調(diào)用mmap()函數(shù)告訴虛擬內(nèi)存系統(tǒng),我想把這部分文件映射到進(jìn)程里的這段地址。這么做的原因是,不用讀取整個(gè)文件,通過(guò)設(shè)置該映射,第一次訪問(wèn)這些不同的地址時(shí),如同已經(jīng)在內(nèi)存里讀過(guò),每次訪問(wèn)未訪問(wèn)過(guò)的地址時(shí),都會(huì)導(dǎo)致頁(yè)面錯(cuò)誤,內(nèi)核會(huì)讀該錯(cuò)誤頁(yè)面。這樣將會(huì)造成讀取文件遲緩。
現(xiàn)在我們結(jié)合前面講的關(guān)于Mach-O的內(nèi)容,可以知道任何Dylib和圖像的TEXT段都可以映射到多個(gè)進(jìn)程,這將會(huì)造成讀取遲緩,而這些頁(yè)面可以在進(jìn)程間共享。那么DATA段呢?
DATA用來(lái)讀寫(xiě),有一個(gè)策略叫寫(xiě)入時(shí)復(fù)制,這和Apple文件系統(tǒng)的克隆很相似。寫(xiě)入時(shí)復(fù)制所做的就是它積極地在所有進(jìn)程里共享DATA頁(yè)面。一個(gè)進(jìn)程會(huì)發(fā)生什么,只要它們只是從共享內(nèi)容的全局變量中讀取就可以了。但是一旦有進(jìn)程想要寫(xiě)入其他DATA頁(yè)面,就會(huì)發(fā)生寫(xiě)入時(shí)復(fù)制。
寫(xiě)入時(shí)復(fù)制使內(nèi)核把該頁(yè)面復(fù)制到另一個(gè)物理RAM中,并將映射重定向到該頁(yè)面。所以該進(jìn)程有了該頁(yè)面的副本。這會(huì)給我們帶來(lái)臟頁(yè)面和凈頁(yè)面,而副本被認(rèn)為是臟頁(yè)面。臟頁(yè)面是指含有進(jìn)程的特定信息。凈頁(yè)面是指內(nèi)核可以按照需要重新建立的頁(yè)面,比如重新讀取磁盤(pán)。所以臟頁(yè)面比凈頁(yè)面要昂貴很多。
最后一點(diǎn)是頁(yè)面也有權(quán)限界限,這指的是可以標(biāo)記一個(gè)頁(yè)面可讀、可寫(xiě)或可執(zhí)行、或者它們的任意組合。
虛擬內(nèi)存的作用:
1. 虛擬內(nèi)存是間接層
2. 將每個(gè)進(jìn)程的地址映射到物理RAM(頁(yè)面粒度)
虛擬內(nèi)存的特征:
1. 頁(yè)面錯(cuò)誤
2. 相同的RAM頁(yè)面出現(xiàn)在多個(gè)進(jìn)程中
3. 文件支持的頁(yè)面
3.1 mmap()
3.2 懶讀取
4. 寫(xiě)入時(shí)復(fù)制(COW)
5. 臟頁(yè)面與干凈頁(yè)面
6. 權(quán)限:rwx
以上就是Mach-O格式和虛擬內(nèi)存的內(nèi)容。現(xiàn)在看看它們是如何一起工作的,在之前,我們先看看Dyld(全稱the dynamic link editor,即動(dòng)態(tài)鏈接器,其本質(zhì)Mach-O文件,專門(mén)用來(lái)加載動(dòng)態(tài)庫(kù)的庫(kù))是如何操作的,它在Mach-O和虛擬內(nèi)存之間是如何映射的。
三、Dyld的操作過(guò)程
現(xiàn)有一個(gè)Dylib文件,如下圖所示:

我們沒(méi)有把它讀到內(nèi)存中,而是把它映射到內(nèi)存,所以在內(nèi)存里該Dylib文件本應(yīng)該占用8個(gè)頁(yè)面??梢钥吹剑煌氖怯羞@些“全零填充”。
大部分全局變量的初始值都是零,所以靜態(tài)鏈接器進(jìn)行了優(yōu)化,把所有值為0的全局變量都移到了尾端,然后不占用任何磁盤(pán)空間。取而代之,我們利用虛擬內(nèi)存的特性,在該頁(yè)面第一次被訪問(wèn)時(shí),告訴虛擬內(nèi)存把它填滿0。所以它不需要讀取。Dyld必須要做的第一件事是在內(nèi)存中查看該進(jìn)程的Mach頭文件。它將查看內(nèi)存的頂盒,此時(shí)那里是空的,沒(méi)有內(nèi)容映射到物理頁(yè)面上,所以產(chǎn)生頁(yè)面錯(cuò)誤。到那時(shí)內(nèi)核意識(shí)到它被映射到了一個(gè)文件,所以它將讀取文件的第一頁(yè),將其放入物理RAM,設(shè)置其映射。

現(xiàn)在Dyld可以真正通過(guò)Mach頭文件開(kāi)始讀取。它通過(guò)讀取Mach頭文件,Mach頭文件讓Dyld到LINKEDIT段上查看這條信息。再一次,Dyld跳下去查看進(jìn)程1的底盒。這又會(huì)產(chǎn)生頁(yè)面錯(cuò)誤,內(nèi)核又讀入RAM的另一個(gè)LINKEDIT的物理頁(yè)面。

Dyld現(xiàn)在可以期望一個(gè)LINKEDIT。此刻在進(jìn)程中,LINKEDIT將會(huì)告訴Dyld對(duì)DATA頁(yè)面做一些修正,讓Dylib可運(yùn)行。所以同樣的事情又發(fā)生了,Dyld現(xiàn)從DATA頁(yè)面讀取數(shù)據(jù),但是有一點(diǎn)不同,Dyld想要寫(xiě)回一些內(nèi)容修改DATA頁(yè)面,此刻寫(xiě)入時(shí)復(fù)制出現(xiàn)了。這個(gè)頁(yè)面變成了臟頁(yè)面。所以臟RAM的8個(gè)頁(yè)面將會(huì)是什么?若我只用malloc()函數(shù)分配8頁(yè)內(nèi)存,然后讀了一些內(nèi)容進(jìn)去,我將會(huì)有8個(gè)頁(yè)面的臟RAM。但現(xiàn)在我只有1頁(yè)的臟RAM和2個(gè)凈頁(yè)面。

如果第二個(gè)進(jìn)程加載同一個(gè)Dylib將會(huì)發(fā)生什么?在第二個(gè)進(jìn)程里,Dyld會(huì)經(jīng)歷相同的步驟,首先它查看Mach頭文件,但內(nèi)核在RAM某處已經(jīng)有這頁(yè)了,所以內(nèi)核只是簡(jiǎn)單地把映射重定向,重利用該頁(yè)面,并沒(méi)有任何IO操作。LINKEDIT也是如此,更加快速。我們來(lái)看DATA頁(yè)面,此時(shí)內(nèi)核必須要看看在DATA頁(yè)面,干凈的副本是否還存在RAM其他地方,如果還在,就可以重利用;如果不在,就必須要重新讀取。




在該進(jìn)程中,Dyld會(huì)讓RAM變臟。

最后一步是LINKEDIT,只在Dyld進(jìn)行操作時(shí)被需要。所以它可以提醒內(nèi)核,當(dāng)它完成時(shí),它不再需要這些LINKEDIT頁(yè)面,當(dāng)別人需要RAM時(shí),可以回收它們?,F(xiàn)在有兩個(gè)進(jìn)程在共享這些Dylib,每個(gè)進(jìn)程都本應(yīng)該有8個(gè)頁(yè)面,也就是一共有16個(gè)臟頁(yè)面。但現(xiàn)在我們只有2個(gè)臟頁(yè)面和1個(gè)干凈的、共享頁(yè)面。

以上講了Dyld如何將Mach-O映射到虛擬內(nèi)存中,下面我們看看安全如何影響Dyld的。
四、安全
有兩點(diǎn)安全問(wèn)題會(huì)影響到Dyld:
-
ASLR地址空間布局隨機(jī)化
這是20年前的舊技術(shù),基本概念是把加載地址隨機(jī)化。 - 代碼簽名
在Xcode中,代碼簽名是指對(duì)整個(gè)文件運(yùn)行一個(gè)加密哈希算法,然后在文件上簽名。為了在運(yùn)行時(shí)進(jìn)行驗(yàn)證,整個(gè)文件都必須要重新讀取。所以在編譯階段,我們讓Mach-O文件的每一個(gè)頁(yè)面都進(jìn)行自己的加密哈希算法,所有哈希都存儲(chǔ)在LINKEDIT里。這使得你的每個(gè)未被修改的頁(yè)面在被讀取的過(guò)程中都能得到及時(shí)驗(yàn)證。
現(xiàn)在我們來(lái)研究從exec()到main()
五、exec()
exec()是一個(gè)系統(tǒng)調(diào)用函數(shù),它用新程序替換當(dāng)前進(jìn)程中的程序。當(dāng)進(jìn)你入內(nèi)核,想把這個(gè)進(jìn)程換成這個(gè)新程序時(shí):
首先內(nèi)核會(huì)抹去整個(gè)地址,映射到你指定的可執(zhí)行程序。ASLR把它映射到一個(gè)隨機(jī)地址。

下一步是從該隨機(jī)地址回溯到零地址,把整個(gè)區(qū)域標(biāo)記為不可訪問(wèn),意思是指不可讀、不可寫(xiě)、不可執(zhí)行。該區(qū)域在32位處理器下至少4KB大小,64位處理器下至少4GB大小。這樣可以捕捉任何空指針引用,捕捉任何指針截?cái)唷?/p>

六、關(guān)于Dylibs
Unix誕生的前幾十年,一切都很簡(jiǎn)單,我只需映射一個(gè)程序,把指針引用指向它,開(kāi)始運(yùn)行它即可。然后共享庫(kù)被發(fā)明出來(lái)。那么誰(shuí)來(lái)加載Dylibs呢?人們很快意識(shí)到情況太過(guò)復(fù)雜,不想讓內(nèi)核做這件事。所以人們新建了幫助程序,在我們的平臺(tái)上叫作Dyld,在其他Unix平臺(tái)又叫作LD.SO。
因此當(dāng)內(nèi)核完成進(jìn)程的映射時(shí),它現(xiàn)在將另一個(gè)名為Dyld的Mach-O文件映射到另一個(gè)隨機(jī)地址的進(jìn)程中。把PC指向Dyld,讓Dyld完成進(jìn)程的啟動(dòng)?,F(xiàn)在Dyld在運(yùn)行進(jìn)程,它的工作是加載所有依賴的動(dòng)態(tài)庫(kù),讓它們完全準(zhǔn)備好開(kāi)始運(yùn)行。

七、Dyld步驟
讓我們來(lái)瀏覽這些步驟,底部有很多步驟和一個(gè)時(shí)間線,我們?yōu)g覽這些的時(shí)候,也會(huì)瀏覽時(shí)間線。
- Map all dependent dylibs, recurse Rebase all images
- Bind all images
- ObjC prepare images
- Run initializers

(一) 加載動(dòng)態(tài)庫(kù)
首先Dyld是否需要映射所有依賴的動(dòng)態(tài)庫(kù)?什么是依賴的動(dòng)態(tài)庫(kù)?
要找到它們,首先要讀取內(nèi)核中已經(jīng)映射好的主可執(zhí)行文件的頭部,在該頭文件中是一個(gè)所有依賴庫(kù)的列表。因此必須將其解析出來(lái)。所以必須要找到每一個(gè)動(dòng)態(tài)庫(kù)。一旦找到每個(gè)動(dòng)態(tài)庫(kù),必須打開(kāi)并運(yùn)行每個(gè)文件的開(kāi)頭,需要確保是這是一個(gè)Mach-O文件,對(duì)它進(jìn)行驗(yàn)證,找到它的編碼簽名,將這個(gè)編碼簽名注冊(cè)到內(nèi)核中。
然后它可以在這個(gè)動(dòng)態(tài)庫(kù)中的每一段調(diào)用mmap()函數(shù)

總結(jié):
- 解析依賴的動(dòng)態(tài)庫(kù)列表;
- 找到必須的`Mach-O`文件;
- 打開(kāi)并讀取文件的開(kāi)頭;
- 驗(yàn)證`Mach-O`文件;
- 注冊(cè)代碼簽名;
- 為每一段調(diào)用`mmap()`函數(shù);
(二) 遞歸加載
假如你的應(yīng)用依賴A.dylib和B.dylib兩個(gè)動(dòng)態(tài)庫(kù),而A.dylib和B.dylib自身也可能依賴其他dylib。所以Dyld必須為每一個(gè)dylib再做一次同樣的事,而每個(gè)dylib可能依賴于已經(jīng)加載的東西或新的東西,所以Dyld必須確定它是否已經(jīng)被加載,如果沒(méi)有被加載,Dyld需要加載它。所以如此繼續(xù)這種操作,最終所有依賴的都被加載了。
通常一個(gè)系統(tǒng)里的普通進(jìn)程,都會(huì)加載1至400個(gè)動(dòng)態(tài)庫(kù),這個(gè)加載數(shù)量很大。還好這些動(dòng)態(tài)庫(kù)大部分都是OS庫(kù),OS系統(tǒng)在構(gòu)建時(shí),會(huì)預(yù)計(jì)算和預(yù)緩存那些Dyld加載內(nèi)容所要做的工作。所以O(shè)S庫(kù)加載很快。

現(xiàn)在所有的動(dòng)態(tài)庫(kù)都已經(jīng)加載完成,但是它們都彼此獨(dú)立,我們必須要把它們捆綁在一起,這就是所謂的修復(fù)(fix-ups)。
(三) 修復(fù)(fix-ups)
關(guān)于修復(fù),有一點(diǎn)我們已經(jīng)知道,由于代碼簽名的存在我們無(wú)法修改指令。那么如果不能修改它調(diào)用的指令,動(dòng)態(tài)庫(kù)如何調(diào)用另一個(gè)動(dòng)態(tài)庫(kù)呢?這又用到了間接引用的技術(shù)。
所以我們的code-gen稱為動(dòng)態(tài)PIC,即地址無(wú)關(guān)代碼。這意味著代碼可以動(dòng)態(tài)地加載到該地址,也就是說(shuō)地址間接地被分配。這所意味的是為了讓一個(gè)調(diào)用另一個(gè),code-gen實(shí)際上在DATA段里新建一個(gè)指針,并且該指針指向了我們想調(diào)用的位置
。代碼加載該指針,并且跳向該指針。所以所有的Dyld都在修復(fù)指針和數(shù)據(jù)。

現(xiàn)在主要有兩種修復(fù),重設(shè)基址和綁定。它們的區(qū)別是什么呢?
-
重設(shè)基址:是指如果有一個(gè)指針指向圖像范圍內(nèi),需要做出的所有的修改。 -
綁定:是指如果指針指向圖像范圍外,他們必須進(jìn)行不同的修復(fù)。

下面我們一起看看其步驟:
我們可以在任何二進(jìn)制文件上運(yùn)行dyldinfo指令,就可以看到dyld必須為該二進(jìn)制文件做的所有修復(fù)工作。
[~]> xcrun dyldinfo -rebase -bind -lazy_bind DongDong.app/DongDong
for arch armv7:
rebase information (from compressed dyld info):
segment section address type value
__DATA __nl_symbol_ptr 0x002F800C pointer 0x002FC9E0
__DATA __nl_symbol_ptr 0x002F8010 pointer 0x002FC458
__DATA __nl_symbol_ptr 0x002F8014 pointer 0x002FEFE8
__DATA __nl_symbol_ptr 0x002F8018 pointer 0x002EDB00
__DATA __nl_symbol_ptr 0x002F8050 pointer 0x00322A6C
__DATA __nl_symbol_ptr 0x002F8054 pointer 0x002FC878
......
bind information:
segment section address type addend dylib symbol
__DATA __nl_symbol_ptr 0x002F833C pointer 0 Alamofire _$s9Alamofire12JSONEncodingVAA17ParameterEncodingAAWP
__DATA __nl_symbol_ptr 0x002F8340 pointer 0 Alamofire _$s9Alamofire12JSONEncodingVN
__DATA __objc_classrefs 0x0031C4C8 pointer 0 CFNetwork _OBJC_CLASS_$_NSHTTPURLResponse
__DATA __objc_classrefs 0x0031C4B4 pointer 0 CFNetwork _OBJC_CLASS_$_NSMutableURLRequest
__DATA __objc_classrefs 0x0031C57C pointer 0 CFNetwork _OBJC_CLASS_$_NSURLConnection
__DATA __objc_classrefs 0x0031C4B0 pointer 0 CFNetwork _OBJC_CLASS_$_NSURLSession (weak import)
......
lazy binding information (from lazy_bind part of dyld info):
segment section address index dylib symbol
__DATA __la_symbol_ptr 0x002F8574 0x0000 libswiftFoundation _$s10Foundation10URLRequestV19_bridgeToObjectiveCSo12NSURLRequestCyF
__DATA __la_symbol_ptr 0x002F8578 0x004D libswiftFoundation _$s10Foundation10URLRequestV3url11cachePolicy15timeoutIntervalAcA3URLV_So017NSURLRequestCacheE0VSdtcfC
__DATA __la_symbol_ptr 0x002F857C 0x00BC libswiftFoundation _$s10Foundation10URLRequestVMa
__DATA __la_symbol_ptr 0x002F8580 0x00E3 libswiftFoundation _$s10Foundation12CharacterSetV11whitespacesACvgZ
__DATA __la_symbol_ptr 0x002F8584 0x011C libswiftFoundation _$s10Foundation12CharacterSetVMa
__DATA __la_symbol_ptr 0x002F8588 0x0145 libswiftFoundation _$s10Foundation17NSLocalizedString_9tableName6bundle5value7commentS2S_SSSgSo8NSBundleCS2StF
......
(四) 重設(shè)基址
在過(guò)去你可以為每一個(gè)dylib指定首選加載地址,該首選加載地址是一個(gè)靜態(tài)鏈接器,和Dyld一起工作。這樣,若把它加載到該首選加載地址,則所有本應(yīng)該在內(nèi)部編碼的指針和數(shù)據(jù)都是正確的,那么Dyld就不用做任何修復(fù)。但是現(xiàn)在,因?yàn)橛辛?code>ASLR,dylib被加載到隨機(jī)地址上。
它被滑動(dòng)到其他地址,也就是說(shuō)所有那些指針和數(shù)據(jù)都還依然指向舊地址。所以為了修復(fù)它們,我們需要計(jì)算滑動(dòng)值,也就是移動(dòng)距離,并且將該滑動(dòng)值添加到每一個(gè)內(nèi)部指針上。
因此重設(shè)基址是指遍歷所有內(nèi)部數(shù)據(jù)指針,然后為它們添加一個(gè)滑動(dòng)值。所以這個(gè)概念很簡(jiǎn)單,讀、添加、寫(xiě),讀、添加、寫(xiě)。但是這些數(shù)據(jù)指針在哪里呢?這些指針在段中的位置都編碼在LINKEDIT段里。此時(shí),所有映射都已經(jīng)結(jié)束,當(dāng)我們開(kāi)始重設(shè)基址時(shí),實(shí)際上在所有DATA頁(yè)面都產(chǎn)生了頁(yè)面錯(cuò)誤。然后對(duì)頁(yè)面進(jìn)行修改時(shí),產(chǎn)生寫(xiě)入時(shí)復(fù)制。
由于所有的這些IO操作,重設(shè)基址有時(shí)會(huì)非常昂貴。但是有一個(gè)技巧,就是按順序操作,從內(nèi)核的角度來(lái)看,它認(rèn)為數(shù)據(jù)錯(cuò)誤是按順序產(chǎn)生的。當(dāng)它如此認(rèn)為時(shí),內(nèi)核會(huì)進(jìn)行預(yù)讀,這樣I/O成本會(huì)降低很多。

下面我們來(lái)看另一種修復(fù)---綁定
(五) 綁定
綁定是針對(duì)那些指向動(dòng)態(tài)庫(kù)范圍外的指針而言的。這些指針通過(guò)名稱進(jìn)行綁定,實(shí)際上都是字符串。本例中,LINKEDIT段里的malloc,也就是說(shuō)該數(shù)據(jù)指針需要指向malloc。所以運(yùn)行時(shí),dylib需要找到實(shí)現(xiàn)該符號(hào)的位置,這需要很多的計(jì)算,遍歷查找符號(hào)表。一旦找到,就把值存儲(chǔ)到該數(shù)據(jù)指針中。所以這種方式的計(jì)算復(fù)雜度要比重設(shè)基址高很多。但是I/O很少,因?yàn)橹卦O(shè)基址已經(jīng)完成大部分的I/O。

(六) 通知ObjC運(yùn)行時(shí)
ObjC有很多DATA結(jié)構(gòu),DATA結(jié)構(gòu)類也就是指向其方法的指針,以及super gloss的指針等等。幾乎所有這些都通過(guò)重設(shè)基址或綁定被修復(fù)。但在ObjC運(yùn)行時(shí)還需要一些額外的操作。首先ObjC是一門(mén)動(dòng)態(tài)語(yǔ)言,可以把一個(gè)類用名稱實(shí)例化。即ObjC在運(yùn)行時(shí),必須要維護(hù)一張表格,這張表中包含了其映射類的所有名稱。每次加載的名稱都將定義一個(gè)類,并將該類的名稱注冊(cè)到一個(gè)全局的表中。接下來(lái),在C++中,你們可能聽(tīng)說(shuō)過(guò)關(guān)于脆弱的ivar問(wèn)題。
在ObjC中不存在脆弱的基類問(wèn)題,因?yàn)槲覀冏龅钠渲幸环N修復(fù)就是,在加載時(shí)動(dòng)態(tài)地改變所有ivar的偏移值。在ObjC里,我們可以定義改變另一個(gè)類中方法的分類。有時(shí)這些分類在一些類中,而這些類不在另一個(gè)動(dòng)態(tài)庫(kù)的圖像中,此刻應(yīng)用那些方法修復(fù)。最后,ObjC基于選擇器是唯一的,所以我們需要唯一的選擇器。

(七) 初始化器
現(xiàn)在我們完成了所有的DATA修復(fù),現(xiàn)在我們可以進(jìn)行所有可以靜態(tài)描述的DATA修復(fù)。現(xiàn)在是進(jìn)行動(dòng)態(tài)DATA修復(fù)的時(shí)機(jī)。
在C++里,有一個(gè)初始化器,可以指定等于任何你想要的表達(dá)式。那個(gè)任意的表達(dá)式此時(shí)需要運(yùn)行,現(xiàn)在就運(yùn)行了。因此,C++編譯器生成初始化器來(lái)完成那些任意DATA的初始化。
在ObjC有一種方法,叫+load方法,現(xiàn)在+load方法已經(jīng)被否決,不建議使用。建議使用+initialize方法。若有+load方法,此時(shí)它開(kāi)始運(yùn)行。
看下面的這張圖,頂端是主可執(zhí)行文件,所有的動(dòng)態(tài)庫(kù)都依照這張圖,必須要運(yùn)行初始化器。按什么順序運(yùn)行呢?我們選擇從下往上,原因在于當(dāng)初始化器運(yùn)行時(shí),可能會(huì)調(diào)用一些動(dòng)態(tài)庫(kù),你需要確保那些動(dòng)態(tài)庫(kù)已經(jīng)準(zhǔn)備好被調(diào)用。所以從下開(kāi)始運(yùn)行初始化器,一直向上到應(yīng)用類,可以很安全地調(diào)用依賴的內(nèi)容。所以一旦所有初始化器完成時(shí),現(xiàn)在我們終于可以調(diào)用主Dyld程序了。

八、main()函數(shù)之前發(fā)生了什么
通過(guò)上面的知識(shí),我們了解了進(jìn)程是如何啟動(dòng)的,知道了Dyld是一個(gè)幫助程序。
- 加載所有的依賴庫(kù);
- 修復(fù)
DATA頁(yè)面的所有指針; - 運(yùn)行所有的初始化器;
- 跳到主函數(shù);
理論部分到此結(jié)束,那么如何把這些理論應(yīng)用到實(shí)際中呢?
請(qǐng)閱讀下一章的實(shí)戰(zhàn)內(nèi)容:App啟動(dòng)優(yōu)化 --- 實(shí)踐部分