程序的開始main函數與Coding生涯的開始hello World!.png
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文件.jpg
有如下幾個部分組成:
Header:保存了一些基本信息,包括了該文件運行的平臺、文件類型、LoadCommands的個數等等。
LoadCommands:可以理解為加載命令,在加載Mach-O文件時會使用這里的數據來確定內存的分布以及相關的加載命令。比如我們的main函數的加載地址,程序所需的dyld的文件路徑,以及相關依賴庫的文件路徑。
Data: 這里包含了具體的代碼、數據等等。
我們可以通過Mach-O文件查看器MachOView查看一個測試項目(這里放上地址)編譯后的可執行文件內容:
Mach-O文件內容.png
這里可以看到,程序需要的dyld的路徑在LC_LOAD_DYLINKER命令里,一般都是在/usr/lib/dyld 路徑下。這里的LC_MAIN指的是程序main函數加載地址,下面還有寫LC_LOAD_DYLIB指向的都是程序依賴庫加載信息,如果我們程序里使用到了AFNetworking,這里就會多一條名為LC_LOAD_DYLIB(AFNetworking)的命令,如下圖
三方庫.png
這里可以看到一些我們比較常用的三方庫: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函數代碼.png
這里的_main函數是dyld的函數,并非我們程序里的main函數。
1.sMainExecutable = instantiateFromLoadedImage(....)與loadInsertedDylib(...)
這一步dyld將我們可執行文件以及插入的lib加載進內存,生成對應的image。
sMainExecutable對應著我們的可執行文件,里面包含了我們項目中所有新建的類。
InsertDylib一些插入的庫,他們配置在全局的環境變量sEnv中,我們可以在項目中設置環境變量DYLD_PRINT_ENV為1來打印該sEnv的值。
環境變量設置.png
運行程序Log如下:
打印出插入庫的log.png
可以看到插入的庫為:libBacktraceRecording.dylib和libViewDebuggerSupport.
有時我們會在三方App的Mach-O文件中通過修改DYLD_INSERT_LIBRARIES的值來加入我們自己的動態庫,從而注入代碼,hook別人的App(相關資料)。
2.link(sMainExecutable,...)和link(image,....)
對上面生成的Image進行進行鏈接。其主要做的事有對image進行load(加載),rebase(基地址復位),bind(外部符號綁定),我們可以查看源碼:
link方法.png
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函數指針.jpg
我們可以在程序中設置環境變量DYLD_PRINT_INITIALIZERS為1來打印出程序的各種依賴庫的initializer方法:
可以打印出調用了Initalizers的image的.png
運行程序,系統Log打印如下:
lnitializer調用log.png
(由于打印的比較長,這樣就截取開頭的log)可以看到每個依賴庫對應著一個初始化方法,名稱各有不同。
這里最開始調用的libSystem.dylib的initializer function比較特殊,因為runtime初始化就在這一階段,而這個方法其實很簡單,我們可以在這里看到init.c源碼,主要方法如下:
libSystem_initializer方法.jpg
其中libdispatch_init里調用了到了runtime初始化方法_objc_init.我們可以、在程序中打個符號斷點來驗證:
_objc_init符號斷點.png
運行程序,然后斷點命中,我們來看下調用棧:
objc_init調用棧.png
這里可以看到_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函數入口.png
結語
這里只是簡單了概括了從程序啟動->dyld加載依賴庫->runtime初始化->main 的過程。但這階段還有很多事情未講,如果想深入了解還得結合源碼來學習,這里我已經將dyld和runtime源碼都放在這了,大家可直接下載,也可以從opensource-apple下載。
再嘮嗑會
dyld源碼前前后后讀個大概懂,花了我3個多禮拜的空閑時間,由于C和C++基礎并不是很好,所以特意跑回學校買了幾本書補了下基礎,不過讀源碼的這段時間還是挺累的。
為什么要去讀源碼,主要是看別人的文章時并不能很好解決我的某些疑問,而且只有真正去認識源碼,去親身體會才能加深對它的理解。
學習的旅途雖然頗累,但一路下來收獲頗多。加油!
前行路,路漫漫,一人一酒似逍遙。
一張圖.jpg
參考資料
喜歡的話點個喜歡唄^_^