引言:
眾所周知,我們的iOS應(yīng)用是通過Dyld進(jìn)行加載的,那么Dyld是如何加載我們的應(yīng)用的,它的流程是怎樣的,下面我們把dyld的加載分為幾個(gè)步驟做個(gè)簡短的分析。
1 dyld的start啟動(dòng)
首先我們創(chuàng)建一個(gè)Demo工程,在我們的AppDelegate.m文件里加入+(load)方法并斷點(diǎn),如下圖所示:

運(yùn)行Demo App后,可以得到所下圖

從圖2中我們可以看到,我們的App是從_dyld_start開始的,我們點(diǎn)擊dyld_start,看到匯編的第18行代碼,這里調(diào)用了dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*)這行代碼,我們再使用bt命令看下詳細(xì)的堆棧,如下圖

從這里可以看出一切的開始是從_dyld_start開始的,這個(gè)dyld_start在可以在dyld的源碼里找(注:這里使用的dyld的源碼的版本是dyld-832.7.3),下面我們打開dyld的源碼進(jìn)行分析
我們在dyld的源碼里找?dyldbootstrap這個(gè)命名空間,按住shift+comand+j進(jìn)入文件,所下圖所示

從這里可以看到是在dyldInitialization.cpp文件里,然后我們在文件里搜索start,找到uintptr_tstart(constdyld3::MachOLoaded* appsMachHeader,intargc,constchar* argv[],
constdyld3::MachOLoaded* dyldsMachHeader,uintptr_t* startGlue)函數(shù),如圖所示:

我們從start函數(shù)逐步往下分析整個(gè)流程,我們先看下參數(shù),appsMachHeader是我們App的MachHeader,dyldsMachHeader是dyld的MachHeader。
第121行代碼是告訴我們的degbugServer,我的dyld開始起動(dòng)了。
第125行代碼rebaseDyld(dyldsMachHeader) 重定位我們的dyld。
第136,143行是棧溢出保護(hù)和初始化dyld,之后就是調(diào)用dyld的main函數(shù)(這是最核心的),我們著重分析下dyld的main函數(shù)流程。

main函數(shù)的前幾行代碼都是代碼檢測相關(guān)的,不是核心內(nèi)容
我們下面來看主程序的配置相關(guān),如圖

這些是配置主程序的MachHeader(就是Macho的頭),主程序的Slide(就是主程序的ASLR的偏移值,每次啟動(dòng)都是不一樣的)
下面調(diào)用setContext(mainExecutableMH, argc, argv, envp, apple);保存我們配置的信息

然后通過configureProcessRestrictions(mainExecutableMH, envp)這個(gè)函數(shù)配置進(jìn)程受限制(AMFI相關(guān)(Apple Mobile File Integrity蘋果移動(dòng)文件保護(hù))),下圖都是進(jìn)程受限相關(guān)的配置,比如是否強(qiáng)制使用dyld3(dyld是在iOS11推出來的,加載高效)

下圖打印我們的環(huán)境變量,這個(gè)環(huán)境變理可通過 Environment Variables配置

以下都是dyld的啟動(dòng),配置,以及主程序的相關(guān)配置和一些代碼檢測的流程,下面我們來分析共享緩存
2 dyld加載共享緩存

點(diǎn)擊進(jìn)去checkSharedRegionDisable發(fā)現(xiàn)有一個(gè)“iOS cannot run without shared region”說明,這是表明我們的iOS是一定有共享緩存的。
接著調(diào)用mapSharedCache傳進(jìn)去主程序的Slide,這個(gè)函數(shù)調(diào)用了loadDyldCache加載我們的dyld庫存,如下圖所示

滿足options.forcePrivate 這個(gè)條件的話,只加載當(dāng)前進(jìn)程
reuseExistingCache如果緩存已經(jīng)加載不再處理,如果第一次加載執(zhí)行mapCacheSystemWide這個(gè)函數(shù)
通過以上分析,可以得出結(jié)論,動(dòng)態(tài)庫的共享緩存是最先被加載的(我們自己開發(fā)的動(dòng)態(tài)庫不可以)。從iOS11引入了dyld3的ClosureMode(閉包模式加載更快),下面我們來分析一下
3 dyld3的閉包模式

