iOS程式啟動->dyld載入->runtime初始化(初識)
程式的開始main函式與Coding生涯的開始hello World!
iOS開發中,main函式是我們熟知的程式啟動入口,但實際上並非真正意義上的入口,因為在我們執行程式,再到main方法被呼叫之間,程式已經做了許許多多的事情,比如我們熟知的runtime的初始化就發生在main函式呼叫前,還有程式動態庫的載入連結也發生在這階段,本文主要對從程式啟動到main函式中發生的主要事情進行簡單介紹。
其實簡單總結起來就是:
系統先讀取App的可執行檔案(Mach-O檔案),從裡面獲得dyld的路徑,然後載入dyld,dyld去初始化執行環境,開啟快取策略,載入程式相關依賴庫(其中也包含我們的可執行檔案),並對這些庫進行連結,最後呼叫每個依賴庫的初始化方法,在這一步,runtime被初始化。當所有依賴庫的初始化後,輪到最後一位(程式可執行檔案)進行初始化,在這時runtime會對專案中所有類進行類結構初始化,然後呼叫所有的load方法。最後dyld返回main函式地址,main函式被呼叫,我們便來到了熟悉的程式入口。
下面我們將結合程式碼對整個過程進行分析:
dyld載入
這裡先說下Mach-O檔案。
Mach-O檔案格式是 OS X 與 iOS 系統上的可執行檔案格式,像我們編譯過程產生的.O檔案,以及程式的可執行檔案,動態庫等都是Mach-O檔案。它的結構如下:
mach-o檔案
有如下幾個部分組成:
-
Header:儲存了一些基本資訊,包括了該檔案執行的平臺、檔案型別、LoadCommands的個數等等。
-
LoadCommands:可以理解為載入命令,在載入Mach-O檔案時會使用這裡的資料來確定記憶體的分佈以及相關的載入命令。比如我們的main函式的載入地址,程式所需的dyld的檔案路徑,以及相關依賴庫的檔案路徑。
-
Data: 這裡包含了具體的程式碼、資料等等。
我們可以通過Mach-O檔案檢視器MachOView檢視一個測試專案(這裡放上地址)編譯後的可執行檔案內容:
Mach-O檔案內容
這裡可以看到,程式需要的dyld的路徑在LC_LOAD_DYLINKER命令裡,一般都是在/usr/lib/dyld 路徑下。這裡的LC_MAIN指的是程式main函式載入地址,下面還有寫LC_LOAD_DYLIB指向的都是程式依賴庫載入資訊,如果我們程式裡使用到了AFNetworking,這裡就會多一條名為LC_LOAD_DYLIB(AFNetworking)的命令,如下圖
三方庫
這裡可以看到一些我們比較常用的三方庫:AFNetworking,IQKeyboard等。
系統載入程式可執行檔案後,通過分析檔案來獲得dyld所在路徑來載入dyld,然後就將後面的事情甩給dyld了。
從dyld開始
dyld: (the dynamic link editor)動態連結器,其原始碼是開源的。
ImageLoader: 用於輔助載入特定可執行檔案格式的類,程式中對應例項可簡稱為image(如程式可執行檔案,Framework庫,bundle檔案)。
dyld接手後得做很多事情,主要負責初始化程式環境,將可執行檔案以及相應的依賴庫與插入庫載入進記憶體生成對應的ImageLoader類的image(映象檔案)物件,對這些image進行連結,呼叫各image的初始化方法等等(注:這裡多數事情都是遞迴的,從底向上的方法呼叫),其中runtime也是在這個過程中被初始化,這些事情大多數在dyld:_mian方法中被髮生,我們可以看段簡潔的程式碼:
dyld::_main函式程式碼
這裡的_main函式是dyld的函式,並非我們程式裡的main函式。
1.sMainExecutable = instantiateFromLoadedImage(....)與loadInsertedDylib(...)
這一步dyld將我們可執行檔案以及插入的lib載入進記憶體,生成對應的image。
sMainExecutable對應著我們的可執行檔案,裡面包含了我們專案中所有新建的類。
InsertDylib一些插入的庫,他們配置在全域性的環境變數sEnv中,我們可以在專案中設定環境變數DYLD_PRINT_ENV為1來列印該sEnv的值。
環境變數設定
執行程式Log如下:
打印出插入庫的log
可以看到插入的庫為:libBacktraceRecording.dylib和libViewDebuggerSupport.
有時我們會在三方App的Mach-O檔案中通過修改DYLD_INSERT_LIBRARIES的值來加入我們自己的動態庫,從而注入程式碼,hook別人的App(相關資料)。
2.link(sMainExecutable,...)和link(image,....)
對上面生成的Image進行進行連結。其主要做的事有對image進行load(載入),rebase(基地址復位),bind(外部符號繫結),我們可以檢視原始碼:
link方法
-
recursiveLoadLibraries(context, preflightOnly, loaderRPaths)
遞迴載入所有依賴庫進記憶體。
-
recursiveRebase(context)
遞迴對自己以及依賴庫進行復基位操作。在以前,程式每次載入其在記憶體中的堆疊基地址都是一樣的,這意味著你的方法,變數等地址每次都一樣的,這使得程式很不安全,後面就出現ASLR(Address space layout randomization,地址空間配置隨機載入),程式每次啟動後地址都會隨機變化,這樣程式裡所有的程式碼地址都是錯的,需要重新對程式碼地址進行計算修復才能正常訪問。
-
recursiveBind(context, forceLazysBound, neverUnload)
對庫中所有nolazy的符號進行bind,一般的情況下多數符號都是lazybind的,他們在第一次使用的時候才進行bind。
3.initializeMainExecutable()
這一步主要是呼叫所有image的Initalizer方法進行初始化。這裡的Initalizers方法並非名為Initalizers的方法,而是C++靜態物件初始化構造器,atribute((constructor))進行修飾的方法,在LmageLoader類中initializer函式指標所指向該初始化方法的地址。
initiallizer函式指標
我們可以在程式中設定環境變數DYLD_PRINT_INITIALIZERS為1來打印出程式的各種依賴庫的initializer方法:
可以打印出呼叫了Initalizers的image的
執行程式,系統Log列印如下:
lnitializer呼叫log
(由於列印的比較長,這樣就擷取開頭的log)可以看到每個依賴庫對應著一個初始化方法,名稱各有不同。
這裡最開始呼叫的libSystem.dylib的initializer function比較特殊,因為runtime初始化就在這一階段,而這個方法其實很簡單,我們可以在這裡看到init.c原始碼,主要方法如下:
libSystem_initializer方法
其中libdispatch_init裡呼叫了到了runtime初始化方法_objc_init.我們可以、在程式中打個符號斷點來驗證:
_objc_init符號斷點
執行程式,然後斷點命中,我們來看下呼叫棧:
objc_init呼叫棧
這裡可以看到_objc_init呼叫的順序,先libSystem_initializer呼叫libdispatch_init再到_objc_init初始化runtime。
runtime初始化後不會閒著,在_objc_init中註冊了幾個通知,從dyld這裡接手了幾個活,其中包括負責初始化相應依賴庫裡的類結構,呼叫依賴庫裡所有的laod方法。
就拿sMainExcuatable來說,它的initializer方法是最後呼叫的,當initializer方法被呼叫前dyld會通知runtime進行類結構初始化,然後再通知呼叫load方法,這些目前還發生在main函式前,但由於lazy bind機制,依賴庫多數都是在使用時才進行bind,所以這些依賴庫的類結構初始化都是發生在程式裡第一次使用到該依賴庫時才進行的。
main函式被呼叫
當所有的依賴庫庫的lnitializer都呼叫完後,dyld::main函式會返回程式的main函式地址,main函式被呼叫,從而程式碼來到了我們熟悉的程式入口。
main函式入口
結語
這裡只是簡單了概括了從程式啟動->dyld載入依賴庫->runtime初始化->main 的過程。但這階段還有很多事情未講,如果想深入瞭解還得結合原始碼來學習,這裡我已經將dyld和runtime原始碼都放在這了,大家可直接下載,也可以從opensource-apple下載。
參考資料