背景
大家都知道iOS在加載app的時候本質(zhì)其實是加載app中的MachO文件,那么MachO文件又是誰來進行加載與執(zhí)行的呢?其中的過程又是如何的?我們就來初探一下MachO文件的加載順序。
一、什么是Mach-O文件?
1.1、初識Mach-O
要想了解MachO文件的加載順序,首先我們要先了解一下什么是MachO文件。MachO屬于一種文件格式,其中包含了可執(zhí)行文件、靜態(tài)庫、動態(tài)庫、dyld等;其中包含的可執(zhí)行文件是集合了多種架構(gòu)的,例如包含了 armv7、arm64等;
1.2、MachO的結(jié)構(gòu):

Header:用于快速確定該文件的CPU類型、文件類型;
Load Commands:指示加載器如何設(shè)置并且加載二進制數(shù)據(jù);
Text:存放代碼。
Data:存放數(shù)據(jù)。例如:數(shù)據(jù)、字符串常量、類、方法等;
1.3、如何找到Mach-O文件?
我們創(chuàng)建一個新項目然后編譯,在Products下會生成一個項目名.app文件,我們右鍵 show in finder 然后再右鍵顯示包內(nèi)容,會看到一個黑框的文件,該文件就是MachO文件。
Mach-O是Machobject文件格式的縮寫,它是一種用于可執(zhí)行文件、目標代碼、動態(tài)庫的文件格式。作為a.out格式的替代,與a.out格式比較Mach-O提供了更強的擴展性。



1.4、如何查看Mach-O文件?
通過終端命令 otool -l +文件名 進行查看,但是命令顯示的內(nèi)容太多了我們不好看,可以通過終端命令 otool -l +文件名 > 輸出路徑 將內(nèi)容輸出成文件,但是打開文件還是不太好看,這時我們就該利用工具了。例如:

既然MachO屬于一種文件格式,那么就一定有解析這種格式的方法與程序,那么對MachO文件進行解析執(zhí)行的就是DYLD。
二、DYLD
DYLD(the dynamic link editor)是蘋果的動態(tài)鏈接器,是蘋果操作系統(tǒng)一個重要組成部分,在系統(tǒng)內(nèi)核做好程序準備工作之后,交由dyld負責余下的工作。而且它是開源的,任何人可以通過蘋果官網(wǎng)下載它的源碼來閱讀理解它的運作方式,了解系統(tǒng)加載動態(tài)庫的細節(jié)。官網(wǎng)地址:https://opensource.apple.com/
2.1、新建一個項目
1、首先我們要先了解一下App啟動時候的運行順序,那么一個App入口就是mian.m文件里面的mian函數(shù),我們現(xiàn)在在main函數(shù)中打印一句話標記一下。

2、App通過了main函數(shù)的之后會調(diào)用AppDelegate,然后最終會引導(dǎo)到ViewController界面上,那我們就在ViewController中增加一個load函數(shù),這里問什么要加load函數(shù)是因為load函數(shù)一定是最先被加載的,我們的目的是為了查看App的啟動順序,所以load函數(shù)最合適,接下來我們還會驗證為什么load函數(shù)是最先加載的。



4、我們現(xiàn)在運行看一下打印結(jié)果是什么,建議最好用真機進行測試。

2.2、如何找到DYLD的加載入口?
-
首先我們現(xiàn)在知道的就是App入口都是通過mian.m文件里面的mian函數(shù)開始的,但根據(jù)上面的測試打印結(jié)果我們發(fā)現(xiàn)了其實framework的load是最先被調(diào)用的,那么我們就在framework的load函數(shù)中增加一個斷點再運行一下看看。
然后我們在右側(cè)查看調(diào)用流程,我們就看到了在App的main函數(shù)之前的調(diào)用流程了。
通過調(diào)用流程我們發(fā)現(xiàn),最開始是調(diào)用了了一個叫 _dyld_start的函數(shù),然后通過dyld的main函數(shù)繼續(xù)進行調(diào)用,然后通過調(diào)用ImageLoader 的函數(shù)調(diào)用notifySingle 后再load_images,最終調(diào)用到了我們framework中的load函數(shù)。image
根據(jù)上面的分析,我們已經(jīng)對DYLD的加載順序有了一個大致的了解,那么大家有沒有興趣跟著我,把DYLD詳細的加載流程走一遍呢?Let‘s go !
三、DYLD加載探究
3.1、前期準備資料:
我要分析DYLD的加載流程就必須要下載DYLD的源代碼,下面給出下載地址https://opensource.apple.com;
注意!要下載DYLD源碼點擊macOS,這里我們選擇11.2這個版本,點進去后搜索dyld 會找到dyld-832.7.3 ;除了dyld之外還需要下載objc4-818.2