這里先判斷閉包模式是否打開,如果沒有的話將會走dyld2的流程,打開走dyld3的流程(dyld2,dyld3的加載流程一致),下面我們來分析dyld3的閉包模式

先從共享緩存中查找這個(gè)實(shí)例,如果拿到就先驗(yàn)證

這里判斷是否查找成功,并且驗(yàn)證閉包的有效性,如果失效,sLaunchModeUsed設(shè)置為NULL

這里如果沒找到,再去緩存中查一次,如果mainClosure為空,這里就調(diào)用buildLaunchClosure創(chuàng)建閉包實(shí)例
最終拿到這個(gè)mainClosure實(shí)例啟動(dòng)這個(gè)實(shí)例,如下圖所示

如果啟動(dòng)失敗或者閉包過期,這里就再重新調(diào)用buildLaunchClosure創(chuàng)建并調(diào)用launchWithClosure重新啟動(dòng)一次。
啟動(dòng)成功后設(shè)置gLinkContext.startedInitializingMainExecutable = true;這個(gè)主程序加載成功了。
同時(shí)返回結(jié)果result(即主程序的main),如下圖所示:

接著就會實(shí)例化我們的主程序了,下面我們來分析是如何加載的。
4 dyld加載主程序
接下看下怎么實(shí)例化主程序的,如下圖所示

第6862行代碼會調(diào)用instantiateFromLoadedImage函數(shù)實(shí)例化我們的主程序,我們來看下instantiateFromLoadedImage這個(gè)函數(shù),下圖所示:

這里通過ImageLoaderMachO這個(gè)函數(shù)傳image的macho_header,ASLR的偏移值,路徑生成ImageLoader對象,然后調(diào)用addImage這個(gè)函數(shù)加入我們的鏡像文件,同時(shí)返回ImageLoader這個(gè)對象。(通過dyld加載的第一個(gè)鏡像是我們的主程序),我們來看下instantiateMainExecutable的這個(gè)函數(shù)的流程,如圖所示:

這里調(diào)用sniffLoadCommands獲取loadCommands,如圖:

compressed是根據(jù)Macho中的LG_DYLD_INFO_ONLY和LG_LOAD_DYLINKER來獲取的。
segCount是SEGMENT的數(shù)量,最大不能超過255。
libCount是LC_LOAD_DYLIB加載動(dòng)態(tài)庫的個(gè)數(shù),最大不能超過4095。
*codeSigCmd是代碼簽名。
*encryptCmd是代碼加密信息。
ImageLoaderMachO這個(gè)函數(shù)調(diào)用sniffLoadCommands這個(gè)之后會根據(jù)compressed這個(gè)變量判斷調(diào)用ImageLoaderMachOCompressed或者ImageLoaderMachOClassic這兩個(gè)函數(shù)實(shí)例化。
實(shí)例化完畢之后添加到AllImage中。

接著檢測當(dāng)前主程否是當(dāng)前設(shè)備的,如上圖所示,到這里我們的主程序?qū)嵗Y(jié)束,接著我們來分析動(dòng)態(tài)庫的加載。
5 dyld加載動(dòng)態(tài)庫

這里先檢查動(dòng)態(tài)庫的版本和路徑,接著加載動(dòng)態(tài)庫,如圖所示:

這里先根據(jù)環(huán)境變量判斷動(dòng)態(tài)插入庫不為空,接著遍歷loadInsertedDylib

這里調(diào)用load插入動(dòng)態(tài)庫,接著開始鏈接主程序,

先配置gLinkContext.linkingMainExecutable = true;這個(gè)變量為true.
接著調(diào)用link函數(shù)進(jìn)行鏈接,我們來看看是如何鏈接的:



這里先記錄起始時(shí)間,在最后在記錄結(jié)束時(shí)間,把加載時(shí)間記錄下來,這個(gè)就是dyld加載應(yīng)用的時(shí)長。
這里鏈接插入動(dòng)態(tài)庫完成了。

