dyld簡介
dyld
全名The dynamic link editor
;是蘋果的
動態鏈接器
;是蘋果操作系統的一個重要組成部分;
在應用被編譯打包成可執行文件之后(即Mach-O),將其交由
dyld
負責鏈接,加載程序
。dyld貫穿了App啟動的過程,包含加載依賴庫、主程序,如果我們需要進行性能優化、啟動優化等,不可避免的需要和dyld打交道
且dyld是開源的,我們可以在官網下載它的源碼來閱讀理解
dyld 1.0(1996-2004)
dyld 1
包含在NeXTStep 3.3
中,在此之前的NeXT使用靜態二進制
數據。作用并不是很大,dyld 1
是在系統廣泛使用C++動態庫之前編寫的,由于C++有許多特性,例如其初始化器的工作,在靜態環境工作良好,但是在動態環境中可能會降低性能。因此大型的C++動態庫會導致dyld需要完成大量的工作,速度變慢在發布
macOS 10.0
和Cheetah
前,還增加了一個特性,即Prebinding預綁定
。我們可以使用Prebinding技術為系統中的所有dylib
和應用程序找到固定的地址
。dyld將會加載這些地址的所有內容。如果加載成功,將會編輯所有dylib和程序的二進制數據,來獲得所有預計算。當下次需要將所有數據放入相同地址時就不需要進行額外操作了,將大大的提高速度。但是這也意味著每次啟動都需要編輯這些二進制數據,至少從安全性來說,這種方式并不友好。
dyld 2(2004-2017)
dyld 2
從2004年發布至今,已經經過了多個版本迭代,我們現在常見的一些特性,例如ASLR
、Code Sign
、share cache
等技術,都是在dyld 2中引入的
dyld 2.0(2004-2007)
2004年在
macOS Tiger
中推出了dyld 2
dyld 2
是dyld 1
完全重寫的版本,可以正確支持C++初始化器語義,同時擴展了mach-o格式并更新dyld。從而獲得了高效率C++庫的支持。-
dyld 2具有完成的
dlopen
和dlsym
(主要用于動態加載庫和調用函數)實現,且具有正確的語義,因此棄用了舊版的APIdlopen
:打開一個庫,獲取句柄dlsym
:在打開的庫中查找符號的值dlclose
:關閉句柄。dlerror
:返回一個描述最后一次調用dlopen、dlsym,或 dlclose 的錯誤信息的字符串。
dyld
的設計目標
是提升啟動速度
。因此僅進行有限的健全性檢查。主要是因為以前的惡意程序比較少同時dyld也有一些安全問題,因此對一些功能進行了改進,來提高dyld在平臺上的安全性
由于啟動速度的大幅提升,因此我們可以
減少Prebinding的工作量
。與編輯程序數據
的區別在于,在這里我們僅編輯系統庫,且可以僅在軟件更新時做這些事情。因此在軟件更新過程中,可能會看到“優化系統性能”類似的文字。這就是在更新時進行Prebinding
。現在dyld用于所有優化,其用途就是優化。因此后面有了dyld 2
dyld 2.x(2007-2017)
- 在2004-20017這幾年間進行了大量改進,dyld 2的性能顯著提高
- 首先,
增加
了大量的基礎架構
和平臺
。- 自從dyld 2在PowerPC發布之后,增加了
x86
、x86_64
、arm
、arm64
和許多的衍生平臺。 - 還推出了
iOS
、tvOS
和watchOS
,這些都需要新的dyld功能
- 自從dyld 2在PowerPC發布之后,增加了
- 通過多種方式增加安全性
- 增加
codeSigning
代碼簽名、 -
ASLR(Address space layout randomization)
地址空間配置隨機加載:每次加載庫時,可能位于不同的地址 -
bound checking
邊界檢查:mach-o文件中增加了Header的邊界檢查功能,從而避免惡意二進制數據的注入
- 增加
- 增強了性能
- 可以消除Prebinding,用
share cache
共享代碼代替
- 可以消除Prebinding,用
ASLR
ASLR
是一種防范內存損壞漏洞被利用的計算機安全技術
,ASLR通過隨機放置進程關鍵數據區域的地址空間來防止攻擊者跳轉到內存特定位置來利用函數Linux已在內核版本2.6.12中添加ASLR
Apple在
Mac OS X Leopard 10.5
(2007年十月發行)中某些庫導入了隨機地址偏移
,但其實現并沒有提供ASLR所定義的完整保護能力。而Mac OS X Lion 10.7則對所有的應用程序均提供了ASLR支持。Apple在
iOS 4.3
內導入了ASLR
。
bounds checking 邊界檢查
- 對mach-o header中的許多內容添加了重要的
邊界檢查
功能,從而可以避免惡意二進制數據的注入
share cache 共享代碼
share cache
最早實在iOS3.1
和macOS Snow Leopard
中被引入,用于完全取代Prebinding-
share cache
是一個單文件
,包含大多數系統dylib
,由于這些dylib合并成了一個文件,所以可以進行優化。重新調整所有
文本段(_TEXT)
和數據段(_DATA)
,并重寫整個符號表,以此來減小文件的大小,從而在每個進程中僅掛載少量的區域。允許我們打包二進制數據段,從而節省大量的RAM本質是一個
dylib預鏈接器
,它在RAM上的節約是顯著的,在普通的iOS程序中運行可以節約500-1g
內存還可以
預生成數據結構
,用來供dyld和Ob-C在運行時使用。從而不必在程序啟動時做這些事情,這也會節約更多的RAM和時間
share cache
在macOS上本地生成,運行dyld共享代碼,將大幅優化系統性能
dyld 2 工作流程
dyld 2是純粹的in-process
,即在程序進程內執行
的,也就意味著只有當應用程序被啟動時,dyld 2才能開始執行任務
以下是dyld 2的工作流程圖示
- 1、dyld的初始化,主要代碼在
dyldbootstrap::start
,接著執行dyld::_main
,dyld::_main
代碼較多,是dyld加載的核心部分; - 2、檢查并準備環境,例如獲取二進制路徑、檢查環境配置,解析主二進制的
image header
等信息 - 3、實例化主二進制的
image loader
,校驗主二進制和dyld的版本是否匹配 - 4、檢查
share cache是否已經map
,如果沒有則需要先執行map share cache操作 - 5、檢查
DYLD_INSERT_LIBRARIES
,如果有則加載插入的動態庫(即實例化image loader) - 6、執行
link
操作,會先遞歸加載依賴的所有動態庫(會對依賴庫進行排序,被依賴的總是在前面),同時在這階段將執行符號綁定,以及rebase
,binding
操作; - 7、執行初始化方法,OC的
+load
和C的constructor
方法都會在這個階段執行; - 8、讀取
Mach-o
的LC_MAIN
段獲取程序的入口地址,調用main函數
簡化版
① 解析 mach-o 文件,找到其依賴的庫,并且遞歸的找到所有依賴的庫,形成一張動態庫的依賴圖。iOS 上的大部分 app 都依賴幾百個動態鏈接庫(大部分是系統的動態庫),所以這個步驟包含了較大的工作量。
② 匹配 mach-o 文件到自身的地址空間
③ 進行符號查找(perform symbol lookups)
④
rebase
和binding
:由于 app 需要讓地址空間配置隨機加載,所以所有的指針都需要加上一個基地址⑤ 運行初始化程序,之后運行
main()
函數
dyld 3(2017-至今)
dyld 3
是2017年WWDC推出的全新的動態鏈接器,它完全改變了動態鏈接的概念,且將成為大多數macOS系統程序的默認設置。2017 Apple OS平臺上的所有系統程序都會默認使用dyld 3.dyld 3
最早是在2017年的iOS 11
中引入,主要用來優化系統庫。而在
iOS 13
系統中,iOS全面采用新的dyld 3來替代之前的dyld 2,因為dyld 3完全兼容dyld 2
,其API接口也是一樣的,所以,在大部分情況下,開發者并不需要做額外的適配就能平滑過渡。
為什么需要重新設計dyld 2,形成新的dyld 3 ?
重新設計dyld,主要從以下幾方面進行考慮
性能
:想要盡可能的提高啟動速度
安全性
:在dyld 2中增加了安全特性,但是很難跟隨現實情形,雖然做了很多工作,但是難以實現這個目標可靠性
和可測試性
:為此Apple發布了很多不錯的測試框架,例如XCTest
,但是這些測試框架依賴于動態鏈接器
的底層
功能,然后將測試框架的庫插入進程中,所以不能用于測試現有的dyld代碼,且難以測試安全性和性能水平
如何將 dyld 2 改進和優化為 dyld 3?
改進和優化建議
從上面的dyld 2的工作流程中,我們了解了dyld 2的執行流程,可以從以下兩個方面來改進和優化:
-
確定
安全敏感
的部分Parse mach-o headers
解析mach-o 和Find dependencies
找到依賴庫,是安全敏感部分,即最大的安全隱患之一;惡意撰改
mach-o頭部
,可以進行某些攻擊;如果App使用了
@rpaths
即搜索路徑
,可以通過惡意撰改路徑
或者將一些庫插入到特定的位置
,來達到破壞程序的目的;
-
確定
大量占用資源
的部分(即可緩存部分)-
Perform symbol lookups
符號查找就是其中一個,因為在一個特定的庫中,除非進行軟件更新或者在磁盤上更改庫,不然符號將始終位于庫中的相同的偏移位置(即符號偏移量固定
);
-
dyld 2 改進和優化
以下是dyld 2
向 dyld 3
的一些改變,主要是將安全敏感
的部分 和 占用大量資源
的部分移動到上層,然后將一個closure
寫入磁盤進行緩存,然后我們在程序進程中使用closure。以下是圖示
dyld 3 組成部分/工作流程
dyld 3的工作流程主要分為3部分,如下所示
第一部分:out-of-process :mach-o parser
進程外的mach-o分析器和編譯器,是普通的后臺程序,用于提高測試基礎架構的性能。
第一部分主要在App進程之外做以下工作:
解析所有搜索路徑
@rpath
、環境變量,因為它們會影響啟動速度分析
mach-o
二進制數據執行
符號查找
利用這些結果創建
launch clourse
第二部分:in-process :engine
進程內的引擎,這部分常駐在內存中,且在dyld 3
不再需要分析mach-o文件頭或者執行符號查找就可以啟動應用,因為分析mach-o和執行符號查找都是耗時操作,所以極大的提高了程序啟動速度。
第二部分主要在App進程中做以下工作:
檢查
launch closure
是否正確映射到
dylib
中,再跳轉main
函數
第三部分:launch closure :cache
啟動閉包launch closure
緩存服務。其中大多數程序啟動都會使用緩存,而不需要調用進程外 mach-o分析器和編譯器。且launch closure
比mach-o
更簡單,因為launch closure
是內存映射文件
,不需要用復雜的方法進行分析,我們可以進行簡單的校驗,目的是為了提高速度
系統應用的
launch closure
直接加入到共享緩存 share cache
對于第三方應用,我們將在應用安裝或者更新期間構建
launch closure
,因為此時system library
已發生更改默認情況下,在
iOS
,tvOS
和watchOS
上,這些操作都將在運行之前為您預先構建
。在
macOS
上,由于可以側向加載應用程序(這里應該是指非App Store
安裝的應用),因此如果需要,in-process engine
可以在首次啟動時RPC(Remote Procedure Call
)到out to the daemon
,然后,它就可以使用緩存的closure了。
所以綜上所述,dyld 3
把很多耗時的查找、計算和 I/O 操作都預先處理好了,使得啟動速度有了很大的提升。即dyld 3把很多耗時的操作都提前處理好了,極大提升了啟動速度。
啟動閉包(launch closure)
這是一個新引入的概念,指的是 app 在啟動期間所需要的所有信息。比如這個 app 使用了哪些動態鏈接庫,其中各個符號的偏移量,代碼簽名在哪里等等。
dyld 3符號缺失問題
dyld 2
中默認采取的是lazy symbol
的符號加載方式dyld 3
中,在app啟動之前,符號解析的結果已經在launch closure內了,所以lazy symbol就不再需要了。-
如果此時,如果
有符號缺失
的情況,dyld 2 和 dyld 3的表現是不同的dyld 2
中,首次調用缺失符號時App會crash
在
dyld 3
中,缺失符號會導致App一啟動就會crash
總結
-
dyld 2
工作流程解析
mach-o頭部
查找依賴庫
映射mach-o
文件,放入地址空間中執行符號查找
使用
ASLR
進行rebase
和bind
綁定運行所有初始化器
執行main函數
-
dyld 3
工作流程進程外:將dyld 2中的mach-o頭部解析、符號查找移到了進程外執行,且將其執行結果放入
啟動閉包
,存儲到磁盤中進程內:驗證
啟動閉包正確性
,并映射dylib,執行main函數啟動閉包緩存服務