導(dǎo)讀
? ? 本文承接自APP啟動(dòng)-流程(一)陨溅,有疑惑的同學(xué)可以先閱讀上一篇的內(nèi)容发钝。本文會(huì)帶大家詳細(xì)的解讀dyld-852.2源碼中關(guān)于APP啟動(dòng)最重要的一個(gè)函數(shù)_main()偶洋。我們從上一節(jié)了解到_main函數(shù)的實(shí)現(xiàn)代碼有900行版保,所以不可能一行一行的來進(jìn)行解讀洲愤,本文只會(huì)針對(duì)重點(diǎn)函數(shù)進(jìn)行跟進(jìn)解讀。有需要的同學(xué)自行下載源碼解讀乐埠。源碼解讀并非跟著文章看一遍就能記住學(xué)會(huì)抗斤,這個(gè)過程需要反復(fù)的跟讀囚企,所以建議讀者將源碼下載下來丈咐,跟著筆者的進(jìn)度同時(shí)對(duì)照著源碼學(xué)習(xí)效果才會(huì)最佳,也不至于看得云里霧里龙宏。
dyld下載地址:http://opensource.apple.com/tarballs/dyld
在分析具體的源碼之前棵逊,我們必須要了解一些前置知識(shí)點(diǎn):
dyld的全稱是dynamic loader。dyld 是 iOS 上的二進(jìn)制加載器银酗,用于加載 Image辆影。有不少人認(rèn)為 dyld 只負(fù)責(zé)加載應(yīng)用依賴的所有動(dòng)態(tài)鏈接庫,這個(gè)理解是錯(cuò)誤的黍特。dyld 工作的具體流程如下:
dyld2與dyld3
在?iOS 13?之前蛙讥,所有的第三方?App?都是通過?dyld 2?來啟動(dòng)?App?的,主要過程如下:
1灭衷、Parse mach-o headers?????解析mach-o頭文件
2次慢、find dependencels 根據(jù)頭文件的信息查找依賴項(xiàng)
3、Map mach-o files ?將mach-o文件映射到內(nèi)存翔曲,簡(jiǎn)單來講就是加載到內(nèi)存中
4迫像、Perform symbol lookups這個(gè)步驟表示執(zhí)行符號(hào)查找。(例如:如果你使用了printf函數(shù)瞳遍,就會(huì)查找printf是否在庫系統(tǒng)中闻妓,找到它的地址,將它賦值到你的程序中的函數(shù)指針)
5掠械、Bind and rebase ?符號(hào)綁定和地址重定位(由于ASLR的原因)
6由缆、Run initializers ?dyld會(huì)運(yùn)行初始化函數(shù)注祖,初始化動(dòng)態(tài)庫,初始化主程序均唉,然后進(jìn)入運(yùn)行時(shí)runtime初始化氓轰,注冊(cè)類,分類浸卦,方法唯一性檢查等(后續(xù)的文章會(huì)詳細(xì)分析runtime的初始化過程)
在iOS 13之后署鸡,dyld3開放給第三方APP使用了,也就是說限嫌,在iOS 13以上的系統(tǒng)里靴庆,APP是通過dyld3來啟動(dòng)的。
從上圖來看dyld3是由2個(gè)部分組成怒医,上半部分在程序啟動(dòng)進(jìn)程外執(zhí)行的炉抒,這一步會(huì)在App下載安裝和版本更新的時(shí)候會(huì)去執(zhí)行。下半部分才是在程序啟動(dòng)進(jìn)程內(nèi)執(zhí)行的稚叹。
原本在dyld2中執(zhí)行的1焰薄、2、4步被放到了程序啟動(dòng)進(jìn)程外執(zhí)行扒袖,然后向磁盤寫入閉包處理 “Write closure to disk”(啟動(dòng)閉包(launch closure):這是一個(gè)新引入的概念塞茅,指的是 app 在啟動(dòng)期間所需要的所有信息。比如這個(gè) app 使用了哪些動(dòng)態(tài)鏈接庫季率,其中各個(gè)符號(hào)的偏移量野瘦,代碼簽名在哪里等等)。這樣飒泻,啟動(dòng)閉包處理就成了啟動(dòng)程序的重要環(huán)節(jié)鞭光。稍后可以在APP的進(jìn)程中使用 dyld 3包含的這三個(gè)部分,
啟動(dòng)閉包比mach-o更簡(jiǎn)單泞遗。它們是內(nèi)存映射文件惰许,不需要用復(fù)雜的方法進(jìn)行分析。
我們可以簡(jiǎn)單的驗(yàn)證它們史辙,這樣可以提高速度汹买,也就是說在不需要修改代碼的情況下官方幫我們做了啟動(dòng)優(yōu)化。
dyld3的主要過程如下:
主程序進(jìn)程外
1髓霞、Parse mach-o headers?????解析mach-o頭文件
2卦睹、find dependencels 根據(jù)頭文件的信息查找依賴項(xiàng)
3、Perform symbol lookups ?這個(gè)步驟表示執(zhí)行符號(hào)查找方库。(例如:如果你使用了printf函數(shù)结序,就會(huì)查找printf是否在庫系統(tǒng)中,找到它的地址纵潦,將它賦值到你的程序中的函數(shù)指針)
4徐鹤、Write closure to disk ?將1垃环、2、3步做完的事情組裝成一個(gè)啟動(dòng)閉包返敬,并且寫入磁盤
主程序進(jìn)程內(nèi)
5遂庄、Read in closure ? ?從啟動(dòng)閉包中讀取必要的信息數(shù)據(jù)
6、Validata closure ? ? 驗(yàn)證啟動(dòng)閉包
7劲赠、Map mach-o files ?將mach-o文件映射到內(nèi)存涛目,簡(jiǎn)單來講就是加載到內(nèi)存中
8、Bind and rebase ?符號(hào)綁定和地址重定位(由于ASLR的原因)
9凛澎、Run initializers ?dyld會(huì)運(yùn)行初始化函數(shù)霹肝,初始化動(dòng)態(tài)庫,初始化主程序塑煎,然后進(jìn)入運(yùn)行時(shí)runtime初始化沫换,注冊(cè)類,分類最铁,方法唯一性檢查等(后續(xù)的文章會(huì)詳細(xì)分析runtime的初始化過程)
dyld3 被分為了三個(gè)組件:
一讯赏、一個(gè)進(jìn)程外的 MachO 解析器
1、預(yù)先處理了所有可能影響啟動(dòng)速度的 search path冷尉、@rpaths 和環(huán)境變量
2漱挎、然后分析 Mach-O 的 Header 和依賴,并完成了所有符號(hào)查找的工作
3网严、最后將這些結(jié)果創(chuàng)建成了一個(gè)啟動(dòng)閉包
4识樱、這是一個(gè)普通的 daemon 進(jìn)程,可以使用通常的測(cè)試架構(gòu)
二震束、一個(gè)進(jìn)程內(nèi)的引擎,用來運(yùn)行啟動(dòng)閉包
1当犯、這部分在進(jìn)程中處理
2垢村、驗(yàn)證啟動(dòng)閉包的安全性,然后映射到 dylib 之中嚎卫,再跳轉(zhuǎn)到 main 函數(shù)
3嘉栓、不需要解析 Mach-O 的 Header 和依賴,也不需要符號(hào)查找拓诸。
三侵佃、一個(gè)啟動(dòng)閉包緩存服務(wù)
1、系統(tǒng) App 的啟動(dòng)閉包被構(gòu)建在一個(gè) Shared Cache 中奠支, 我們甚至不需要打開一個(gè)單獨(dú)的文件
2馋辈、對(duì)于第三方的 App,我們會(huì)在 App 安裝或者升級(jí)的時(shí)候構(gòu)建這個(gè)啟動(dòng)閉包倍谜。
3迈螟、在 iOS叉抡、tvOS、watchOS中答毫,這這一切都是 App 啟動(dòng)之前完成的褥民。在 macOS 上,由于有 Side Load App洗搂,進(jìn)程內(nèi)引擎會(huì)在首次啟動(dòng)的時(shí)候啟動(dòng)一個(gè) daemon 進(jìn)程消返,之后就可以使用啟動(dòng)閉包啟動(dòng)了。
dyld 3 把很多耗時(shí)的查找耘拇、計(jì)算和 I/O 的事前都預(yù)先處理好了侦副,這使得啟動(dòng)速度有了很大的提升。
_main函數(shù)
函數(shù)的每個(gè)參數(shù)意義如下:
// dyld的main函數(shù) dyld的入口方法kernel加載dyld并設(shè)置設(shè)置一些寄存器并調(diào)用此函數(shù),之后跳轉(zhuǎn)到__dyld_start
// mainExecutableSlide 主程序的slider,用于做重定向 會(huì)在main方法中被賦值
// mainExecutableMH 主程序MachO的header
// argc 表示main函數(shù)參數(shù)個(gè)數(shù)
// argv 表示main函數(shù)的參數(shù)值 argv[argc] 可以獲取到參數(shù)值
// envp[] 表示以設(shè)置好的環(huán)境變量
// apple 是從envp開始獲取到第一個(gè)值為NULL的指針地址
uintptr_t
_main(constmacho_header* mainExecutableMH,uintptr_tmainExecutableSlide,?
intargc,constchar* argv[],constchar* envp[],constchar* apple[],?
uintptr_t* startGlue)
源碼分析_main()函數(shù):
配置運(yùn)行環(huán)境
檢查是否開啟debug追蹤驼鞭,追蹤的是啟動(dòng)可執(zhí)行文件的過程秦驯。
檢查是否有內(nèi)核相關(guān)的標(biāo)記,setFlags內(nèi)部會(huì)檢查dyld3是否已經(jīng)初始化挣棕,沒有初始化則不設(shè)置flags
檢查并查看內(nèi)核是否禁用了JOP(Jump-Oriented Programming)指針簽名译隘,指針簽名是用于防范內(nèi)核攻擊的一個(gè)手段。
從環(huán)境變量中獲取主要可執(zhí)行文件的?cdHash?值洛心。這個(gè)哈希值?mainExecutableCDHash?在后面用來校驗(yàn)?dyld3?的啟動(dòng)閉包
獲取當(dāng)前設(shè)備的一些信息固耘,比如:cpu架構(gòu)類型,基本信息词身,mach進(jìn)程通信端口等
通知內(nèi)核開始加載dyld和主程序的可執(zhí)行文件了
獲取主程序的macho_header結(jié)構(gòu)
獲取主程序的slide值厅目,slide其實(shí)就是mach-o映射到內(nèi)存的偏移量
查找可執(zhí)行文件支持的平臺(tái)(看綠色高亮的FIXME這行,其實(shí)這部分代碼可以刪除法严,因?yàn)樵趦?nèi)核中已經(jīng)處理過了)
接著沒截出來的那一段代碼是基于OS系統(tǒng)的判斷邏輯损敷,本文不做分析。
設(shè)置上下文信息深啤,保存回調(diào)函數(shù)的地址拗馒,以便后續(xù)直接調(diào)用∷萁郑可以看看setContext的源碼如下:
根據(jù)環(huán)境變量從內(nèi)核拿到可執(zhí)行文件的路徑诱桂。
移除過渡代碼(修復(fù)了rdar://problem/13868260這個(gè)bug)
由于是多平臺(tái)共用一套代碼,內(nèi)核傳遞出來的exec路徑是不全的呈昔,如果是iphone真機(jī)環(huán)境挥等,則會(huì)拼接一段路徑的前綴,大家肯定打印過很多Document的路徑堤尾,真機(jī)都是/var/開頭的和模擬器的不一樣肝劲。
判斷?exec?路徑是否為絕對(duì)路徑,如果為相對(duì)路徑,使用?cwd?轉(zhuǎn)化為絕對(duì)路徑
為了后續(xù)的日志打印從?exec?路徑中取出進(jìn)程的名稱 (strrchr?函數(shù)是獲取第二個(gè)參數(shù)出現(xiàn)的最后的一個(gè)位置涡相,然后返回從這個(gè)位置開始到結(jié)束的內(nèi)容)
配置進(jìn)程的受限模式哲泊,設(shè)置gLinkContext的環(huán)境變量,跟簽名以及代碼注入有關(guān)催蝗。查看詳細(xì)的configureProcessRestrictions代碼實(shí)現(xiàn)切威,會(huì)發(fā)現(xiàn)一個(gè)新系統(tǒng)AMFI (AppleMobileFileIntegrity)。
簡(jiǎn)單介紹下AMFI是一個(gè)內(nèi)核擴(kuò)展丙号,最初是在iOS中引入的先朦。在版本10.10中,它也被添加到macOS中犬缨。它擴(kuò)展了MACF(強(qiáng)制訪問控制框架)喳魏,就像沙盒一樣,它在實(shí)施SIP和代碼簽名方面起著關(guān)鍵作用(這一塊涉及的內(nèi)容非常多怀薛,不做詳細(xì)分析)
dyld3::internalInstall()這個(gè)函數(shù)在xcode默認(rèn)設(shè)置下是返回yes的刺彩。
ClosureMode是個(gè)calss類型的枚舉,有4種模式:
Unset????表示我們沒有提供env變量或boot arg來顯式選擇模式
On????表示我們將DYLD_USE_CLOSURES設(shè)置為1枝恋,或者我們沒有將DYLD_USE_CLOSURES設(shè)置為0创倔,但在iOS上設(shè)置了-force_dyld3=1環(huán)境變量或者一個(gè)外部緩存(啟動(dòng)閉包)
Off????意味著我們?cè)O(shè)置了DYLD_USE_CLOSURES=0,或者我們沒有設(shè)置DYLD_USE_CLOSURES=1焚碌,但在iOS上設(shè)置了-force\u dyld2=1環(huán)境變量或者一個(gè)內(nèi)部緩存
PreBuiltOnly????意味著只使用共享緩存閉包畦攘,而不嘗試構(gòu)建新的緩存閉包
ClosureKind 也是個(gè)class類型的枚舉,有3種狀態(tài):unset, full, minimal
默認(rèn)值都是unset
檢查是否強(qiáng)制使用共享緩存十电,在iOS上為了節(jié)省內(nèi)存知押,是要求強(qiáng)制使用共享緩存的。
創(chuàng)建了緩存閉包路徑的數(shù)組變量鹃骂,如果出錯(cuò)台盯,則直接退出進(jìn)程。
成功則將閉包模式設(shè)置為on偎漫。
到此爷恳,運(yùn)行環(huán)境全部設(shè)置完成。細(xì)心的同學(xué)應(yīng)該會(huì)注意到象踊,整個(gè)過程中有很多DYLD_****開頭的環(huán)境變量。其實(shí)這些都是可以在Xcode中配置使其在上面的流程中生效的棚壁,我們打開工程然后依次點(diǎn)擊“Product”->“Scheme”->“Edit Scheme…”杯矩,如下圖所示。
然后運(yùn)行Xcode即可看到控制臺(tái)打印的詳細(xì)信息袖外。有很多這樣的DYLD_*開頭的環(huán)境變量史隆,感興趣的同學(xué)可以自行測(cè)試。
加載共享緩存
這里補(bǔ)充一下共享緩存的知識(shí)點(diǎn):
在iOS系統(tǒng)中曼验,每個(gè)程序依賴的動(dòng)態(tài)庫都需要通過dyld(位于/usr/lib/dyld)一個(gè)一個(gè)加載到內(nèi)存泌射,然而如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次粘姜,勢(shì)必造成運(yùn)行緩慢,為了優(yōu)化啟動(dòng)速度和提高程序性能熔酷,共享緩存機(jī)制就應(yīng)運(yùn)而生孤紧。所有默認(rèn)的動(dòng)態(tài)鏈接庫被合并成一個(gè)大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下拒秘,按不同的架構(gòu)保存分別保存著号显。
如果沒有緩存庫存在的話,那么我們手機(jī)上的每一個(gè)App躺酒,如果要用到系統(tǒng)動(dòng)態(tài)庫的話押蚤,是需要每一個(gè)App都要去加載一次的,一樣的資源被加載多次(加載幾次就需要消耗幾份內(nèi)存)羹应,無論是空間還是執(zhí)行效率揽碘,都是造成了浪費(fèi)。
如果有共享緩存庫(系統(tǒng)會(huì)提前將一些常用的庫加載到內(nèi)存中)存在的話园匹,那么我們手機(jī)上的每一個(gè)App雳刺,如果要用到系統(tǒng)動(dòng)態(tài)庫的話,只需要先去內(nèi)存中找偎肃,找到了就直接鏈接就行煞烫,沒有找到的花,加載一份到共享緩存的內(nèi)存空間中累颂,然后再鏈接這個(gè)庫滞详。節(jié)省了內(nèi)存,還提高了運(yùn)行速度紊馏。
前面說過料饥,iOS系統(tǒng)是強(qiáng)制使用共享緩存的,所以checkSharedRegionDisable里面什么都不會(huì)做朱监,然后將共享緩存映射到當(dāng)前進(jìn)程的內(nèi)存空間內(nèi)岸啡。
真正加載共享緩存的是這句代碼:mapSharedCache(); 里面調(diào)用了loadDyldCache(),從if else代碼可以看出赫编,共享緩存加載又分為三種情況:
僅加載到當(dāng)前進(jìn)程巡蘸,調(diào)用mapCachePrivate()。
共享緩存已加載擂送,不做任何處理悦荒。
當(dāng)前進(jìn)程首次加載共享緩存,調(diào)用mapCacheSystemWide()嘹吨。
mapCachePrivate()搬味、mapCacheSystemWide()里面就是具體的共享緩存解析邏輯,感興趣的同學(xué)可以自己深入詳細(xì)分析。
dyld3
查找啟動(dòng)閉包
如果沒有設(shè)置閉包模式碰纬,則檢查環(huán)境變量里的字段以及緩存類型來判斷設(shè)置哪種模式萍聊。
4種模式在前文有詳細(xì)介紹。
宏檢查是否是真機(jī)悦析,創(chuàng)建一些零時(shí)變量存儲(chǔ)啟動(dòng)閉包的信息寿桨,檢查共享緩存中是否有啟動(dòng)閉包,前文有提到她按,系統(tǒng)app會(huì)將啟動(dòng)閉包保存到共享緩存中牛隅,所以是有必要做這項(xiàng)檢查的。
從 findClosure() 實(shí)現(xiàn)里面也可以看出:
(anything in /System/ should have a closure)任何在/System/ 路徑下的可執(zhí)行文件都有一個(gè)啟動(dòng)閉包酌泰。
重新回到_main()函數(shù)
接下來是一系列重新構(gòu)建閉包的判斷媒佣。所有的if else 都是在為allowClosureRebuilds服務(wù)的。
需要特別注意的是6997行用紅色框框出來的closureValid()函數(shù)陵刹,這個(gè)函數(shù)里面做了大量的判斷默伍,來判斷緩存里是否存在閉包。注意衰琐,這里的緩存和上文的共享緩存是有區(qū)別的也糊!
下面是具體的實(shí)現(xiàn),由于實(shí)現(xiàn)代碼太長(zhǎng)羡宙,把很多分支都隱藏還是無法展示全部代碼狸剃,所以這里只截取了前半部分代碼,不過我會(huì)把有注釋的判斷都列出來:
如上圖狗热,在_main()函數(shù)里獲取的CDHash在這里發(fā)揮了作用钞馁,代碼能執(zhí)行到這說明是有緩存閉包的,后續(xù)的判斷都是針對(duì)這個(gè)存在的閉包匿刮,判斷是否有效僧凰。
下面列出剩余的注釋,一行注釋就是一個(gè)判斷:
//If we found cd hashes, but they were all invalid, then print them out - 如果我們發(fā)現(xiàn)了cdHash熟丸,但它們都是無效的训措,那么就把它們打印出來
// verify UUID of main executable is same as recorded in closure - 驗(yàn)證主可執(zhí)行文件的UUID是否與閉包中記錄的相同
// verify DYLD_* env vars are same as when closure was built -?驗(yàn)證DYLD_*env變量是否與構(gòu)建閉包時(shí)相同
// verify files that are supposed to be missing actually are missing -?驗(yàn)證應(yīng)該丟失的文件是否確實(shí)丟失
// verify files that are supposed to exist are there with the -?驗(yàn)證假定存在的文件是否與 (這里的注釋不全,根據(jù)打印log光羞,應(yīng)該是驗(yàn)證文件完整性的)
// verify closure did not require anything unavailable -?驗(yàn)證閉包是否有依賴任何不可用的內(nèi)容
分析完closureValid()绩鸣,我們回到_main()函數(shù)中,繼續(xù)閉包相關(guān)的源碼閱讀:
這里驗(yàn)證了上文提到的只有第三方app的啟動(dòng)閉包會(huì)保存在磁盤中纱兑。
主要邏輯是 獲取主程序啟動(dòng)閉包CDHash全闷,然后做如下判斷:
1、如果CDHash不為空萍启,則再判斷是否啟動(dòng)了dyld3,如果啟動(dòng)了則什么都不做,從打印我們知道勘纯,在啟動(dòng)dyld3的情況下內(nèi)部系統(tǒng)是允許磁盤上的啟動(dòng)閉包的局服。如果沒有啟動(dòng)dyld3,則設(shè)置canUseClosureFromDisk = false驳遵,也就是說不允許使用磁盤上的啟動(dòng)閉包淫奔。
2、如果CDHash為空堤结,則判斷主程序啟動(dòng)閉包是否為空唆迁,如果不為空則將mainClosure設(shè)置為空指針。結(jié)合注釋我們可以了解到cdHash和啟動(dòng)閉包是必須同時(shí)存在的竞穷,如果CDHash為空唐责,則找到的緩存閉包也不能使用。
構(gòu)建啟動(dòng)閉包
rdar的bug先不管(其實(shí)是筆者水平不夠沒弄懂)瘾带。
然后如果發(fā)現(xiàn)沒有找到一個(gè)有效的啟動(dòng)閉包鼠哥,則嘗試自己構(gòu)建一個(gè)。
buildLaunchClosure()這個(gè)函數(shù)就是構(gòu)建閉包的函數(shù)看政,內(nèi)部實(shí)現(xiàn)比較長(zhǎng)比較復(fù)雜朴恳,在本文不做詳細(xì)的分析。后續(xù)有空的話會(huì)新開一篇專門分析啟動(dòng)閉包構(gòu)建和加載允蚣。
接下來判斷sJustBuildClosure是否為yes于颖,如果為yes意思就是只構(gòu)建閉包,不做加載動(dòng)作嚷兔,上文提到APP在下載完成以及升級(jí)完成后會(huì)重新構(gòu)建啟動(dòng)閉包森渐,在這種情況下dyld構(gòu)建完閉包后就直接退出了。
加載閉包
這里的邏輯比較簡(jiǎn)單谴垫,launchWithClosure()加載啟動(dòng)閉包章母,判斷是否成功,如果失敗翩剪,判斷是否啟動(dòng)閉包是否過期乳怎,過期了重新構(gòu)建一個(gè),構(gòu)建完成后再重新加載閉包前弯。
launchWithClosure()函數(shù)內(nèi)部實(shí)現(xiàn)很長(zhǎng)蚪缀,粗略的看了下具體的實(shí)現(xiàn),簡(jiǎn)單說一下:
拿到所有的image數(shù)組(最多三個(gè):緩存動(dòng)態(tài)庫恕出、其他操作系統(tǒng)動(dòng)態(tài)庫和主程序)询枚,
然后調(diào)用了dyld3 的一個(gè)Load的loader()方法,然后遞歸的加載依賴庫浙巫,并且將加載的庫添加到allImages數(shù)組中保存金蜀。
找到dyld的入口刷后,把所有鏡像的信息傳遞給dyld,然后run?initializers渊抄。
加載完閉包后返回一個(gè)result尝胆,這是一個(gè)假main()函數(shù)的入口,內(nèi)部實(shí)現(xiàn)就是return 0护桦;
到此含衔,dyld3的啟動(dòng)過程就完成了。
但是_main()函數(shù)還未結(jié)束二庵,后續(xù)的代碼是使用dyld2的方式加載流程贪染。模擬器以及iOS13以下是沒有dyld3的,所以是走的dyld2啟動(dòng)流程催享。
dyld2
后面我們繼續(xù)分析dyld2的流程:
實(shí)例化主程序
// add dyld itself to UUID list
addDyldImageToUUIDList();
這里會(huì)添加?dyld?的鏡像文件到?UUID?列表中杭隙,主要的目的是啟用堆棧的符號(hào)化。
這里有個(gè)SUPPORT_ACCELERATE_TABLES宏睡陪,判斷是否支持加速器表寺渗。從注釋我們可以得出arm64架構(gòu)的情況下是不使用加速器表的。(后面還會(huì)有個(gè)有意思的判斷)
然后接著就是reloadAllImages:的一個(gè)標(biāo)簽兰迫⌒攀猓可以使用goto語句直接跳轉(zhuǎn)到這個(gè)標(biāo)簽,然后從這開始執(zhí)行代碼汁果。
這里插一個(gè)知識(shí)點(diǎn):ImageLoader
ImageLoader 是一個(gè)用于加載可執(zhí)行文件的基類涡拘,它負(fù)責(zé)鏈接鏡像,但不關(guān)心具體文件格式据德,因?yàn)檫@些都交給子類去實(shí)現(xiàn)鳄乏。每個(gè)可執(zhí)行文件都會(huì)對(duì)應(yīng)一個(gè) ImageLoader實(shí)例。ImageLoaderMachO 是用于加載 Mach-O 格式文件的 ImageLoader 子類棘利,而 ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 都繼承于 ImageLoaderMachO橱野,分別用于加載那些 __LINKEDIT 段為傳統(tǒng)格式和壓縮格式的 Mach-O 文件。
這一步將主程序的Mach-O加載進(jìn)內(nèi)存善玫,并實(shí)例化一個(gè)ImageLoader水援。instantiateFromLoadedImage()首先調(diào)用isCompatibleMachO()檢測(cè)Mach-O頭部的magic、cputype茅郎、cpusubtype等相關(guān)屬性蜗元,判斷Mach-O文件的兼容性,如果兼容性滿足系冗,則調(diào)用ImageLoaderMachO::instantiateMainExecutable()實(shí)例化主程序的ImageLoader奕扣。
ImageLoaderMachO::instantiateMainExecutable()函數(shù)里面首先會(huì)調(diào)用sniffLoadCommands()函數(shù)來獲取一些數(shù)據(jù),包括:
compressed:若Mach-O存在LC_DYLD_INFO和LC_DYLD_INFO_ONLY加載命令掌敬,則說明是壓縮類型的Mach-O
segCount:根據(jù) LC_SEGMENT_COMMAND 加載命令來統(tǒng)計(jì)段數(shù)量惯豆,這里拋出的錯(cuò)誤日志也說明了段的數(shù)量是不能超過255個(gè)
libCount:根據(jù) LC_LOAD_DYLIB池磁、LC_LOAD_WEAK_DYLIB、LC_REEXPORT_DYLIB循帐、LC_LOAD_UPWARD_DYLIB 這幾個(gè)加載命令來統(tǒng)計(jì)庫的數(shù)量娇跟,庫的數(shù)量不能超過4095個(gè)
當(dāng)sniffLoadCommands()解析完以后仲墨,根據(jù)compressed的值來決定調(diào)用哪個(gè)子類進(jìn)行實(shí)例化基括。
這里總結(jié)為4步:
ImageLoaderMachOCompressed::instantiateStart()創(chuàng)建ImageLoaderMachOCompressed對(duì)象簿姨。
image->disableCoverageCheck()禁用段覆蓋檢測(cè)沈撞。
image->instantiateFinish()首先調(diào)用parseLoadCmds()解析加載命令值骇,然后調(diào)用this->setDyldInfo()設(shè)置動(dòng)態(tài)庫鏈接信息萨赁,最后調(diào)用this->setSymbolTableInfo() 設(shè)置符號(hào)表相關(guān)信息透绩,代碼片段如下:
image->setMapped()函數(shù)注冊(cè)通知回調(diào)寻馏、計(jì)算執(zhí)行時(shí)間等等棋弥。
在調(diào)用完ImageLoaderMachO::instantiateMainExecutable()后繼續(xù)調(diào)用addImage(),將image加入到sAllImages全局鏡像列表诚欠,并將image映射到申請(qǐng)的內(nèi)存中顽染。
加載插入的動(dòng)態(tài)庫
這一步是加載環(huán)境變量DYLD_INSERT_LIBRARIES中配置的動(dòng)態(tài)庫,先判斷環(huán)境變量DYLD_INSERT_LIBRARIES中是否存在要加載的動(dòng)態(tài)庫轰绵,如果存在則調(diào)用loadInsertedDylib()依次加載粉寞。
loadInsertedDylib()內(nèi)部設(shè)置了一個(gè)LoadContext參數(shù)后,調(diào)用了load()函數(shù)左腔,
load()函數(shù)的實(shí)現(xiàn)為一系列的loadPhase*()函數(shù)唧垦,loadPhase0()~loadPhase1()函數(shù)會(huì)按照下面所示順序搜索動(dòng)態(tài)庫,并調(diào)用不同的函數(shù)來繼續(xù)處理液样。
DYLD_ROOT_PATH ? --> ?LD_LIBRARY_PATH ?--> ?DYLD_FRAMEWORK_PATH ||?DYLD_LIBRARY_PATH ?--> ?raw path ?--> ?DYLD_FALLBACK_LIBRARY_PATH
當(dāng)內(nèi)部調(diào)用到loadPhase5load()函數(shù)的時(shí)候振亮,會(huì)先在共享緩存中搜尋,如果存在則使用ImageLoaderMachO::instantiateFromCache()來實(shí)例化ImageLoader鞭莽,否則通過loadPhase5open()打開文件并讀取數(shù)據(jù)到內(nèi)存后坊秸,再調(diào)用loadPhase6(),通過ImageLoaderMachO::instantiateFromFile()實(shí)例化ImageLoader澎怒,最后調(diào)用checkandAddImage()驗(yàn)證鏡像并將其加入到全局鏡像列表中褒搔。
鏈接主程序
在調(diào)用link()進(jìn)行鏈接主程序之前,會(huì)有一個(gè)宏判斷丹拯,如果支持加速器表并且主程序已經(jīng)rebase了站超,則重新rebase一次,用于ASLR的工作乖酬。
這一步調(diào)用link()函數(shù)先將傳入的image添加進(jìn)sAllImages全局靜態(tài)數(shù)組死相,接著將image添加進(jìn)sImageRoots(后續(xù)遞歸初始化所有image的時(shí)候有用到)數(shù)組,然后將實(shí)例化后的主程序進(jìn)行動(dòng)態(tài)修正咬像,讓二進(jìn)制變?yōu)榭烧?zhí)行的狀態(tài)算撮。link()函數(shù)內(nèi)部調(diào)用了ImageLoader::link()函數(shù)
從源代碼可以看到生宛,這一步主要做了以下幾個(gè)事情:
recursiveLoadLibraries() 根據(jù)LC_LOAD_DYLIB加載命令把所有依賴庫加載進(jìn)內(nèi)存。
recursiveUpdateDepth() 遞歸刷新依賴庫的層級(jí)肮柜。
recursiveRebase() 由于ASLR的存在陷舅,必須遞歸對(duì)主程序以及依賴庫進(jìn)行重定位操作。
recursiveBind() 把主程序二進(jìn)制和依賴進(jìn)來的動(dòng)態(tài)庫全部執(zhí)行符號(hào)表綁定审洞。
weakBind() 如果鏈接的不是主程序二進(jìn)制的話莱睁,會(huì)在此時(shí)執(zhí)行弱符號(hào)綁定,主程序二進(jìn)制則在link()完后再執(zhí)行弱符號(hào)綁定芒澜,后面會(huì)進(jìn)行分析仰剿。
recursiveGetDOFSections()、context.registerDOFs() 注冊(cè)DOF(DTrace Object Format)節(jié)痴晦。
鏈接插入的動(dòng)態(tài)庫
這一步與鏈接主程序一樣南吮,將前面調(diào)用addImage()函數(shù)保存在sAllImages中的動(dòng)態(tài)庫列表循環(huán)取出并調(diào)用link()進(jìn)行鏈接,需要注意的是誊酌,sAllImages中保存的第一項(xiàng)是主程序的鏡像部凑,所以要從i+1的位置開始,取到的才是動(dòng)態(tài)庫的ImageLoader:
ImageLoader* image = sAllImages[i+1];
接下來循環(huán)調(diào)用每個(gè)鏡像的registerInterposing()函數(shù)碧浊,該函數(shù)會(huì)遍歷Mach-O的LC_SEGMENT_COMMAND加載命令涂邀,讀取__DATA,__interpose,并將讀取到的信息保存到fgInterposingTuples中辉词,為后續(xù)的bind做準(zhǔn)備工作必孤。
這里宏判斷加速器表,然后如果加速器表和隱試插入庫同時(shí)存在瑞躺,則禁用加速器表敷搪,并且使用goto 語句跳轉(zhuǎn)到reloadAllImages重新加載。
遞歸Bind
先調(diào)用applyInterposingToDyldCache()申請(qǐng)將所有鏈接的庫插入共享緩存幢哨,內(nèi)部將會(huì)找到鏈接庫的symbol符號(hào)表赡勘,為后面的Rebase做準(zhǔn)備。
接著執(zhí)行主程序的recursiveBindWithAccounting()函數(shù)捞镰,該函數(shù)內(nèi)部其實(shí)就是調(diào)用了recursiveBind()闸与,然后for循環(huán)對(duì)插入的庫執(zhí)行recursiveBind(),recursiveBind內(nèi)部先進(jìn)行遞歸所有依賴庫調(diào)用recursiveBind岸售,然后調(diào)用doBind()函數(shù)践樱,該函數(shù)有2個(gè)不一樣的實(shí)現(xiàn),分別在ImageLoaderMachOClassic.cpp 和?ImageLoaderMachOCompressed.cpp文件中凸丸。
ImageLoaderMachOClassic.cpp文件的實(shí)現(xiàn):調(diào)用doBindExternalRelocations 以及?bindIndirectSymbolPointers執(zhí)行真正的符號(hào)綁定拷邢。
ImageLoaderMachOCompressed.cpp文件的實(shí)現(xiàn):調(diào)用eachBind() 和eachLazyBind(),具體處理函數(shù)是bindAt()屎慢。
執(zhí)行弱符號(hào)綁定
weakBind()首先通過getCoalescedImages()合并所有動(dòng)態(tài)庫的弱符號(hào)到一個(gè)列表里瞭稼,然后調(diào)用initializeCoalIterator()對(duì)需要綁定的弱符號(hào)進(jìn)行排序忽洛,接著調(diào)用incrementCoalIterator()讀取dyld_info_command結(jié)構(gòu)的weak_bind_off和weak_bind_size字段,確定弱符號(hào)的數(shù)據(jù)偏移與大小环肘,最終進(jìn)行弱符號(hào)綁定欲虚。
執(zhí)行初始化方法(重點(diǎn))
這一步由initializeMainExecutable()完成。我們看看內(nèi)部的具體實(shí)現(xiàn):
先看注釋?run initialzers for any inserted dylibs 將已經(jīng)插入的動(dòng)態(tài)庫執(zhí)行初始化操作悔雹。
然后用一個(gè)for循環(huán)拿到每一個(gè)動(dòng)態(tài)庫的指針調(diào)用runInitializers()函數(shù)复哆。
繼續(xù)看注釋?run initializers for main executable and everything it brings up ,執(zhí)行主程序的初始化荠商。
所以dyld會(huì)優(yōu)先初始化鏈接的動(dòng)態(tài)庫寂恬,然后再初始化主程序。
這點(diǎn)很重要莱没,后面我會(huì)從源碼分析為什么dyld要這樣做!
runInitializers()內(nèi)部調(diào)用了processInitializers()
函數(shù)的注釋解釋:向上動(dòng)態(tài)庫初始化的執(zhí)行為時(shí)過早酷鸦。為了處理向上鏈接而不是向下鏈接的懸空動(dòng)態(tài)庫饰躲,所有向上鏈接的動(dòng)態(tài)庫都將其初始化延遲到通過向下動(dòng)態(tài)庫的遞歸完成之后。
什么意思呢臼隔,動(dòng)態(tài)庫直接也是有依賴關(guān)系的嘹裂,舉個(gè)例子:現(xiàn)在要初始化動(dòng)態(tài)庫A,但是動(dòng)態(tài)庫A依賴了動(dòng)態(tài)庫B摔握,動(dòng)態(tài)庫B又依賴了動(dòng)態(tài)庫C,D等寄狼,那么這里就會(huì)遞歸的先初始化C、D氨淌,然后再初始化B泊愧,最后才初始化A。
processInitializers內(nèi)部調(diào)用了recursiveInitialization()來進(jìn)行遞歸初始化盛正,我們?cè)賮砜纯磖ecursiveInitialization的實(shí)現(xiàn)
為了方便截圖删咱,筆著把for循環(huán)遞歸調(diào)用邏輯給折疊了,從注釋也可以知道低級(jí)的庫優(yōu)先初始化豪筝。這跟我們上面的分析是一樣的痰滋。然后我們重點(diǎn)看看紅框里的代碼,context上下文我們之前分析過了续崖,里面保存了很多函數(shù)的地址敲街,參數(shù)等。
紅框內(nèi)先調(diào)用了notifySingle严望,注意第一個(gè)參數(shù)dyld_image_state_dependents_initialized多艇,說明這次是調(diào)用的依賴庫的初始化,然后接著調(diào)用了doInitialization()著蟹,從函數(shù)名稱完全可以猜到這個(gè)函數(shù)才是真正去執(zhí)行初始化操作的墩蔓。接著再次調(diào)用notifySingle梢莽,看第一個(gè)參數(shù)dyld_image_state_initialized,這次是當(dāng)前庫的初始化奸披。我們先分析notifySingle的內(nèi)部實(shí)現(xiàn)(非常重要;杳!U竺妗)轻局,后面再分析doInitialization。
notifySingle函數(shù)里面唯一跟init有關(guān)的能調(diào)用的函數(shù)就是上圖紅色框里的sNotifyObjCInit样刷。全局搜索sNotifyObjCInit仑扑。只發(fā)現(xiàn)registerObjCNotifiers函數(shù)內(nèi)部的賦值操作。
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
看注釋:記錄函數(shù)用于調(diào)用置鼻。這里賦值了3個(gè)函數(shù)镇饮,一個(gè)mapped,一個(gè)init箕母,一個(gè)unmapped储藐。
接著全局搜索registerObjCNotifiers,發(fā)現(xiàn)是_dyld_objc_notify_register函數(shù)調(diào)用的嘶是。
全局搜索一下钙勃,發(fā)現(xiàn)在dyldAPIsInLibSystem.cpp文件中還有一個(gè)具體的實(shí)現(xiàn)。但是沒有發(fā)現(xiàn)有任何函數(shù)調(diào)用過_dyld_objc_notify_register聂喇。
上圖說明這個(gè)函數(shù)可能跟LibSystem.dylib有關(guān)辖源。
那么到底誰調(diào)用了_dyld_objc_notify_register()呢?靜態(tài)分析已經(jīng)無法得知希太,只能對(duì)_dyld_objc_notify_register()下個(gè)符號(hào)斷點(diǎn)觀察一下了克饶,
點(diǎn)擊Xcode的“Debug”菜單,然后點(diǎn)擊“Breakpoints”跛十,接著選擇“Create Symbolic Breakpoint...”彤路。然后添加_dyld_objc_notify_register,運(yùn)行工程芥映,如下圖所示
請(qǐng)看打印信息洲尊,調(diào)用順序如下:
最后發(fā)現(xiàn)是_objc_init()方法調(diào)用的,而且也驗(yàn)證了上面真正做初始化的函數(shù)是doInitialization()
我們把上面的幾個(gè)庫(libSystem奈偏、libdispatch坞嘀、libobjc)的源碼都下載下來驗(yàn)證一下!
從libSystem_init()到_objc_init()惊来,內(nèi)部調(diào)用順序和我們打印的確實(shí)一樣丽涩,最終調(diào)用了_dyld_objc_notify_register(),然后_objc_init函數(shù)就結(jié)束了。
那么問題來了矢渊,libSystem_initializer()什么時(shí)候調(diào)用的呢继准?
其實(shí)這個(gè)答案在前文分析initializeMainExecutable()源碼的時(shí)候就已經(jīng)講過了。dyld會(huì)優(yōu)先初始化動(dòng)態(tài)庫矮男,然后初始化主程序移必。libSystem_initializer()就是在這個(gè)時(shí)候調(diào)用的。
我們回到_dyld_objc_notify_register函數(shù)來毡鉴,看看這3個(gè)參數(shù)崔泵,都是函數(shù)指針
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
最后得到
sNotifyObjCMapped = map_images;
sNotifyObjCInit = load_images;
sNotifyObjCUnmapped = unmap_image;
也就是說sNotifyObjCInit的調(diào)用其實(shí)就是調(diào)用load_images();
由于篇幅的原因,load_images的分析我們放到下一節(jié)猪瞬。
前文提到doInitialization()才是真正執(zhí)行初始化的函數(shù)憎瘸,我們回過頭來繼續(xù)分析doInitialization()的具體實(shí)現(xiàn):
內(nèi)部調(diào)用doImageInit() 和?doModInitFunctions()
這2個(gè)函數(shù)內(nèi)部都有l(wèi)ibSystemInitialized的判斷,要求libSystem這個(gè)動(dòng)態(tài)庫必須先初始化陈瘦,這也驗(yàn)證了我們前面的結(jié)論是對(duì)的幌甘。
然后調(diào)用func執(zhí)行初始化方法。假設(shè)當(dāng)前是libSystem庫痊项,那么這個(gè)func就是libSystem_initializer()
到此含潘,所有的初始化方法調(diào)用已經(jīng)閉環(huán)。
查找入口點(diǎn)并返回
這一步調(diào)用主程序鏡像的getEntryFromLC_MAIN()线婚,從加載命令讀取LC_MAIN入口,如果沒有LC_MAIN就調(diào)用getEntryFromLC_UNIXTHREAD()讀取LC_UNIXTHREAD盆均,找到后就跳到入口點(diǎn)指定的地址并返回塞弊。
至此,整個(gè)dyld的加載過程就分析完成了泪姨。
總結(jié)下:
dyld3的加載流程如下:
第一步:設(shè)置運(yùn)行環(huán)境游沿。
第二步:加載共享緩存。
第三步:檢查啟動(dòng)閉包肮砾。
第四步:構(gòu)建啟動(dòng)閉包诀黍。
第五步:加載啟動(dòng)閉包。(這一步包括dyld2中的3仗处、4眯勾、5、6婆誓、7吃环、8)
第六步:找到入口點(diǎn)并返回。
dyld2的加載流程如下:
第一步:設(shè)置運(yùn)行環(huán)境洋幻。
第二步:加載共享緩存郁轻。
第三步:實(shí)例化主程序。
第四步:加載插入的動(dòng)態(tài)庫。
第五步:鏈接主程序好唯。
第六步:鏈接插入的動(dòng)態(tài)庫竭沫。
第七步:執(zhí)行弱符號(hào)綁定
第八步:執(zhí)行初始化方法。
第九步:查找入口點(diǎn)并返回骑篙。
那么我們現(xiàn)在既然了解了dyld的加載流程蜕提,那么在這個(gè)階段我們能做什么具體的優(yōu)化呢?
1替蛉、避免鏈接無用的 frameworks贯溅,在 Xcode 中檢查一下項(xiàng)目中的「Linked Frameworks and Librares」部分是否有無用的鏈接
2、避免在啟動(dòng)時(shí)加載動(dòng)態(tài)庫躲查,將項(xiàng)目的 Pods 以靜態(tài)編譯的方式打包它浅,尤其是 Swift 項(xiàng)目,這地方的時(shí)間損耗是很大的
3镣煮、硬鏈接你的依賴項(xiàng)姐霍,這里做了緩存優(yōu)化
也許有人會(huì)有疑惑,現(xiàn)在都使用了 dyld3 了典唇,我們就不需要做 Static Link 了镊折,其實(shí)還是需要的,這里放2張對(duì)比圖給大家看看介衔。
Dyld 2 和 Dyld 3啟動(dòng)時(shí)間的對(duì)比:
Static linking 和 Dyld 3啟動(dòng)時(shí)間對(duì)比:
結(jié)果很明顯恨胚,可以看出,在冷啟動(dòng)時(shí)Dyld3 比 Dyld2快20%炎咖,靜態(tài)鏈接依舊是比dyld3要快赃泡,提升了23%左右。
dyld2加載流程圖如下: