iOS程序啟動->dyld加載->runtime初始化(初識)

程序的開始main函數(shù)與Coding生涯的開始hello World!.png

iOS開發(fā)中,main函數(shù)是我們熟知的程序啟動入口,但實際上并非真正意義上的入口,因為在我們運行程序,再到main方法被調(diào)用之間,程序已經(jīng)做了許許多多的事情,比如我們熟知的runtime的初始化就發(fā)生在main函數(shù)調(diào)用前,還有程序動態(tài)庫的加載鏈接也發(fā)生在這階段,本文主要對從程序啟動到main函數(shù)中發(fā)生的主要事情進行簡單介紹。

其實簡單總結(jié)起來就是:

系統(tǒng)先讀取App的可執(zhí)行文件(Mach-O文件),從里面獲得dyld的路徑,然后加載dyld,dyld去初始化運行環(huán)境,開啟緩存策略,加載程序相關(guān)依賴庫(其中也包含我們的可執(zhí)行文件),并對這些庫進行鏈接,最后調(diào)用每個依賴庫的初始化方法,在這一步,runtime被初始化。當所有依賴庫的初始化后,輪到最后一位(程序可執(zhí)行文件)進行初始化,在這時runtime會對項目中所有類進行類結(jié)構(gòu)初始化,然后調(diào)用所有的load方法。最后dyld返回main函數(shù)地址,main函數(shù)被調(diào)用,我們便來到了熟悉的程序入口。

下面我們將結(jié)合代碼對整個過程進行分析:

dyld加載

這里先說下Mach-O文件。
Mach-O文件格式是 OS X 與 iOS 系統(tǒng)上的可執(zhí)行文件格式,像我們編譯過程產(chǎn)生的.O文件,以及程序的可執(zhí)行文件,動態(tài)庫等都是Mach-O文件。它的結(jié)構(gòu)如下:

mach-o文件.jpg

有如下幾個部分組成:

  1. Header:保存了一些基本信息,包括了該文件運行的平臺、文件類型、LoadCommands的個數(shù)等等。
  2. LoadCommands:可以理解為加載命令,在加載Mach-O文件時會使用這里的數(shù)據(jù)來確定內(nèi)存的分布以及相關(guān)的加載命令。比如我們的main函數(shù)的加載地址,程序所需的dyld的文件路徑,以及相關(guān)依賴庫的文件路徑。
  3. Data: 這里包含了具體的代碼、數(shù)據(jù)等等。
    我們可以通過Mach-O文件查看器MachOView查看一個測試項目(這里放上地址)編譯后的可執(zhí)行文件內(nèi)容:
Mach-O文件內(nèi)容.png

這里可以看到,程序需要的dyld的路徑在LC_LOAD_DYLINKER命令里,一般都是在/usr/lib/dyld 路徑下。這里的LC_MAIN指的是程序main函數(shù)加載地址,下面還有寫LC_LOAD_DYLIB指向的都是程序依賴庫加載信息,如果我們程序里使用到了AFNetworking,這里就會多一條名為LC_LOAD_DYLIB(AFNetworking)的命令,如下圖

三方庫.png

這里可以看到一些我們比較常用的三方庫:AFNetworking,IQKeyboard等。

系統(tǒng)加載程序可執(zhí)行文件后,通過分析文件來獲得dyld所在路徑來加載dyld,然后就將后面的事情甩給dyld了。

從dyld開始

dyld: (the dynamic link editor)動態(tài)鏈接器,其源碼是開源的。
ImageLoader: 用于輔助加載特定可執(zhí)行文件格式的類,程序中對應實例可簡稱為image(如程序可執(zhí)行文件,F(xiàn)ramework庫,bundle文件)。

dyld接手后得做很多事情,主要負責初始化程序環(huán)境,將可執(zhí)行文件以及相應的依賴庫與插入庫加載進內(nèi)存生成對應的ImageLoader類的image(鏡像文件)對象,對這些image進行鏈接,調(diào)用各image的初始化方法等等(注:這里多數(shù)事情都是遞歸的,從底向上的方法調(diào)用),其中runtime也是在這個過程中被初始化,這些事情大多數(shù)在dyld:_mian方法中被發(fā)生,我們可以看段簡潔的代碼:

dyld::_main函數(shù)代碼.png

這里的_main函數(shù)是dyld的函數(shù),并非我們程序里的main函數(shù)。

1.sMainExecutable = instantiateFromLoadedImage(....)與loadInsertedDylib(...)

這一步dyld將我們可執(zhí)行文件以及插入的lib加載進內(nèi)存,生成對應的image。
sMainExecutable對應著我們的可執(zhí)行文件,里面包含了我們項目中所有新建的類。
InsertDylib一些插入的庫,他們配置在全局的環(huán)境變量sEnv中,我們可以在項目中設(shè)置環(huán)境變量DYLD_PRINT_ENV為1來打印該sEnv的值。

環(huán)境變量設(shè)置.png

運行程序Log如下:

打印出插入庫的log.png

可以看到插入的庫為:libBacktraceRecording.dyliblibViewDebuggerSupport.
有時我們會在三方App的Mach-O文件中通過修改DYLD_INSERT_LIBRARIES的值來加入我們自己的動態(tài)庫,從而注入代碼,hook別人的App(相關(guān)資料)。

