dyld簡介
dyld全名The dynamic link editor;是蘋果的
動(dòng)態(tài)鏈接器;是蘋果操作系統(tǒng)的一個(gè)重要組成部分;
在應(yīng)用被編譯打包成可執(zhí)行文件之后(即Mach-O),將其交由
dyld負(fù)責(zé)鏈接,加載程序。dyld貫穿了App啟動(dòng)的過程,包含加載依賴庫、主程序,如果我們需要進(jìn)行性能優(yōu)化、啟動(dòng)優(yōu)化等,不可避免的需要和dyld打交道
且dyld是開源的,我們可以在官網(wǎng)下載它的源碼來閱讀理解
dyld 1.0(1996-2004)
dyld 1包含在NeXTStep 3.3中,在此之前的NeXT使用靜態(tài)二進(jìn)制數(shù)據(jù)。作用并不是很大,dyld 1是在系統(tǒng)廣泛使用C++動(dòng)態(tài)庫之前編寫的,由于C++有許多特性,例如其初始化器的工作,在靜態(tài)環(huán)境工作良好,但是在動(dòng)態(tài)環(huán)境中可能會(huì)降低性能。因此大型的C++動(dòng)態(tài)庫會(huì)導(dǎo)致dyld需要完成大量的工作,速度變慢在發(fā)布
macOS 10.0和Cheetah前,還增加了一個(gè)特性,即Prebinding預(yù)綁定。我們可以使用Prebinding技術(shù)為系統(tǒng)中的所有dylib和應(yīng)用程序找到固定的地址。dyld將會(huì)加載這些地址的所有內(nèi)容。如果加載成功,將會(huì)編輯所有dylib和程序的二進(jìn)制數(shù)據(jù),來獲得所有預(yù)計(jì)算。當(dāng)下次需要將所有數(shù)據(jù)放入相同地址時(shí)就不需要進(jìn)行額外操作了,將大大的提高速度。但是這也意味著每次啟動(dòng)都需要編輯這些二進(jìn)制數(shù)據(jù),至少從安全性來說,這種方式并不友好。
dyld 2(2004-2017)
dyld 2從2004年發(fā)布至今,已經(jīng)經(jīng)過了多個(gè)版本迭代,我們現(xiàn)在常見的一些特性,例如ASLR、Code Sign、share cache等技術(shù),都是在dyld 2中引入的
dyld 2.0(2004-2007)
2004年在
macOS Tiger中推出了dyld 2dyld 2是dyld 1完全重寫的版本,可以正確支持C++初始化器語義,同時(shí)擴(kuò)展了mach-o格式并更新dyld。從而獲得了高效率C++庫的支持。-
dyld 2具有完成的
dlopen和dlsym(主要用于動(dòng)態(tài)加載庫和調(diào)用函數(shù))實(shí)現(xiàn),且具有正確的語義,因此棄用了舊版的APIdlopen:打開一個(gè)庫,獲取句柄dlsym:在打開的庫中查找符號(hào)的值dlclose:關(guān)閉句柄。dlerror:返回一個(gè)描述最后一次調(diào)用dlopen、dlsym,或 dlclose 的錯(cuò)誤信息的字符串。
dyld的設(shè)計(jì)目標(biāo)是提升啟動(dòng)速度。因此僅進(jìn)行有限的健全性檢查。主要是因?yàn)橐郧暗膼阂獬绦虮容^少同時(shí)dyld也有一些安全問題,因此對(duì)一些功能進(jìn)行了改進(jìn),來提高dyld在平臺(tái)上的安全性
由于啟動(dòng)速度的大幅提升,因此我們可以
減少Prebinding的工作量。與編輯程序數(shù)據(jù)的區(qū)別在于,在這里我們僅編輯系統(tǒng)庫,且可以僅在軟件更新時(shí)做這些事情。因此在軟件更新過程中,可能會(huì)看到“優(yōu)化系統(tǒng)性能”類似的文字。這就是在更新時(shí)進(jìn)行Prebinding。現(xiàn)在dyld用于所有優(yōu)化,其用途就是優(yōu)化。因此后面有了dyld 2
dyld 2.x(2007-2017)
- 在2004-20017這幾年間進(jìn)行了大量改進(jìn),dyld 2的性能顯著提高
- 首先,
增加了大量的基礎(chǔ)架構(gòu)和平臺(tái)。- 自從dyld 2在PowerPC發(fā)布之后,增加了
x86、x86_64、arm、arm64和許多的衍生平臺(tái)。 - 還推出了
iOS、tvOS和watchOS,這些都需要新的dyld功能
- 自從dyld 2在PowerPC發(fā)布之后,增加了
- 通過多種方式增加安全性
- 增加
codeSigning代碼簽名、 -
ASLR(Address space layout randomization)地址空間配置隨機(jī)加載:每次加載庫時(shí),可能位于不同的地址 -
bound checking邊界檢查:mach-o文件中增加了Header的邊界檢查功能,從而避免惡意二進(jìn)制數(shù)據(jù)的注入
- 增加
- 增強(qiáng)了性能
- 可以消除Prebinding,用
share cache共享代碼代替
- 可以消除Prebinding,用
ASLR
ASLR是一種防范內(nèi)存損壞漏洞被利用的計(jì)算機(jī)安全技術(shù),ASLR通過隨機(jī)放置進(jìn)程關(guān)鍵數(shù)據(jù)區(qū)域的地址空間來防止攻擊者跳轉(zhuǎn)到內(nèi)存特定位置來利用函數(shù)Linux已在內(nèi)核版本2.6.12中添加ASLR
Apple在
Mac OS X Leopard 10.5(2007年十月發(fā)行)中某些庫導(dǎo)入了隨機(jī)地址偏移,但其實(shí)現(xiàn)并沒有提供ASLR所定義的完整保護(hù)能力。而Mac OS X Lion 10.7則對(duì)所有的應(yīng)用程序均提供了ASLR支持。Apple在
iOS 4.3內(nèi)導(dǎo)入了ASLR。
bounds checking 邊界檢查
- 對(duì)mach-o header中的許多內(nèi)容添加了重要的
邊界檢查功能,從而可以避免惡意二進(jìn)制數(shù)據(jù)的注入
share cache 共享代碼
share cache最早實(shí)在iOS3.1和macOS Snow Leopard中被引入,用于完全取代Prebinding-
share cache是一個(gè)單文件,包含大多數(shù)系統(tǒng)dylib,由于這些dylib合并成了一個(gè)文件,所以可以進(jìn)行優(yōu)化。重新調(diào)整所有
文本段(_TEXT)和數(shù)據(jù)段(_DATA),并重寫整個(gè)符號(hào)表,以此來減小文件的大小,從而在每個(gè)進(jìn)程中僅掛載少量的區(qū)域。允許我們打包二進(jìn)制數(shù)據(jù)段,從而節(jié)省大量的RAM本質(zhì)是一個(gè)
dylib預(yù)鏈接器,它在RAM上的節(jié)約是顯著的,在普通的iOS程序中運(yùn)行可以節(jié)約500-1g內(nèi)存還可以
預(yù)生成數(shù)據(jù)結(jié)構(gòu),用來供dyld和Ob-C在運(yùn)行時(shí)使用。從而不必在程序啟動(dòng)時(shí)做這些事情,這也會(huì)節(jié)約更多的RAM和時(shí)間
share cache在macOS上本地生成,運(yùn)行dyld共享代碼,將大幅優(yōu)化系統(tǒng)性能
dyld 2 工作流程
dyld 2是純粹的in-process,即在程序進(jìn)程內(nèi)執(zhí)行的,也就意味著只有當(dāng)應(yīng)用程序被啟動(dòng)時(shí),dyld 2才能開始執(zhí)行任務(wù)
以下是dyld 2的工作流程圖示