之后把這些實(shí)例化的鏡像文件加入到AllImages中(這里是從i+1開始的,因?yàn)橹鞒绦蛞呀?jīng)先加載了),之后再調(diào)用link進(jìn)行鏈接,這里跟主程序的鏈接是一樣的。

這里的條件不滿足的話,將會持續(xù)的調(diào)用reloadAllImage,這里執(zhí)行之后,就開始綁定動(dòng)態(tài)庫了,如圖所示:

這里遍歷AllImages綁定插入動(dòng)態(tài)庫,之后進(jìn)行弱符號綁定。
接著調(diào)用initializeMainExecutable初始化主程序的Main方法,如圖所示:

下面我們來分析主程序Main方法加載的流程。
6? load方法與初始化方法的加載
我們先進(jìn)入initializeMainExecutable()這個(gè)函數(shù),看下它的實(shí)現(xiàn)

這里有一個(gè)runInitializers函數(shù),我們再進(jìn)去

這里會調(diào)用processInitializers這個(gè)函數(shù),我們再跟進(jìn)去看看

接著我們再跟下recursiveInitialization這個(gè)函數(shù)


這里調(diào)用了notifySingle這個(gè)函數(shù),我們需要再跟進(jìn)去一下,


而這里沒有找到loadImge的函數(shù)調(diào)用,這里到底是怎么回事,我們通過匯編可以看到load_image是在libobjc.dylib中,也就是說在objc的源碼中,那它是怎么調(diào)用的,我們來看下代碼。
在1019行中 (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()) 這個(gè)回調(diào)進(jìn)行關(guān)聯(lián)的。
這里首先判斷sNotifyObjCInit這個(gè)是否為空,我們在這文件里搜下,發(fā)現(xiàn)是在registerObjCNotifiers這里調(diào)用的時(shí)候賦值的,如下圖:

我們搜下registerObjCNotifiers這個(gè)函數(shù)發(fā)現(xiàn)是在dyldAPIS.cpp中的

這個(gè)函數(shù)調(diào)用的,這里有傳遞init進(jìn)來,那么又是誰調(diào)用的_dyld_objc_notify_register這個(gè)函數(shù)呢,搜了之后,發(fā)現(xiàn)dyld里沒用調(diào)用的,那么怎么辦呢,我們可以在Demo工程中下一個(gè)符號斷點(diǎn)_dyld_objc_notify_register,結(jié)果發(fā)現(xiàn)是在libobjc.dylib中的_objc_init調(diào)用的,下面打開objc的的源代碼,按信shift+command+o找到定義,再按住shift+command+j找到源文件是在objc-os.mm文件中,如圖所示:

這里可以看到_dyld_objc_notify_register這個(gè)函數(shù)是在_objc_init調(diào)用的,這里有一個(gè)load_images,我們再看下

這里面有一個(gè)call_load_methods方法,點(diǎn)進(jìn)去看下

這里會do while調(diào)用call_class_loads方法來加載所有類的+(void)load方法,load方法加載完成后調(diào)用了call_category_loads這個(gè)方法,加載類加的loads方法,這也是為什么類別的方法與原類的方法重名后,會覆蓋原類的方法。
我們回到dyld的源代碼找到ImageLoader.cpp文件中的recursiveInitialization函數(shù)中調(diào)用notifySingle這里走到了objc中,objc把所有的load加載完成后,會調(diào)用doInitialization這個(gè)函數(shù),進(jìn)去看下

這里doModInitFunctions調(diào)用這個(gè)函數(shù),這個(gè)函數(shù)的作用是什么,我們來看下,

這里就是在加載我們的構(gòu)造函數(shù),我們在Demo的main.m上面加入構(gòu)造函數(shù)
__attribute__((constructor)) void test1() {
?printf("test調(diào)用了");
}
經(jīng)過調(diào)試,它比main函數(shù)先調(diào)用。
我們再回到dyld的main函數(shù),找到這里,如圖

這里通過LC_MAIN找到程序入口給result,最后返回主程序的main地址。dyld的加載就結(jié)束了。
下面是dyld的initializeMainExecutable初始化主程序的思維導(dǎo)圖
