DYLD 加載Mach-O的流程

背景

大家都知道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):
圖片.png

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提供了更強的擴展性。


圖片.png

圖片.png

這里要注意,Mach-O是一種文件類型,我們常見的.o文件、.a庫、.Framework等都屬于這個類型;我們可通過file命令查看文件類型。例如:
image
1.4、如何查看Mach-O文件?

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

image
我們需要下載一個叫MachOView的工具,直接將MachO文件拖到工具圖標上就可以了。效果跟MachO的結(jié)構(gòu)圖一樣。 工具下載地址:鏈接: https://pan.baidu.com/s/112A7mZ0ssPdJHSvPNl5OGg 密碼: t5ab
既然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ù)中打印一句話標記一下。


圖片.png

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


圖片.png
3、除了上面的main函數(shù)和VC的load函數(shù)之外,我們的App一般還會引用一些framework,所以我們不妨也看看framework是什么是后被加載運行的。我們可以自己創(chuàng)建一個framework,然后再framework中新建一個Main.m的文件,同樣的參考ViewController我們也增加一個load函數(shù)。
圖片.png

圖片.png

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


圖片.png
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)用流程了。
    image
    通過調(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ù)。

    根據(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

image

3.2、分析過程:
3.2.1、start函數(shù):

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

image
image
第一個關(guān)鍵步驟就是rebaseDyld(dyldsMachHeader);方法,他的目的是重定位,其中參數(shù)dyldsMachHeader就是MachO文件里的Header內(nèi)容。這里牽扯到一個概念就是ASLR大家可以百度一下,我這里簡要解釋一下,在iOS系統(tǒng)中打開一個App的時候是會將App的二進制數(shù)據(jù)從硬盤copy到內(nèi)存里,那么這時候二進制數(shù)據(jù)就會對應(yīng)一個內(nèi)存地址,由于考慮安全等因素的問題,內(nèi)存的地址都是由虛擬緩存地址替代,而且地址的起始位置都是動態(tài)的,每次啟動的時候都會不一樣,這個技術(shù)就是ASLR。所以當DYLD加載MachO的時候最先一步要做的就是對數(shù)據(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ā)送改版,所以需要再一次重新進行保存。

image

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

image

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

image

![image](https://upload-images.jianshu.io/upload_images/26320104-5bc616925e83e990.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![image](https://upload-images.jianshu.io/upload_images/26320104-e9d0318e6faeb0b8.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
(2)、加載共享緩存:
image

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

我們知道iOS在加載的時候最終會將MachO加載到內(nèi)存中,其中一些通用的框架不只一個App會使用到,所以為了節(jié)省空間提升效率,蘋果采用了共享緩存的機制,如下圖:
圖片.png
(3)、DYLD加載方式:

經(jīng)過了以上的配置、加載共享緩存的步驟之后,DYLD現(xiàn)在要正式開始,目前DYLD的執(zhí)行方式分為2種;Dyld2 和 Dyld3。

(3-1)、DYLD3方式:

iOS11 之后增加了Dyld3 通過使用Closure閉包方式進行加載,這種方式比之前Dyld2的效率更高效,但本質(zhì)的流程還是與Dyld2一致的,我們可以快速的看一下。先從共享緩存中查找閉包(Closure);
image

如果mainClosure是空,并且有失效了,則加載方式也會發(fā)生改變;
image
沒有從緩存中找到有效的Closure,就新建一個;嘗試啟動Closure,驗證知否過期,如果過期了則再創(chuàng)建一個,讓后再次啟動Closure;圖
image
啟動成功返回result;
(3-2)、DYLD2方式:

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

image
reloadAllImages: 往下就開始進行Dyld2的流程;實例化主程序sMainExecutable,這是dyld第一個加載的image;我們看一下instantiateFromLoadedImage() 跟蹤到最后我們發(fā)現(xiàn)了mach-O中加載segmentdylib的數(shù)量是有上限的,如果超過上限就會報錯。
image
image
image
image
image
checkVersionedPaths()檢查動態(tài)庫的版本;
image

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

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

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

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

圖片.png

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

2、繼續(xù)跟進runInitializers(command+shift+O)然后繼續(xù)調(diào)用了processInitializers()函數(shù)。這里可能我們通過command+左鍵 無法追蹤,我們使用command+shift+O 然后選擇即可。
image
image

3、繼續(xù)跟進processInitializers() 然后繼續(xù)調(diào)用了recursiveInitialization()函數(shù)。
image

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

image

image

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)。
圖片.png

圖片.png

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()?。
圖片.png

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ù)。
圖片.png

圖片.png

圖片.png

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ù)跟蹤進去。
圖片.png

圖片.png

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

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

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

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

8、以上執(zhí)行完畢之后就會回到我們最初的dyld的main函數(shù)了。

(4)、返回app主函數(shù):

(uintptr_t)sMainExecutable->getEntryFromLC_MAIN() 找到主程序main函數(shù);

圖片.png

最后一步返回result;
圖片.png

三、總結(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ù)
    • 返回主程序的入口函數(shù)。開始進入主程序的main函數(shù)!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容