- 1、dyld的初始化,主要代碼在
dyldbootstrap::start,接著執(zhí)行dyld::_main,dyld::_main代碼較多,是dyld加載的核心部分; - 2、檢查并準(zhǔn)備環(huán)境,例如獲取二進(jìn)制路徑、檢查環(huán)境配置,解析主二進(jìn)制的
image header等信息 - 3、實(shí)例化主二進(jìn)制的
image loader,校驗(yàn)主二進(jìn)制和dyld的版本是否匹配 - 4、檢查
share cache是否已經(jīng)map,如果沒有則需要先執(zhí)行map share cache操作 - 5、檢查
DYLD_INSERT_LIBRARIES,如果有則加載插入的動(dòng)態(tài)庫(即實(shí)例化image loader) - 6、執(zhí)行
link操作,會(huì)先遞歸加載依賴的所有動(dòng)態(tài)庫(會(huì)對(duì)依賴庫進(jìn)行排序,被依賴的總是在前面),同時(shí)在這階段將執(zhí)行符號(hào)綁定,以及rebase,binding操作; - 7、執(zhí)行初始化方法,OC的
+load和C的constructor方法都會(huì)在這個(gè)階段執(zhí)行; - 8、讀取
Mach-o的LC_MAIN段獲取程序的入口地址,調(diào)用main函數(shù)
簡化版
① 解析 mach-o 文件,找到其依賴的庫,并且遞歸的找到所有依賴的庫,形成一張動(dòng)態(tài)庫的依賴圖。iOS 上的大部分 app 都依賴幾百個(gè)動(dòng)態(tài)鏈接庫(大部分是系統(tǒng)的動(dòng)態(tài)庫),所以這個(gè)步驟包含了較大的工作量。
② 匹配 mach-o 文件到自身的地址空間
③ 進(jìn)行符號(hào)查找(perform symbol lookups)
④
rebase和binding:由于 app 需要讓地址空間配置隨機(jī)加載,所以所有的指針都需要加上一個(gè)基地址⑤ 運(yùn)行初始化程序,之后運(yùn)行
main()函數(shù)
dyld 3(2017-至今)
dyld 3是2017年WWDC推出的全新的動(dòng)態(tài)鏈接器,它完全改變了動(dòng)態(tài)鏈接的概念,且將成為大多數(shù)macOS系統(tǒng)程序的默認(rèn)設(shè)置。2017 Apple OS平臺(tái)上的所有系統(tǒng)程序都會(huì)默認(rèn)使用dyld 3.dyld 3最早是在2017年的iOS 11中引入,主要用來優(yōu)化系統(tǒng)庫。而在
iOS 13系統(tǒng)中,iOS全面采用新的dyld 3來替代之前的dyld 2,因?yàn)?code>dyld 3完全兼容dyld 2,其API接口也是一樣的,所以,在大部分情況下,開發(fā)者并不需要做額外的適配就能平滑過渡。
為什么需要重新設(shè)計(jì)dyld 2,形成新的dyld 3 ?
重新設(shè)計(jì)dyld,主要從以下幾方面進(jìn)行考慮
性能:想要盡可能的提高啟動(dòng)速度安全性:在dyld 2中增加了安全特性,但是很難跟隨現(xiàn)實(shí)情形,雖然做了很多工作,但是難以實(shí)現(xiàn)這個(gè)目標(biāo)可靠性和可測(cè)試性:為此Apple發(fā)布了很多不錯(cuò)的測(cè)試框架,例如XCTest,但是這些測(cè)試框架依賴于動(dòng)態(tài)鏈接器的底層功能,然后將測(cè)試框架的庫插入進(jìn)程中,所以不能用于測(cè)試現(xiàn)有的dyld代碼,且難以測(cè)試安全性和性能水平
如何將 dyld 2 改進(jìn)和優(yōu)化為 dyld 3?
改進(jìn)和優(yōu)化建議
從上面的dyld 2的工作流程中,我們了解了dyld 2的執(zhí)行流程,可以從以下兩個(gè)方面來改進(jìn)和優(yōu)化:
-
確定
安全敏感的部分Parse mach-o headers解析mach-o 和Find dependencies找到依賴庫,是安全敏感部分,即最大的安全隱患之一;惡意撰改
mach-o頭部,可以進(jìn)行某些攻擊;如果App使用了
@rpaths即搜索路徑,可以通過惡意撰改路徑或者將一些庫插入到特定的位置,來達(dá)到破壞程序的目的;
-
確定
大量占用資源的部分(即可緩存部分)-
Perform symbol lookups符號(hào)查找就是其中一個(gè),因?yàn)樵谝粋€(gè)特定的庫中,除非進(jìn)行軟件更新或者在磁盤上更改庫,不然符號(hào)將始終位于庫中的相同的偏移位置(即符號(hào)偏移量固定);
-
dyld 2 改進(jìn)和優(yōu)化
以下是dyld 2 向 dyld 3 的一些改變,主要是將安全敏感的部分 和 占用大量資源的部分移動(dòng)到上層,然后將一個(gè)closure寫入磁盤進(jìn)行緩存,然后我們?cè)诔绦蜻M(jìn)程中使用closure。以下是圖示