3.2、分析過程:
3.2.1、start函數(shù):
start函數(shù)的內(nèi)容不多,我們簡要的分析一下;首先我們打開dyld源碼,然后Command+shift+O 搜索start,然后Command+shift+J 定位文件。


執(zhí)行__guard_setup(apple); 這一步是為了進行棧溢出保護;
執(zhí)行_subsystem_init(apple); 這一步是初始化相關(guān)數(shù)據(jù);
最后執(zhí)行一個返回return dyld::_main((macho_header)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue); 返回調(diào)用的是dyld的_main函數(shù),是具體加載步驟。這里的返回值也是一個main函數(shù),而這個返回的main函數(shù)就是就是咱們App項目中main.m中的main函數(shù),所以這個dyld的main函數(shù)就是一個尋找、加載、初始化這個App的main函數(shù)的過程,那么所有的加載邏輯都是由這個函數(shù)來執(zhí)行的,我們繼續(xù)跟進下去。
3.2.2、main函數(shù):
(1)、初期配置:
我們繼續(xù)跟蹤dyld::_main函數(shù),剛一進來就看到這個函數(shù)的很大,將近1千多行的代碼。我們用不著每行都看一遍,只要抓住幾個重點步驟就行了。
-
大概再6473行左右會出現(xiàn)一個setContext(mainExecutableMH, argc, argv, envp, apple);函數(shù),從_mian函數(shù)開始到這里都是在做一些配置的信息,把配置好的信息保存起來,當前這步的含義是用過調(diào)用setContext上下文,將括號內(nèi)的參數(shù)信息保存起來,我們可以再看一下setContext里面的內(nèi)容,就會發(fā)現(xiàn)其實上下文都是通過一個叫g(shù)LinkContext的來進行保存的。到這里只是初步設(shè)置,如果下面的信息發(fā)生了改變還會進行更新。
image
接下來往下50行左右,configureProcessRestrictions; 函數(shù)開始再到s是對進程進行了受限配置,進程是受AMFI(Apple Mobile File Integrity蘋果移動文件保護)內(nèi)核模塊,用來檢查一些參數(shù)的存在性。最后我們可以看到又執(zhí)行了setContext,只是因為上面的進行保護可能會引起一些環(huán)境變量發(fā)送改版,所以需要再一次重新進行保存。

再往下走到defaultUninitializedFallbackPaths,目前到這里,都是配置和初始化環(huán)境,還沒有加載程序。

繼續(xù)往下我們會看到兩個環(huán)境變量sEnv.DYLD_PRINT_OPTS 和 sEnv.DYLD_PRINT_ENV,通過代碼我們發(fā)現(xiàn)如果配置了這兩個變量就會執(zhí)行下面的pint方法,我們可以通過在項目中的target里設(shè)置,讓他們進行打印。



(2)、加載共享緩存:

再往下幾行,我們發(fā)現(xiàn)了checkSharedRegionDisable這個函數(shù),從這往下到mapSharedCache都是在加載共享緩存,例如:UIKit,F(xiàn)oundation框架。這里要再提一下iOS的共享緩存的意義。

(3)、DYLD加載方式:
經(jīng)過了以上的配置、加載共享緩存的步驟之后,DYLD現(xiàn)在要正式開始,目前DYLD的執(zhí)行方式分為2種;Dyld2 和 Dyld3。
(3-1)、DYLD3方式:
iOS11 之后增加了Dyld3 通過使用Closure閉包方式進行加載,這種方式比之前Dyld2的效率更高效,但本質(zhì)的流程還是與Dyld2一致的,我們可以快速的看一下。先從共享緩存中查找閉包(Closure);


(3-2)、DYLD2方式:
通過了解Dyld3閉包模式我們對dyld的執(zhí)行有了一個大概的認知,但是從分析Dyld3的加載過程我們并沒有發(fā)現(xiàn)我們的framework、vc、main函數(shù)是如何加載的,load函數(shù)是如何被調(diào)用執(zhí)行的,這些都需要我們通過了解Dyld2來進行驗證(DYLD3的方式更加優(yōu)化,流程更加便捷)。







DYLD_INSERT_LIBRARIES 環(huán)境變量,作用是在dyld加載時允許插入動態(tài)庫,這個環(huán)境變量可以通過在root環(huán)境(越獄設(shè)備)下把自己的類庫加入到三方應(yīng)用中,從而實現(xiàn)代碼注入;這塊我先埋個伏筆,后續(xù)我會對iOS 防HOOK方面進行詳細的介紹。
link()鏈接主程序;

從 sAllImages 中一次鏈接動態(tài)庫,sAllImages[i+1] +1是因為上面已經(jīng)加載了dyld的image程序,所以下標從+1開始;
如果加載失敗了,需要再次回到 reloadAllImages 繼續(xù)執(zhí)行;

image->recursiveBind() 綁定插入的動態(tài)庫;