2.link(sMainExecutable,...)和link(image,....)
對上面生成的Image進行進行鏈接。其主要做的事有對image進行l(wèi)oad(加載),rebase(基地址復位),bind(外部符號綁定),我們可以查看源碼:

link方法.png
  • recursiveLoadLibraries(context, preflightOnly, loaderRPaths)
    遞歸加載所有依賴庫進內(nèi)存。

  • recursiveRebase(context)
    遞歸對自己以及依賴庫進行復基位操作。在以前,程序每次加載其在內(nèi)存中的堆?;刂范际且粯拥?,這意味著你的方法,變量等地址每次都一樣的,這使得程序很不安全,后面就出現(xiàn)ASLR(Address space layout randomization,地址空間配置隨機加載),程序每次啟動后地址都會隨機變化,這樣程序里所有的代碼地址都是錯的,需要重新對代碼地址進行計算修復才能正常訪問。

  • recursiveBind(context, forceLazysBound, neverUnload)
    對庫中所有nolazy的符號進行bind,一般的情況下多數(shù)符號都是lazybind的,他們在第一次使用的時候才進行bind。

3.initializeMainExecutable()

這一步主要是調(diào)用所有image的Initalizer方法進行初始化。這里的Initalizers方法并非名為Initalizers的方法,而是C++靜態(tài)對象初始化構(gòu)造器,atribute((constructor))進行修飾的方法,在LmageLoader類中initializer函數(shù)指針所指向該初始化方法的地址。

initiallizer函數(shù)指針.jpg

我們可以在程序中設(shè)置環(huán)境變量DYLD_PRINT_INITIALIZERS為1來打印出程序的各種依賴庫的initializer方法:

可以打印出調(diào)用了Initalizers的image的.png

運行程序,系統(tǒng)Log打印如下:

lnitializer調(diào)用log.png

(由于打印的比較長,這樣就截取開頭的log)可以看到每個依賴庫對應著一個初始化方法,名稱各有不同。
這里最開始調(diào)用的libSystem.dylib的initializer function比較特殊,因為runtime初始化就在這一階段,而這個方法其實很簡單,我們可以在這里看到init.c源碼,主要方法如下:

libSystem_initializer方法.jpg

其中l(wèi)ibdispatch_init里調(diào)用了到了runtime初始化方法_objc_init.我們可以、在程序中打個符號斷點來驗證:

_objc_init符號斷點.png

運行程序,然后斷點命中,我們來看下調(diào)用棧:

objc_init調(diào)用棧.png

這里可以看到_objc_init調(diào)用的順序,先libSystem_initializer調(diào)用libdispatch_init再到_objc_init初始化runtime。

runtime初始化后不會閑著,在_objc_init中注冊了幾個通知,從dyld這里接手了幾個活,其中包括負責初始化相應依賴庫里的類結(jié)構(gòu),調(diào)用依賴庫里所有的laod方法。

就拿sMainExcuatable來說,它的initializer方法是最后調(diào)用的,當initializer方法被調(diào)用前dyld會通知runtime進行類結(jié)構(gòu)初始化,然后再通知調(diào)用load方法,這些目前還發(fā)生在main函數(shù)前,但由于lazy bind機制,依賴庫多數(shù)都是在使用時才進行bind,所以這些依賴庫的類結(jié)構(gòu)初始化都是發(fā)生在程序里第一次使用到該依賴庫時才進行的。

main函數(shù)被調(diào)用

當所有的依賴庫庫的lnitializer都調(diào)用完后,dyld::main函數(shù)會返回程序的main函數(shù)地址,main函數(shù)被調(diào)用,從而代碼來到了我們熟悉的程序入口。

main函數(shù)入口.png

結(jié)語

這里只是簡單了概括了從程序啟動->dyld加載依賴庫->runtime初始化->main 的過程。但這階段還有很多事情未講,如果想深入了解還得結(jié)合源碼來學習,這里我已經(jīng)將dyld和runtime源碼都放在這了,大家可直接下載,也可以從opensource-apple下載。

再嘮嗑會

dyld源碼前前后后讀個大概懂,花了我3個多禮拜的空閑時間,由于C和C++基礎(chǔ)并不是很好,所以特意跑回學校買了幾本書補了下基礎(chǔ),不過讀源碼的這段時間還是挺累的。

為什么要去讀源碼,主要是看別人的文章時并不能很好解決我的某些疑問,而且只有真正去認識源碼,去親身體會才能加深對它的理解。

學習的旅途雖然頗累,但一路下來收獲頗多。加油!

前行路,路漫漫,一人一酒似逍遙。


一張圖.jpg

參考資料

1.Mach-O 可執(zhí)行文件
2.dylib動態(tài)庫加載過程分析
3.iOS 程序 main 函數(shù)之前發(fā)生了什么
4.今日頭條iOS客戶端啟動速度優(yōu)化
5.App 啟動時間:過去,現(xiàn)在和未來
6.優(yōu)化 App 的啟動時間
7.dyld在hook方面的小東西

喜歡的話點個喜歡唄_

最后編輯于
?著作權(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ù)。

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

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