dyld 3 組成部分/工作流程
dyld 3的工作流程主要分為3部分,如下所示

第一部分:out-of-process :mach-o parser
進(jìn)程外的mach-o分析器和編譯器,是普通的后臺(tái)程序,用于提高測(cè)試基礎(chǔ)架構(gòu)的性能。
第一部分主要在App進(jìn)程之外做以下工作:
解析所有搜索路徑
@rpath、環(huán)境變量,因?yàn)樗鼈儠?huì)影響啟動(dòng)速度分析
mach-o二進(jìn)制數(shù)據(jù)執(zhí)行
符號(hào)查找利用這些結(jié)果創(chuàng)建
launch clourse
第二部分:in-process :engine
進(jìn)程內(nèi)的引擎,這部分常駐在內(nèi)存中,且在dyld 3不再需要分析mach-o文件頭或者執(zhí)行符號(hào)查找就可以啟動(dòng)應(yīng)用,因?yàn)榉治鰉ach-o和執(zhí)行符號(hào)查找都是耗時(shí)操作,所以極大的提高了程序啟動(dòng)速度。
第二部分主要在App進(jìn)程中做以下工作:
檢查
launch closure是否正確映射到
dylib中,再跳轉(zhuǎn)main函數(shù)
第三部分:launch closure :cache
啟動(dòng)閉包launch closure緩存服務(wù)。其中大多數(shù)程序啟動(dòng)都會(huì)使用緩存,而不需要調(diào)用進(jìn)程外 mach-o分析器和編譯器。且launch closure比mach-o更簡單,因?yàn)?code>launch closure是內(nèi)存映射文件,不需要用復(fù)雜的方法進(jìn)行分析,我們可以進(jìn)行簡單的校驗(yàn),目的是為了提高速度
系統(tǒng)應(yīng)用的
launch closure直接加入到共享緩存 share cache對(duì)于第三方應(yīng)用,我們將在應(yīng)用安裝或者更新期間構(gòu)建
launch closure,因?yàn)榇藭r(shí)system library已發(fā)生更改默認(rèn)情況下,在
iOS,tvOS和watchOS上,這些操作都將在運(yùn)行之前為您預(yù)先構(gòu)建。在
macOS上,由于可以側(cè)向加載應(yīng)用程序(這里應(yīng)該是指非App Store安裝的應(yīng)用),因此如果需要,in-process engine可以在首次啟動(dòng)時(shí)RPC(Remote Procedure Call)到out to the daemon,然后,它就可以使用緩存的closure了。
所以綜上所述,dyld 3 把很多耗時(shí)的查找、計(jì)算和 I/O 操作都預(yù)先處理好了,使得啟動(dòng)速度有了很大的提升。即dyld 3把很多耗時(shí)的操作都提前處理好了,極大提升了啟動(dòng)速度。
啟動(dòng)閉包(launch closure)
這是一個(gè)新引入的概念,指的是 app 在啟動(dòng)期間所需要的所有信息。比如這個(gè) app 使用了哪些動(dòng)態(tài)鏈接庫,其中各個(gè)符號(hào)的偏移量,代碼簽名在哪里等等。
dyld 3符號(hào)缺失問題
dyld 2中默認(rèn)采取的是lazy symbol的符號(hào)加載方式dyld 3中,在app啟動(dòng)之前,符號(hào)解析的結(jié)果已經(jīng)在launch closure內(nèi)了,所以lazy symbol就不再需要了。-
如果此時(shí),如果
有符號(hào)缺失的情況,dyld 2 和 dyld 3的表現(xiàn)是不同的dyld 2中,首次調(diào)用缺失符號(hào)時(shí)App會(huì)crash在
dyld 3中,缺失符號(hào)會(huì)導(dǎo)致App一啟動(dòng)就會(huì)crash
總結(jié)
-
dyld 2工作流程解析
mach-o頭部查找依賴庫
映射mach-o文件,放入地址空間中執(zhí)行符號(hào)查找
使用
ASLR進(jìn)行rebase和bind綁定運(yùn)行所有初始化器
執(zhí)行main函數(shù)
-
dyld 3工作流程進(jìn)程外:將dyld 2中的mach-o頭部解析、符號(hào)查找移到了進(jìn)程外執(zhí)行,且將其執(zhí)行結(jié)果放入
啟動(dòng)閉包,存儲(chǔ)到磁盤中進(jìn)程內(nèi):驗(yàn)證
啟動(dòng)閉包正確性,并映射dylib,執(zhí)行main函數(shù)啟動(dòng)閉包緩存服務(wù)