引言:
眾所周知座韵,我們的iOS應(yīng)用是通過(guò)Dyld進(jìn)行加載的,那么Dyld是如何加載我們的應(yīng)用的换可,它的流程是怎樣的,下面我們把dyld的加載分為幾個(gè)步驟做個(gè)簡(jiǎn)短的分析厦幅。
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*)這行代碼吞歼,我們?cè)偈褂胋t命令看下詳細(xì)的堆棧,如下圖
從這里可以看出一切的開始是從_dyld_start開始的塔猾,這個(gè)dyld_start在可以在dyld的源碼里找(注:這里使用的dyld的源碼的版本是dyld-832.7.3),下面我們打開dyld的源碼進(jìn)行分析
我們?cè)赿yld的源碼里找?dyldbootstrap這個(gè)命名空間篙骡,按住shift+comand+j進(jìn)入文件,所下圖所示
從這里可以看到是在dyldInitialization.cpp文件里,然后我們?cè)谖募锼阉鱯tart丈甸,找到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ù)的前幾行代碼都是代碼檢測(cè)相關(guān)的今膊,不是核心內(nèi)容
我們下面來(lái)看主程序的配置相關(guān)些阅,如圖
這些是配置主程序的MachHeader(就是Macho的頭),主程序的Slide(就是主程序的ASLR的偏移值斑唬,每次啟動(dòng)都是不一樣的)
下面調(diào)用setContext(mainExecutableMH, argc, argv, envp, apple);保存我們配置的信息
然后通過(guò)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推出來(lái)的恕刘,加載高效)
下圖打印我們的環(huán)境變量缤谎,這個(gè)環(huán)境變理可通過(guò) Environment Variables配置
以下都是dyld的啟動(dòng),配置褐着,以及主程序的相關(guān)配置和一些代碼檢測(cè)的流程坷澡,下面我們來(lái)分析共享緩存
2 dyld加載共享緩存
點(diǎn)擊進(jìn)去checkSharedRegionDisable發(fā)現(xiàn)有一個(gè)“iOS cannot run without shared region”說(shuō)明,這是表明我們的iOS是一定有共享緩存的含蓉。
接著調(diào)用mapSharedCache傳進(jìn)去主程序的Slide频敛,這個(gè)函數(shù)調(diào)用了loadDyldCache加載我們的dyld庫(kù)存,如下圖所示
滿足options.forcePrivate 這個(gè)條件的話项郊,只加載當(dāng)前進(jìn)程
reuseExistingCache如果緩存已經(jīng)加載不再處理,如果第一次加載執(zhí)行mapCacheSystemWide這個(gè)函數(shù)
通過(guò)以上分析斟赚,可以得出結(jié)論着降,動(dòng)態(tài)庫(kù)的共享緩存是最先被加載的(我們自己開發(fā)的動(dòng)態(tài)庫(kù)不可以)。從iOS11引入了dyld3的ClosureMode(閉包模式加載更快)拗军,下面我們來(lái)分析一下
3 dyld3的閉包模式
這里先判斷閉包模式是否打開任洞,如果沒(méi)有的話將會(huì)走dyld2的流程,打開走dyld3的流程(dyld2,dyld3的加載流程一致),下面我們來(lái)分析dyld3的閉包模式
先從共享緩存中查找這個(gè)實(shí)例发侵,如果拿到就先驗(yàn)證
這里判斷是否查找成功交掏,并且驗(yàn)證閉包的有效性,如果失效器紧,sLaunchModeUsed設(shè)置為NULL
這里如果沒(méi)找到耀销,再去緩存中查一次,如果mainClosure為空铲汪,這里就調(diào)用buildLaunchClosure創(chuàng)建閉包實(shí)例
最終拿到這個(gè)mainClosure實(shí)例啟動(dòng)這個(gè)實(shí)例熊尉,如下圖所示
如果啟動(dòng)失敗或者閉包過(guò)期,這里就再重新調(diào)用buildLaunchClosure創(chuàng)建并調(diào)用launchWithClosure重新啟動(dòng)一次掌腰。
啟動(dòng)成功后設(shè)置gLinkContext.startedInitializingMainExecutable = true;這個(gè)主程序加載成功了狰住。
同時(shí)返回結(jié)果result(即主程序的main),如下圖所示:
接著就會(huì)實(shí)例化我們的主程序了齿梁,下面我們來(lái)分析是如何加載的催植。
4 dyld加載主程序
接下看下怎么實(shí)例化主程序的,如下圖所示
第6862行代碼會(huì)調(diào)用instantiateFromLoadedImage函數(shù)實(shí)例化我們的主程序勺择,我們來(lái)看下instantiateFromLoadedImage這個(gè)函數(shù)创南,下圖所示:
這里通過(guò)ImageLoaderMachO這個(gè)函數(shù)傳image的macho_header,ASLR的偏移值,路徑生成ImageLoader對(duì)象省核,然后調(diào)用addImage這個(gè)函數(shù)加入我們的鏡像文件稿辙,同時(shí)返回ImageLoader這個(gè)對(duì)象。(通過(guò)dyld加載的第一個(gè)鏡像是我們的主程序),我們來(lái)看下instantiateMainExecutable的這個(gè)函數(shù)的流程气忠,如圖所示:
這里調(diào)用sniffLoadCommands獲取loadCommands,如圖:
compressed是根據(jù)Macho中的LG_DYLD_INFO_ONLY和LG_LOAD_DYLINKER來(lái)獲取的邻储。
segCount是SEGMENT的數(shù)量,最大不能超過(guò)255旧噪。
libCount是LC_LOAD_DYLIB加載動(dòng)態(tài)庫(kù)的個(gè)數(shù),最大不能超過(guò)4095吨娜。
*codeSigCmd是代碼簽名。
*encryptCmd是代碼加密信息淘钟。
ImageLoaderMachO這個(gè)函數(shù)調(diào)用sniffLoadCommands這個(gè)之后會(huì)根據(jù)compressed這個(gè)變量判斷調(diào)用ImageLoaderMachOCompressed或者ImageLoaderMachOClassic這兩個(gè)函數(shù)實(shí)例化宦赠。
實(shí)例化完畢之后添加到AllImage中。
接著檢測(cè)當(dāng)前主程否是當(dāng)前設(shè)備的,如上圖所示,到這里我們的主程序?qū)嵗Y(jié)束勾扭,接著我們來(lái)分析動(dòng)態(tài)庫(kù)的加載缤骨。
5 dyld加載動(dòng)態(tài)庫(kù)
這里先檢查動(dòng)態(tài)庫(kù)的版本和路徑,接著加載動(dòng)態(tài)庫(kù),如圖所示:
這里先根據(jù)環(huán)境變量判斷動(dòng)態(tài)插入庫(kù)不為空尺借,接著遍歷loadInsertedDylib
這里調(diào)用load插入動(dòng)態(tài)庫(kù)绊起,接著開始鏈接主程序,
先配置gLinkContext.linkingMainExecutable = true;這個(gè)變量為true.
接著調(diào)用link函數(shù)進(jìn)行鏈接燎斩,我們來(lái)看看是如何鏈接的:
這里先記錄起始時(shí)間虱歪,在最后在記錄結(jié)束時(shí)間,把加載時(shí)間記錄下來(lái)栅表,這個(gè)就是dyld加載應(yīng)用的時(shí)長(zhǎng)笋鄙。
這里鏈接插入動(dòng)態(tài)庫(kù)完成了。
之后把這些實(shí)例化的鏡像文件加入到AllImages中(這里是從i+1開始的怪瓶,因?yàn)橹鞒绦蛞呀?jīng)先加載了)萧落,之后再調(diào)用link進(jìn)行鏈接,這里跟主程序的鏈接是一樣的洗贰。
這里的條件不滿足的話找岖,將會(huì)持續(xù)的調(diào)用reloadAllImage,這里執(zhí)行之后敛滋,就開始綁定動(dòng)態(tài)庫(kù)了许布,如圖所示:
這里遍歷AllImages綁定插入動(dòng)態(tài)庫(kù),之后進(jìn)行弱符號(hào)綁定绎晃。
接著調(diào)用initializeMainExecutable初始化主程序的Main方法,如圖所示:
下面我們來(lái)分析主程序Main方法加載的流程蜜唾。
6? load方法與初始化方法的加載
我們先進(jìn)入initializeMainExecutable()這個(gè)函數(shù),看下它的實(shí)現(xiàn)
這里有一個(gè)runInitializers函數(shù)庶艾,我們?cè)龠M(jìn)去
這里會(huì)調(diào)用processInitializers這個(gè)函數(shù)袁余,我們?cè)俑M(jìn)去看看
接著我們?cè)俑聄ecursiveInitialization這個(gè)函數(shù)
這里調(diào)用了notifySingle這個(gè)函數(shù),我們需要再跟進(jìn)去一下咱揍,
而這里沒(méi)有找到loadImge的函數(shù)調(diào)用颖榜,這里到底是怎么回事,我們通過(guò)匯編可以看到load_image是在libobjc.dylib中述召,也就是說(shuō)在objc的源碼中朱转,那它是怎么調(diào)用的蟹地,我們來(lái)看下代碼积暖。
在1019行中 (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()) 這個(gè)回調(diào)進(jìn)行關(guān)聯(lián)的。
這里首先判斷sNotifyObjCInit這個(gè)是否為空怪与,我們?cè)谶@文件里搜下夺刑,發(fā)現(xiàn)是在registerObjCNotifiers這里調(diào)用的時(shí)候賦值的,如下圖:
我們搜下registerObjCNotifiers這個(gè)函數(shù)發(fā)現(xiàn)是在dyldAPIS.cpp中的
這個(gè)函數(shù)調(diào)用的,這里有傳遞init進(jìn)來(lái)遍愿,那么又是誰(shuí)調(diào)用的_dyld_objc_notify_register這個(gè)函數(shù)呢存淫,搜了之后,發(fā)現(xiàn)dyld里沒(méi)用調(diào)用的沼填,那么怎么辦呢桅咆,我們可以在Demo工程中下一個(gè)符號(hào)斷點(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,我們?cè)倏聪?/p>
這里面有一個(gè)call_load_methods方法梯澜,點(diǎn)進(jìn)去看下
這里會(huì)do while調(diào)用call_class_loads方法來(lái)加載所有類的+(void)load方法寞冯,load方法加載完成后調(diào)用了call_category_loads這個(gè)方法,加載類加的loads方法晚伙,這也是為什么類別的方法與原類的方法重名后吮龄,會(huì)覆蓋原類的方法。
我們回到dyld的源代碼找到ImageLoader.cpp文件中的recursiveInitialization函數(shù)中調(diào)用notifySingle這里走到了objc中咆疗,objc把所有的load加載完成后螟蝙,會(huì)調(diào)用doInitialization這個(gè)函數(shù),進(jìn)去看下
這里doModInitFunctions調(diào)用這個(gè)函數(shù)民傻,這個(gè)函數(shù)的作用是什么胰默,我們來(lái)看下,
這里就是在加載我們的構(gòu)造函數(shù),我們?cè)贒emo的main.m上面加入構(gòu)造函數(shù)
__attribute__((constructor)) void test1() {
?printf("test調(diào)用了");
}
經(jīng)過(guò)調(diào)試漓踢,它比main函數(shù)先調(diào)用牵署。
我們?cè)倩氐絛yld的main函數(shù),找到這里喧半,如圖
這里通過(guò)LC_MAIN找到程序入口給result奴迅,最后返回主程序的main地址。dyld的加載就結(jié)束了挺据。
下面是dyld的initializeMainExecutable初始化主程序的思維導(dǎo)圖