下面就來到最重要的 initializeMainExecutable() 初始化方法;雖然這么看只是一句簡單函數(shù)調(diào)用,其實這個函數(shù)涉及的步驟很多,我們根據(jù)剛才debug獲得信息大致能猜到,這個函數(shù)應(yīng)該是對Image(鏡像)進行處理,我們繼續(xù)前進。

1、跟進initializeMainExecutable() 我們看到的是一個循環(huán),內(nèi)容是將所以的Image執(zhí)行初始化runInitializers函數(shù)。




4、繼續(xù)跟進recursiveInitialization(),我們把焦點放到notifySingle()上,


5、到這步我們已經(jīng)距離結(jié)果越來越近了,繼續(xù)跟進notifySingle(),上面的內(nèi)容我們直接忽略,先關(guān)注這個函數(shù) (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()) sNotifyObjCInit是一個回調(diào)指針,我們搜索一下sNotifyObjCInit看看他是在什么時候被初始化的。通過搜索我們發(fā)現(xiàn)了sNotifyObjCInit是在registerObjCNotifiers函數(shù)下將第二個參數(shù)賦值給了它,因為是回調(diào)指針,我們?nèi)绻业搅薸nit這個函數(shù)就知道了具體是實現(xiàn)。


5-1、接下來我們就看是誰調(diào)用了registerObjCNotifiers(),通過搜索registerObjCNotifiers發(fā)現(xiàn)是由_dyld_objc_notify_register()這個函數(shù)調(diào)用它的,init參數(shù)都透傳過來的,我們還需要繼續(xù)追蹤是誰調(diào)用了dyld_objc_notify_register()?。

5-2、追蹤到dyld_objc_notify_register()函數(shù)這里,我們已無法從源碼中得到結(jié)果了,怎么辦呢?不要慌。我們需要插上真機,增加符號斷點進行調(diào)試,看看是否有結(jié)果。 順利進入了debug后我們查看右側(cè)欄的調(diào)用順序,發(fā)現(xiàn)在dyld_objc_notify_register之前是調(diào)用的是_objc_init,那么我們就可以再去查看_objc_init函數(shù)。



5-3、打開objc源碼后我們command+shift+O搜索_objc_init,我馬上就能發(fā)現(xiàn)確實是_objc_init調(diào)用了_dyld_objc_notify_register,這時我們查看第二個參數(shù)load_images,這個就是init的真實實現(xiàn),我們繼續(xù)跟蹤進去。


5-4、load_images()最后一句話調(diào)用了call_load_methods()函數(shù),從名字我們就知道了這個是開始調(diào)用load方法了。

5-5、繼續(xù) call_load_methods() 函數(shù),發(fā)現(xiàn)是通過循環(huán)將每個類的load函數(shù)進行了調(diào)用。

5-6、到這里notifySingle()函數(shù)全部執(zhí)行完畢,我們繼續(xù)往下看。
6、回到ImageLoader的recursiveInitialization函數(shù)中,this->doInitialization(context);會調(diào)用全局C++對象的構(gòu)造函數(shù)attribute((constructor))的C函數(shù)

7、doInitialization() 內(nèi)部執(zhí)行 doModInitFunctions() 加載構(gòu)造函數(shù)。

8、以上執(zhí)行完畢之后就會回到我們最初的dyld的main函數(shù)了。
(4)、返回app主函數(shù):
(uintptr_t)sMainExecutable->getEntryFromLC_MAIN() 找到主程序main函數(shù);

最后一步返回result;

三、總結(jié)
3.1、簡要總結(jié)分析
- 始開 從行執(zhí)序程
- 進入dyld:main函數(shù)
- 加載共享緩存
- DYLD2 / DYLD3 (閉包模式)
- 實例化主程序
- 加載動態(tài)庫
- 鏈接主程序、綁定符號(優(yōu)先加載的是 非懶加載、弱符號)等等
- 最關(guān)鍵的初始化方法initializeMainExecutable
- dyld:ImageLoader::runInitializers
- dyld:ImageLoader::processInitializers:
- dyld:ImageLoader::recursiveInitialization:
- dyld:dyld::notifySingle:函數(shù)
- 此函數(shù)執(zhí)行一個回調(diào)
- 通過斷點調(diào)試:此回調(diào)是_objc_init初始化時賦值一個函數(shù)Load_images
- Load_images里面執(zhí)行class_load_methods函數(shù)
- call_class_loads函數(shù):循環(huán)調(diào)用各類的load方法
- doModInitFunction函數(shù)
- 內(nèi)部會調(diào)用全局C++對象的構(gòu)造函數(shù)attribute((constructor))的C函數(shù)
- dyld:dyld::notifySingle:函數(shù)
- dyld:ImageLoader::recursiveInitialization:
- dyld:ImageLoader::processInitializers:
- dyld:ImageLoader::runInitializers
- 返回主程序的入口函數(shù)。開始進入主程序的main函數(shù)!

