本文分為理論【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)庫【Dylib】:它是一個(gè)動(dòng)態(tài)庫几苍,在其他平臺(tái)上又稱為DSO或DLL檬贰,
- 捆綁包【Bundle】:它是一種特殊的動(dòng)態(tài)庫攒磨,無法進(jìn)行鏈接,只能在運(yùn)行時(shí)使用
dlopen()
函數(shù)打開它戈鲁。Mac OS的插件會(huì)用到。
圖像【Image】:它是指可執(zhí)行文件嘹叫,動(dòng)態(tài)庫或捆綁包的任意一種類型;
框架【Framework】:它是一種帶有資源和標(biāo)頭目錄的動(dòng)態(tài)庫诈乒,存儲(chǔ)該動(dòng)態(tài)庫需要的文件罩扇。
函數(shù)定義:
void * dlopen( const char * pathname, int mode );
函數(shù)描述:
dlopen函數(shù)以指定模式打開指定的動(dòng)態(tài)連接庫文件,并返回一個(gè)句柄給調(diào)用進(jìn)程怕磨。使用dlclose()來卸載打開的庫喂饥。
mode:分為這兩種
RTLD_LAZY 暫緩決定,等有需要時(shí)再解出符號(hào)
RTLD_NOW 立即決定肠鲫,返回前解除所有未決定的符號(hào)员帮。
RTLD_LOCAL
RTLD_GLOBAL 允許導(dǎo)出符號(hào)
RTLD_GROUP
RTLD_WORLD
返回值:
打開錯(cuò)誤返回NULL
成功,返回庫引用
編譯時(shí)候要加入 -ldl (指定dl庫)
(二)Mach-O
圖像格式
- 段
-
Mach-O
圖像被分成數(shù)段导饲; - 所有的段名都由大寫字母組成捞高;
- 每一段都是頁面大小的倍數(shù),而頁面大小由硬件決定渣锦,
arm64
處理器的頁面大小是16KB硝岗,其他都是4KB;
下例中TEXT
段大小是3頁袋毙,DATA
和LINKEDIT
段大小都是1頁型檀。
最常見的段名是TEXT
,DATA
和LINKEDIT
听盖。實(shí)際上幾乎每一個(gè)二進(jìn)制文件都包含這三段胀溺,你可以添加自定義段,但一般不會(huì)給它賦值皆看。
TEXT
仓坞,DATA
和LINKEDIT
段的作用:
-
TEXT
:它是文件的開頭,包含了Mach的頭文件悬蔽,任何機(jī)器指令以及任何只讀常量扯躺,比如C字符串。 -
DATA
:它是重寫段蝎困,它包含了所有的全局變量录语。 -
LINKEDIT
:它不包含全局變量的函數(shù),它包含變量函數(shù)信息禾乘,比如名稱和地址
- 分區(qū)
- 分區(qū)是段的子范圍澎埠;
- 分區(qū)不用遵循頁面的大小始藕;
- 分區(qū)的名稱都用小寫字母表示蒲稳;
(三)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è)頁面的大小觉至。
通過上面我們知道,Mach-O
圖像的每段都是頁面大小的倍數(shù)睡腿,而且頭文件也需要一個(gè)頁面的大小语御,這樣會(huì)浪費(fèi)很多空間。那為何還要這樣做呢席怪?這就涉及到虛擬內(nèi)存沃暗。
二、虛擬內(nèi)存
在軟件工程里有句格言何恶,任何問題都可以通過添加一個(gè)間接層加以解決孽锥。而虛擬內(nèi)存所解決的問題就是,所有這些進(jìn)程存在時(shí)該如何管理所有的物理內(nèi)存细层。為了解決這個(gè)問題惜辑,添加了一個(gè)小的間接層,每個(gè)進(jìn)程都是一個(gè)邏輯地址空間疫赎,映射到RAM的某個(gè)物理頁面盛撑,這種映射不一定是一對(duì)一的。邏輯地址可以不對(duì)應(yīng)任何物理RAM捧搞,也可以多個(gè)邏輯地址對(duì)應(yīng)同一個(gè)物理RAM抵卫,這樣帶來很多中可能。那能利用虛擬內(nèi)存做什么呢胎撇?
首先如果有一個(gè)邏輯地址不映射任何物理RAM介粘,當(dāng)進(jìn)程要訪問該地址時(shí)就會(huì)產(chǎn)生頁面錯(cuò)誤,內(nèi)核將停止該線程晚树,并試圖找出解決方案姻采。
下一點(diǎn)是如果有兩個(gè)進(jìn)程,對(duì)應(yīng)兩個(gè)邏輯地址爵憎,這兩個(gè)邏輯地址映射同一個(gè)物理頁面慨亲,這兩個(gè)進(jìn)程共享相同的RAM位婚瓜,進(jìn)程之間開始共享。
另一個(gè)有趣的功能是基于文件的映射刑棵,不用把整個(gè)文件讀入RAM巴刻,而是可以調(diào)用mmap()
函數(shù)告訴虛擬內(nèi)存系統(tǒng),我想把這部分文件映射到進(jìn)程里的這段地址蛉签。這么做的原因是冈涧,不用讀取整個(gè)文件,通過設(shè)置該映射正蛙,第一次訪問這些不同的地址時(shí),如同已經(jīng)在內(nèi)存里讀過营曼,每次訪問未訪問過的地址時(shí)乒验,都會(huì)導(dǎo)致頁面錯(cuò)誤,內(nèi)核會(huì)讀該錯(cuò)誤頁面蒂阱。這樣將會(huì)造成讀取文件遲緩锻全。
現(xiàn)在我們結(jié)合前面講的關(guān)于Mach-O
的內(nèi)容,可以知道任何Dylib
和圖像的TEXT
段都可以映射到多個(gè)進(jìn)程录煤,這將會(huì)造成讀取遲緩鳄厌,而這些頁面可以在進(jìn)程間共享。那么DATA
段呢妈踊?
DATA
用來讀寫了嚎,有一個(gè)策略叫寫入時(shí)復(fù)制,這和Apple文件系統(tǒng)的克隆很相似廊营。寫入時(shí)復(fù)制所做的就是它積極地在所有進(jìn)程里共享DATA
頁面歪泳。一個(gè)進(jìn)程會(huì)發(fā)生什么,只要它們只是從共享內(nèi)容的全局變量中讀取就可以了露筒。但是一旦有進(jìn)程想要寫入其他DATA
頁面呐伞,就會(huì)發(fā)生寫入時(shí)復(fù)制。
寫入時(shí)復(fù)制使內(nèi)核把該頁面復(fù)制到另一個(gè)物理RAM中慎式,并將映射重定向到該頁面伶氢。所以該進(jìn)程有了該頁面的副本。這會(huì)給我們帶來臟頁面和凈頁面瘪吏,而副本被認(rèn)為是臟頁面癣防。臟頁面是指含有進(jìn)程的特定信息。凈頁面是指內(nèi)核可以按照需要重新建立的頁面掌眠,比如重新讀取磁盤劣砍。所以臟頁面比凈頁面要昂貴很多。
最后一點(diǎn)是頁面也有權(quán)限界限扇救,這指的是可以標(biāo)記一個(gè)頁面可讀刑枝、可寫或可執(zhí)行香嗓、或者它們的任意組合。
虛擬內(nèi)存的作用:
1. 虛擬內(nèi)存是間接層
2. 將每個(gè)進(jìn)程的地址映射到物理RAM(頁面粒度)
虛擬內(nèi)存的特征:
1. 頁面錯(cuò)誤
2. 相同的RAM頁面出現(xiàn)在多個(gè)進(jìn)程中
3. 文件支持的頁面
3.1 mmap()
3.2 懶讀取
4. 寫入時(shí)復(fù)制(COW)
5. 臟頁面與干凈頁面
6. 權(quán)限:rwx
以上就是Mach-O
格式和虛擬內(nèi)存的內(nèi)容∽俺現(xiàn)在看看它們是如何一起工作的靠娱,在之前,我們先看看Dyld(全稱the dynamic link editor掠兄,即動(dòng)態(tài)鏈接器像云,其本質(zhì)Mach-O文件,專門用來加載動(dòng)態(tài)庫的庫)
是如何操作的蚂夕,它在Mach-O
和虛擬內(nèi)存之間是如何映射的迅诬。
三、Dyld
的操作過程
現(xiàn)有一個(gè)Dylib
文件婿牍,如下圖所示:
我們沒有把它讀到內(nèi)存中侈贷,而是把它映射到內(nèi)存,所以在內(nèi)存里該Dylib
文件本應(yīng)該占用8個(gè)頁面等脂∏温可以看到,不同的是有這些“全零填充”上遥。
大部分全局變量的初始值都是零搏屑,所以靜態(tài)鏈接器進(jìn)行了優(yōu)化,把所有值為0的全局變量都移到了尾端粉楚,然后不占用任何磁盤空間辣恋。取而代之,我們利用虛擬內(nèi)存的特性模软,在該頁面第一次被訪問時(shí)展运,告訴虛擬內(nèi)存把它填滿0蛤售。所以它不需要讀取。Dyld
必須要做的第一件事是在內(nèi)存中查看該進(jìn)程的Mach
頭文件。它將查看內(nèi)存的頂盒友存,此時(shí)那里是空的设拟,沒有內(nèi)容映射到物理頁面上祭衩,所以產(chǎn)生頁面錯(cuò)誤棍郎。到那時(shí)內(nèi)核意識(shí)到它被映射到了一個(gè)文件,所以它將讀取文件的第一頁鲫剿,將其放入物理RAM鳄逾,設(shè)置其映射。
現(xiàn)在Dyld
可以真正通過Mach
頭文件開始讀取灵莲。它通過讀取Mach
頭文件雕凹,Mach
頭文件讓Dyld
到LINKEDIT
段上查看這條信息。再一次,Dyld
跳下去查看進(jìn)程1的底盒枚抵。這又會(huì)產(chǎn)生頁面錯(cuò)誤线欲,內(nèi)核又讀入RAM的另一個(gè)LINKEDIT
的物理頁面。
Dyld
現(xiàn)在可以期望一個(gè)LINKEDIT
汽摹。此刻在進(jìn)程中李丰,LINKEDIT
將會(huì)告訴Dyld
對(duì)DATA
頁面做一些修正,讓Dylib
可運(yùn)行逼泣。所以同樣的事情又發(fā)生了趴泌,Dyld
現(xiàn)從DATA
頁面讀取數(shù)據(jù),但是有一點(diǎn)不同拉庶,Dyld
想要寫回一些內(nèi)容修改DATA
頁面嗜憔,此刻寫入時(shí)復(fù)制出現(xiàn)了。這個(gè)頁面變成了臟頁面氏仗。所以臟RAM的8個(gè)頁面將會(huì)是什么吉捶?若我只用malloc()
函數(shù)分配8頁內(nèi)存,然后讀了一些內(nèi)容進(jìn)去廓鞠,我將會(huì)有8個(gè)頁面的臟RAM。但現(xiàn)在我只有1頁的臟RAM和2個(gè)凈頁面谣旁。
如果第二個(gè)進(jìn)程加載同一個(gè)Dylib
將會(huì)發(fā)生什么床佳?在第二個(gè)進(jìn)程里,Dyld
會(huì)經(jīng)歷相同的步驟榄审,首先它查看Mach
頭文件砌们,但內(nèi)核在RAM某處已經(jīng)有這頁了,所以內(nèi)核只是簡(jiǎn)單地把映射重定向搁进,重利用該頁面浪感,并沒有任何IO操作。LINKEDIT
也是如此饼问,更加快速影兽。我們來看DATA
頁面,此時(shí)內(nèi)核必須要看看在DATA
頁面莱革,干凈的副本是否還存在RAM其他地方峻堰,如果還在,就可以重利用盅视;如果不在捐名,就必須要重新讀取。
在該進(jìn)程中闹击,Dyld
會(huì)讓RAM變臟镶蹋。
最后一步是LINKEDIT
,只在Dyld
進(jìn)行操作時(shí)被需要。所以它可以提醒內(nèi)核,當(dāng)它完成時(shí)贺归,它不再需要這些LINKEDIT
頁面淆两,當(dāng)別人需要RAM時(shí),可以回收它們∧恋現(xiàn)在有兩個(gè)進(jìn)程在共享這些Dylib
琼腔,每個(gè)進(jìn)程都本應(yīng)該有8個(gè)頁面,也就是一共有16個(gè)臟頁面踱葛。但現(xiàn)在我們只有2個(gè)臟頁面和1個(gè)干凈的丹莲、共享頁面。
以上講了Dyld
如何將Mach-O
映射到虛擬內(nèi)存中尸诽,下面我們看看安全如何影響Dyld
的甥材。
四、安全
有兩點(diǎ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è)頁面都進(jìn)行自己的加密哈希算法,所有哈希都存儲(chǔ)在LINKEDIT
里格郁。這使得你的每個(gè)未被修改的頁面在被讀取的過程中都能得到及時(shí)驗(yàn)證腹殿。
現(xiàn)在我們來研究從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)記為不可訪問暂幼,意思是指不可讀、不可寫移迫、不可執(zhí)行旺嬉。該區(qū)域在32位處理器下至少4KB大小,64位處理器下至少4GB大小厨埋。這樣可以捕捉任何空指針引用邪媳,捕捉任何指針截?cái)唷?/p>
六、關(guān)于Dylibs
Unix誕生的前幾十年,一切都很簡(jiǎn)單雨效,我只需映射一個(gè)程序迅涮,把指針引用指向它,開始運(yùn)行它即可徽龟。然后共享庫被發(fā)明出來叮姑。那么誰來加載Dylibs
呢?人們很快意識(shí)到情況太過復(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)。現(xiàn)在Dyld
在運(yùn)行進(jìn)程骇径,它的工作是加載所有依賴的動(dòng)態(tài)庫躯肌,讓它們完全準(zhǔn)備好開始運(yùn)行。
七破衔、Dyld
步驟
讓我們來瀏覽這些步驟清女,底部有很多步驟和一個(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)庫
首先Dyld
是否需要映射所有依賴的動(dòng)態(tài)庫忠售?什么是依賴的動(dòng)態(tài)庫传惠?
要找到它們,首先要讀取內(nèi)核中已經(jīng)映射好的主可執(zhí)行文件的頭部稻扬,在該頭文件中是一個(gè)所有依賴庫的列表卦方。因此必須將其解析出來。所以必須要找到每一個(gè)動(dòng)態(tài)庫泰佳。一旦找到每個(gè)動(dòng)態(tài)庫盼砍,必須打開并運(yùn)行每個(gè)文件的開頭,需要確保是這是一個(gè)Mach-O
文件逝她,對(duì)它進(jìn)行驗(yàn)證浇坐,找到它的編碼簽名,將這個(gè)編碼簽名注冊(cè)到內(nèi)核中黔宛。
然后它可以在這個(gè)動(dòng)態(tài)庫中的每一段調(diào)用mmap()
函數(shù)
總結(jié):
- 解析依賴的動(dòng)態(tài)庫列表近刘;
- 找到必須的`Mach-O`文件;
- 打開并讀取文件的開頭;
- 驗(yàn)證`Mach-O`文件觉渴;
- 注冊(cè)代碼簽名介劫;
- 為每一段調(diào)用`mmap()`函數(shù);
(二) 遞歸加載
假如你的應(yīng)用依賴A.dylib
和B.dylib
兩個(gè)動(dòng)態(tài)庫案淋,而A.dylib
和B.dylib
自身也可能依賴其他dylib
座韵。所以Dyld
必須為每一個(gè)dylib
再做一次同樣的事,而每個(gè)dylib
可能依賴于已經(jīng)加載的東西或新的東西踢京,所以Dyld
必須確定它是否已經(jīng)被加載誉碴,如果沒有被加載,Dyld
需要加載它漱挚。所以如此繼續(xù)這種操作翔烁,最終所有依賴的都被加載了。
通常一個(gè)系統(tǒng)里的普通進(jìn)程旨涝,都會(huì)加載1至400個(gè)動(dòng)態(tài)庫蹬屹,這個(gè)加載數(shù)量很大。還好這些動(dòng)態(tài)庫大部分都是OS庫白华,OS系統(tǒng)在構(gòu)建時(shí)慨默,會(huì)預(yù)計(jì)算和預(yù)緩存那些Dyld
加載內(nèi)容所要做的工作。所以O(shè)S庫加載很快弧腥。
現(xiàn)在所有的動(dòng)態(tài)庫都已經(jīng)加載完成厦取,但是它們都彼此獨(dú)立,我們必須要把它們捆綁在一起管搪,這就是所謂的修復(fù)(fix-ups)
虾攻。
(三) 修復(fù)(fix-ups)
關(guān)于修復(fù),有一點(diǎn)我們已經(jīng)知道更鲁,由于代碼簽名的存在我們無法修改指令霎箍。那么如果不能修改它調(diào)用的指令,動(dòng)態(tài)庫如何調(diào)用另一個(gè)動(dòng)態(tài)庫呢澡为?這又用到了間接引用的技術(shù)漂坏。
所以我們的code-gen
稱為動(dòng)態(tài)PIC
,即地址無關(guān)代碼媒至。這意味著代碼可以動(dòng)態(tài)地加載到該地址顶别,也就是說地址間接地被分配。這所意味的是為了讓一個(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è)基址
在過去你可以為每一個(gè)dylib
指定首選加載地址,該首選加載地址是一個(gè)靜態(tài)鏈接器是辕,和Dyld
一起工作囤热。這樣,若把它加載到該首選加載地址获三,則所有本應(yīng)該在內(nèi)部編碼的指針和數(shù)據(jù)都是正確的旁蔼,那么Dyld
就不用做任何修復(fù)。但是現(xiàn)在疙教,因?yàn)橛辛?code>ASLR棺聊,dylib
被加載到隨機(jī)地址上。
它被滑動(dòng)到其他地址贞谓,也就是說所有那些指針和數(shù)據(jù)都還依然指向舊地址限佩。所以為了修復(fù)它們,我們需要計(jì)算滑動(dòng)值裸弦,也就是移動(dòng)距離祟同,并且將該滑動(dòng)值添加到每一個(gè)內(nèi)部指針上。
因此重設(shè)基址是指遍歷所有內(nèi)部數(shù)據(jù)指針理疙,然后為它們添加一個(gè)滑動(dòng)值晕城。所以這個(gè)概念很簡(jiǎn)單,讀沪斟、添加广辰、寫暇矫,讀主之、添加、寫李根。但是這些數(shù)據(jù)指針在哪里呢槽奕?這些指針在段中的位置都編碼在LINKEDIT
段里。此時(shí)房轿,所有映射都已經(jīng)結(jié)束粤攒,當(dāng)我們開始重設(shè)基址時(shí)所森,實(shí)際上在所有DATA
頁面都產(chǎn)生了頁面錯(cuò)誤。然后對(duì)頁面進(jìn)行修改時(shí)夯接,產(chǎn)生寫入時(shí)復(fù)制焕济。
由于所有的這些IO操作,重設(shè)基址有時(shí)會(huì)非常昂貴盔几。但是有一個(gè)技巧晴弃,就是按順序操作,從內(nèi)核的角度來看逊拍,它認(rèn)為數(shù)據(jù)錯(cuò)誤是按順序產(chǎn)生的上鞠。當(dāng)它如此認(rèn)為時(shí),內(nèi)核會(huì)進(jìn)行預(yù)讀芯丧,這樣I/O
成本會(huì)降低很多芍阎。
下面我們來看另一種修復(fù)---綁定
(五) 綁定
綁定是針對(duì)那些指向動(dòng)態(tài)庫范圍外的指針而言的。這些指針通過名稱進(jìn)行綁定缨恒,實(shí)際上都是字符串谴咸。本例中,LINKEDIT
段里的malloc
骗露,也就是說該數(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
的指針等等惭婿。幾乎所有這些都通過重設(shè)基址或綁定被修復(fù)。但在ObjC
運(yùn)行時(shí)還需要一些額外的操作叶雹。首先ObjC
是一門動(dòng)態(tài)語言财饥,可以把一個(gè)類用名稱實(shí)例化。即ObjC
在運(yùn)行時(shí)折晦,必須要維護(hù)一張表格钥星,這張表中包含了其映射類的所有名稱。每次加載的名稱都將定義一個(gè)類满着,并將該類的名稱注冊(cè)到一個(gè)全局的表中谦炒。接下來贯莺,在C++中,你們可能聽說過關(guān)于脆弱的ivar問題宁改。
在ObjC
中不存在脆弱的基類問題缕探,因?yàn)槲覀冏龅钠渲幸环N修復(fù)就是,在加載時(shí)動(dòng)態(tài)地改變所有ivar的偏移值还蹲。在ObjC
里撕蔼,我們可以定義改變另一個(gè)類中方法的分類。有時(shí)這些分類在一些類中秽誊,而這些類不在另一個(gè)動(dòng)態(tài)庫的圖像中鲸沮,此刻應(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++編譯器生成初始化器來完成那些任意DATA的初始化鄙早。
在ObjC
有一種方法汪茧,叫+load
方法,現(xiàn)在+load
方法已經(jīng)被否決限番,不建議使用舱污。建議使用+initialize
方法。若有+load
方法弥虐,此時(shí)它開始運(yùn)行扩灯。
看下面的這張圖,頂端是主可執(zhí)行文件霜瘪,所有的動(dòng)態(tài)庫都依照這張圖珠插,必須要運(yùn)行初始化器。按什么順序運(yùn)行呢粥庄?我們選擇從下往上丧失,原因在于當(dāng)初始化器運(yùn)行時(shí)豺妓,可能會(huì)調(diào)用一些動(dòng)態(tài)庫惜互,你需要確保那些動(dòng)態(tài)庫已經(jīng)準(zhǔn)備好被調(diào)用布讹。所以從下開始運(yùn)行初始化器,一直向上到應(yīng)用類训堆,可以很安全地調(diào)用依賴的內(nèi)容描验。所以一旦所有初始化器完成時(shí),現(xiàn)在我們終于可以調(diào)用主Dyld
程序了坑鱼。
八膘流、main()
函數(shù)之前發(fā)生了什么
通過上面的知識(shí),我們了解了進(jìn)程是如何啟動(dòng)的鲁沥,知道了Dyld
是一個(gè)幫助程序呼股。
- 加載所有的依賴庫;
- 修復(fù)
DATA
頁面的所有指針画恰; - 運(yùn)行所有的初始化器彭谁;
- 跳到主函數(shù);
理論部分到此結(jié)束允扇,那么如何把這些理論應(yīng)用到實(shí)際中呢缠局?
請(qǐng)閱讀下一章的實(shí)戰(zhàn)內(nèi)容:App啟動(dòng)優(yōu)化 --- 實(shí)踐部分