庫是已寫好的、供使用的 可複用代碼,每一個程序都要依賴不少基礎的底層庫。c++
從本質上,庫是一種可執行代碼的二進制形式。能夠被操做系統
載入內存執行。庫分爲兩種:靜態庫(.a .lib)和 動態庫 (framework .so .dll)。bootstrap
所謂的靜態、動態指的是 連接的過程
。windows
將一個程序編譯成可執行程序的步驟以下:緩存
之因此稱之爲【靜態庫】,是由於在連接階段,會將彙編生成的目標文件.o 與 引用的庫一塊兒連接到可執行文件中。對應的連接方式稱爲 靜態連接。安全
若是多個進程須要引用到【靜態庫】,在內存中就會存在多份拷貝,如上圖中進程1 用到了靜態庫一、5,進程2也用到了靜態庫一、5,那麼靜態庫一、5在編譯期
就分別被連接到了進程1和進程2中,假設靜態庫1佔用2M內存,若是有20個這樣的進程須要用到靜態庫1,將佔用40M的空間。markdown
【靜態庫】的特色以下:閉包
編譯期
完成的。執行期間代碼裝載速度快。佔空間
)。全量更新
。若是 某一個靜態庫更新了,全部使用它的應用程序都須要從新編譯、發佈給用戶。【動態庫】在程序編譯時並不會連接到目標代碼中,而是在運行時
才被載入。不一樣的應用程序若是調用相同的庫,那麼在內存中只須要有一份該共享庫的實例,避免了空間浪費問題。同時也解決了靜態庫對程序的更新的依賴,用戶只需更新動態庫便可。架構
【動態庫】在內存中只存在一份拷貝,若是某一進程須要用到動態庫,只需在運行時動態載入便可。app
【動態庫】的特色:dom
運行時
期(佔時間
)。資源共享
。(所以動態庫也稱爲共享庫)增量更新
。
程序想要運行起來,它的可執行文件格式就要被操做系統所理解,好比 ELF
(Executable and Linking Format) 是 Linux
下可執行文件的格式,PE32/PE32+
(Portable Executable) 是 windows
的可執行文件的格式,那麼對於 OS X
和 iOS
來講 Mach-O
是其可執行文件的格式。
【Mach-O】 爲 Mach Object 文件格式的縮寫,是 iOS 系統不一樣運行時期 可執行文件 的文件類型統稱。它是一種用於 可執行文件、目標代碼、動態庫、內核轉儲的文件格式。
【Mach-O】 的三種文件類型:Executable、Dylib、Bundle
Executable 是 app
的二進制主文件,咱們能夠在 Xcode 項目中的 products 文件中找到它:
Dylib 是動態庫,動態庫分爲 動態連接庫
和 動態加載庫
。
動態連接庫
:在沒有被加載到內存的前提下,當可執行文件被加載,動態庫也隨着被加載到內存中。【隨着程序啓動而啓動】
動態加載庫
:當須要的時候再使用dlopen
等經過代碼或者命令的方式加載。【程序啓動以後】
Bundle 是一種特殊類型的Dylib,你沒法對其進行連接。所能作的是在Runtime運行時經過dlopen
來加載它,它能夠在macOS 上用於插件。
Image (鏡像文件)包含了上述的三種類型;
Framework 能夠理解爲動態庫。
【Mach-O】是一個以數據塊
分組的二進制字節流,每一個【Mach-O】文件包括一個Mach-O頭,而後是一系列的載入命令,再是一個或多個段,每一個段包括0到255個塊。
保存【Mach-O】的一些基本信息,包括運行平臺、文件類型、LoadCommands指令的個數、指令總大小,dyld標記Flags
等等。
緊跟Header,這些加載指令清晰地告訴加載器如何處理二進制數據,有些命令是由內核處理的,有些是由動態連接器處理的。加載【Mach-O】文件時會使用這部分數據肯定內存分佈
以及相關的加載命令,對系統內核加載器和動態鏈接器起指導做用。好比咱們的main()
函數的加載地址、程序所需的dyld的文件路徑、以及相關依賴庫的文件路徑。
每一個segment的具體數據保存在這裏,包含具體的代碼、數據
等等。
【Mach-O】 鏡像文件 是由 segments
段組成的。
全部的段都是 page size
的倍數。
16kB
4KB
這裏在普及一下 虛擬內存 和 內存頁 的知識:
具備
VM
機制的操做系統,會對每一個運行的進程建立一個邏輯地址空間logical address space
或者叫 虛擬地址空間virtual address space
;該空間的大小由操做系統位數決定。
虛擬地址空間 會被分爲相同大小的塊
,這些塊被稱爲內存頁
(page)。計算機處理器和它的內存管理單元(MMU - memory management unit)維護着一張將程序的 虛擬地址空間 映射到 物理地址 上的分頁表 page table
。
在 macOS
和早版本的 iOS
中,分頁大小爲 4kb
。在以後的基於A7
和 A8
的系統中,虛擬內存(64位的地址空間)地址空間的分頁大小變爲了16kb
,而物理RAM上的內存分頁大小仍然維持在 4kb
;基於 A9
及之後的系統,虛擬內存和物理內存的分頁都是16kb
。
在 segment
段內部還有許多的 section
區。section
名稱爲小寫格式。 section
節 實際上只是一個 segment
段的子範圍,它們沒有頁面大小的任何限制,可是它們是不重疊的。
代碼段
,包含頭文件、代碼和只讀常量
。只讀
不可修改數據段
,包含全局變量,靜態變量
等。可讀可寫
如何加載程序
,包含了方法和變量的元數據
(位置,偏移量),以及代碼簽名
等信息。只讀
不可修改。【Mach-O】 通用文件,將多種架構的 Mach-O 文件合併而成。它經過 header
來記錄不一樣架構在文件中的偏移量,segment
佔多個分頁,header
佔一頁的空間。header
單獨佔一頁 有利於 虛擬內存
的實現。
虛擬內存是一層 間接尋址 。
【虛擬內存】是在物理內存上創建的一個邏輯地址空間。創建在進程
和物理內存
之間的中間層
,它向上(應用)提供了一個連續的邏輯地址空間,向下隱藏了物理內存的細節。
虛擬內存被劃分爲一個個大小相同的Page
(64位系統上是16KB),提升管理和讀寫
的效率。 Page又分爲只讀
和讀寫
的Page。
虛擬內存解決的是管理全部進程使用 物理RAM 的問題。經過添加間接層來讓每一個進程使用 邏輯地址空間,它能夠映射到RAM 上的某個物理頁上。這種映射 不是一對一
的,邏輯地址可能映射不到 RAM 上,也有可能有多個邏輯地址映射到同一個物理RAM 上。
虛擬內存使得邏輯地址能夠沒有實際的物理地址,也可讓多個邏輯地址對應到一個物理地址。
- 針對第一種狀況(邏輯地址可能映射不到 RAM ):在應用執行的時候,它被分配的邏輯地址空間都是能夠訪問的,當應用訪問一個邏輯Page,而在對應的物理內存中並不存在的時候,這時候就發生了一次
Page fault
。當Page fault發生的時候,會中斷當前的程序,在物理內存中尋找一個可用的Page,而後從磁盤中讀取數據到物理內存,接着繼續執行當前程序。- 而第二種狀況(多個邏輯地址映射到同一個物理RAM 上)就是
多進程共享內存
。
對於文件能夠不用一次性讀入整個文件,可使用分頁映射 mmap()
的方式獲取。也就是把文件 某個片斷 映射到進程邏輯內存的 某個頁 上。當某個想要讀取的頁沒有在內存中,就會觸發 page fault
,內核只會讀入那一頁,實現文件的 懶加載。也就是說 【Mach-O】 文件中的 __TEXT 段能夠映射到多個進程,並能夠懶加載,且進程之間 共享內存。
__DATA 段是可讀寫的。這裏使用到了Copy-On-Write
技術,簡稱【COW】。 也就是多個進程共享一頁內存空間時,一旦有進程要作寫操做,它會先將這頁內存內容複製一份出來,而後從新映射邏輯地址到新的RAM 頁上。也就是這個進程本身擁有了那頁內存的拷貝。這就涉及到了 clean/dirty page
的概念。dirty page
含有進程本身的信息,而clean page
能夠被內核從新生成(從新讀磁盤)。多以 dirty page
的代價大於 clean page
。
共享內存
的,讀取速度就會很快。dirty page
,若是檢測有 clean page 就能夠直接使用,反之就須要從新讀取 DATA page。一旦產生了 dirty page,當dyld
執行結束後,__LINKEDIT 須要通知內核
當前頁面再也不須要了,當別人須要使用的時候就能夠從新 clean 這些頁面。有兩種主要的技術來保證應用的安全:ASLR 和 Code Sign。
【ASLR】的全稱是Address space layout randomization
,翻譯過來就是「地址空間佈局隨機化」
。App
被啓動的時候,程序會被映射到邏輯的地址空間,這個邏輯的地址空間有一個起始地址,而【ASLR】技術使得這個起始地址是隨機的。若是是固定的,那麼黑客很容易就能夠由起始地址+偏移量找到函數的地址。
【Code Sign】相信大多數開發者都知曉,這裏要提一點的是,爲了在運行時 驗證【Mach-O】 文件的簽名,在進行【Code Sign】的時候,加密哈希不是針對於整個文件,而是針對於每個Page
的。並存儲在 __LINKEDIT 中。這就保證了在dyld
進行加載的時候,能夠對每個page
進行獨立的驗證
。
exec()
是一個系統調用。系統內核把應用程序映射到新的地址空間,且每次起始位置都是隨機的(由於ASLR
)。並將起始位置到0x000000
這段範圍的進程權限都標記爲不可讀寫不可執行。若是是32
位進程,這個範圍至少是4kb
;若是是64
位進程則至少是4GB
。NULL
指針引用和指針截斷偏差都是會被它捕獲,這個範圍也叫作 PAGEZERO
。
當內核
完成映射進程的工做後,會將名字爲 dyld
的 Mach-O
文件映射到進程中的隨機地址,它將PC 寄存器設爲 dyld
的地址並運行。dyld
在應用進程中運行的工做是加載應用依賴的全部動態連接庫,準備好運行所需的一切,它擁有的權限跟應用程序同樣。
dyld(the dynamic link editor),【動態連接器】是蘋果操做系統一個重要部分,在 iOS / macOS
系統中,僅有不多的進程只需內核就能夠完成加載,基本上全部的進程都是動態連接的,因此 Mach-O
鏡像文件中會有不少對外部的庫和符號
的引用,可是這些引用並不能直接用,在啓動時還必需要經過這些引用進行內容填充,這個填充的工做就是由 dyld
來完成的。
【動態連接加載器】在系統中以一個用戶態
的可執行文件形式存在,通常應用程序會在Mach-O
文件部分指定一個 LC_LOAD_DYLINKER 的加載命令,此加載命令指定了dyld
的路徑,一般它的默認值是「/usr/lib/dyld」
。系統內核在加載Mach-O
文件時,會使用該路徑指定的程序做爲動態庫的加載器來加載dylib
。
dyld
加載時,爲了優化程序啓動,啓用了共享緩存
(shared cache)技術。共享緩存會在進程啓動時被dyld
映射到內存中,以後,當任何Mach-O
鏡像加載時,dyld
首先會檢查該Mach-O
鏡像與所需的動態庫是否在共享緩存
中,若是存在,則直接將它在共享內存中的內存地址映射到進程的內存地址空間。在程序依賴的系統動態庫不少的狀況下,這種作法對程序啓動性能是有明顯提高的。
從主執行文件header
獲取到須要加載的所依賴的動態庫列表,而header早就被內核映射過。而後它須要找到每一個dylib
,而後打開文件,讀取文件起始位置,確保它是Mach-O
文件。接着會找到代碼簽名
並將其註冊到內核
。而後在dylib文件的每一個segment
上調用 mmap()
。應用所依賴的dylib文件可能會再依賴其餘dylib,因此dyld
所須要加載的是動態庫列表一個遞歸
依賴的集合。通常應用會加載100到400 個dylib文件,但大部分都是系統的dylib,它們會被預先計算和緩存起來,加載速度很快。
在加載全部的動態連接庫以後,它們只是處在相互獨立的狀態,須要將它們綁定起來,這就是Fix-ups
。代碼簽名使得咱們不能修改指令,那樣就不能讓一個dylib
調用另外一個 dylib
,這是就須要不少間接層。
Mach-O
中有不少符號,有指向當前 Mach-O 的,也有指向其餘 dylib 的,好比printf。那麼,在運行時,代碼如何準確的找到printf
的地址呢?
Mach-O
中採用了PIC技術,全稱是Position Independ code。意味着代碼能夠被加載到間接的地址上。當你的程序要調用printf
的時候,會先在 __DATA 段中創建一個指針指向printf
,在經過這個指針實現間接調用。dyld
這時候須要作一些fix-up
工做,即幫助應用程序找到這些符號的實際地址。主要包括兩部分:rebasing
和binding
。
Rebasing:在鏡像內部調整指針的指向。 Binding: 將指針指向鏡像外部的內容。
之因此須要Rebase
,是由於剛剛提到的 ASLR 使得地址隨機化,致使起始地址不固定,另外因爲 Code Sign,致使不能直接修改 Image
。Rebase
的時候只須要增長對應的偏移量便可。(待Rebase的數據都存放在 __LINKEDIT中,能夠經過MachOView查看:Dynamic Loader Info -> Rebase Info)
Binding
就是將這個二進制調用的外部符號進行綁定的過程。 好比咱們objc代碼中須要使用到NSObject, 即符號_OBJC_CLASS_$_NSObject,可是這個符號又不在咱們的二進制中,在系統庫 Foundation.framework中,所以就須要Binding
這個操做將對應關係綁定到一塊兒。
Rebase
解決了內部的符號引用問題,而外部的符號引用則是由Bind
解決。在解決Bind
的時候,是根據字符串匹配的方式查找符號表,因此這個過程相對於Rebase
來講是略慢的。
在 iOS 13
以前,全部的第三方App
都是經過dyld 2
來啓動 App
的,主要過程以下:
Mach-O
的Header
和 Load Commands
,找到其依賴的庫,並遞歸找到全部依賴的庫dyld 3
被分爲了三個組件:
Mach-O
解析器預先處理了全部可能影響啓動速度的search path、@rpaths
和環境變量 而後分析Mach-O
的Header
和依賴,並完成了全部符號查找的工做 最後將這些結果建立成一個啓動閉包 這是一個普通的daemon
進程,可使用一般的測試架構
這部分在進程中處理 驗證啓動閉包的安全性,而後映射到dylib之中,再跳轉到main函數 不須要解析Mach-O
的 Header
和依賴,也不須要符號查找。
系統App的啓動閉包被構建在一個Shared Cache
中,咱們甚至不須要打開一個單獨的文件 對於第三方的App
,咱們會在App
安裝或者升級的時候構建這個啓動閉包。 在iOS、tvOS、watchOS
中,這一切都是App
啓動以前完成的。在macOS
上,因爲有Side Load App
,進程內引擎會在首次啓動的時候啓動一個daemon
進程,以後就可使用啓動閉包啓動了。
dyld 3
把不少耗時的查找、計算和I/O 的事件都預先處理好,這使得啓動速度有了很大的提高。
有了前面的知識儲備,接下來將探索app
的加載流程。
在應用程序的入口 main()
函數以前斷點,查看堆棧信息
能夠看到,先於main
函數調用的是 start
,同時,這一流程是由libdyld.dylib
庫執行的。dyld
是開源庫,能夠下載源碼探索。點擊下載dyld 源碼
爲了看到更詳細的調用過程,咱們在項目中的 ViewController 的 + (void) load
方法打斷點。詳細堆棧信息以下
可見,調用流程是從 _dyld_start
開始的,咱們在下載好的源碼中搜索 _dyld_start
。在 dyldStartup.s
文件中找到了入口,這裏是用匯編實現的,儘管在不一樣架構下有所區別,但都是會調用 dyldbootstrap
命名空間下的start
方法,這和上面的堆棧順序也是相同的。
call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
複製代碼
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[], const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue) {
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
rebaseDyld(dyldsMachHeader);
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
複製代碼
dyldbootstrap::start中,主要過程爲:
①使用全局變量以前,對dyld
進行rebase
操做,以修復爲 real pointer
來運行;
②設置參數和環境變量;
③讀取 app
二進制文件 Mach-O
的header
獲得偏移量 appSlide
,而後調用dyld
命名空間下的_main
方法。
這裏是dyld
的入口。內核加載了dyld
而後跳轉到 _dyld_start
來設置一些寄存器的值以後 進入這個方法。返回 _dyld_start
所跳轉到的目標程序的main
函數地址。精簡的代碼以下:
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
......
// 設置運行環境,可執行文件準備工做
......
// load shared cache 加載共享緩存
mapSharedCache();
......
reloadAllImages:
......
// instantiate ImageLoader for main executable 加載可執行文件並生成一個ImageLoader實例對象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
......
// load any inserted libraries 加載插入的動態庫
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// link main executable 連接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
......
// link any inserted libraries 連接全部插入的動態庫
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// 註冊符號插入
image->registerInterposing(gLinkContext);
}
}
}
......
//弱符號綁定
sMainExecutable->weakBind(gLinkContext);
sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);
......
// run all initializers 執行初始化方法
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
return result;
}
複製代碼
主要過程:
①第一步: 設置運行環境,爲可執行文件的加載作準備工做;
②第二步: 映射共享緩存到當前進程的邏輯內存空間;
③第三步: 實例化主程序;
④第四步: 加載插入的動態庫;
⑤第五步: 連接主程序;
⑥第六步: 連接插入的動態庫;
⑦第七步: 執行弱符號綁定(weakBind);
⑧第八步: 執行初始化方法;
⑨第九步: 查找程序入口並返回main( ).
這一步 dyld
將咱們可執行文件以及插入的 lib
加載進內存,生成對應的image
。 sMainExecutable
對應着咱們的可執行文件,裏面包含了咱們項目中全部新建的類。 InsertDylib
一些插入的庫,他們配置在全局的環境變量 sEnv 中,咱們能夠在項目中設置環境變量 DYLD_PRINT_ENV 爲1來打印該 sEnv 的值。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path) {
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
複製代碼
isCompatibleMachO
是檢查Mach-O的subtype是不是當前cpu能夠支持; 內核會映射到主可執行文件中,咱們須要爲映射到主可執行文件的文件,建立ImageLoader。
instantiateMainExecutable 就是實例化可執行文件, 這個期間會解析LoadCommand
, 這個以後會發送 dyld_image_state_mapped
通知; 在此方法中,讀取image,而後addImage()
到鏡像列表。
對上面生成的 Image
進行連接。這個過程就是將加載進來的二進制變爲可用狀態的過程。其主要作的事有對image
進行 load
(加載),rebase
(基地址復位),bind
(外部符號綁定),咱們能夠查看源碼:
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath) {
......
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
......
this->recursiveRebaseWithAccounting(context);
......
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
}
複製代碼
遞歸加載
全部依賴庫
進內存。
遞歸
對本身以及依賴庫進行rebase操做
。在之前,程序每次加載其在內存中的堆棧基地址都是同樣的,這意味着你的方法,變量等地址每次都同樣的,這使得程序很不安全,後面就出現 ASLR(Address space layout randomization,地址空間佈局隨機化),程序每次啓動後地址都會隨機變化,這樣程序裏全部的代碼地址都是錯的,須要從新對代碼地址進行計算修復才能正常訪問。
對庫中全部nolazy的符號進行bind
,通常的狀況下多數符號都是lazybind的,他們在第一次使用的時候才進行bind。
void initializeMainExecutable() {
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
複製代碼
這一步主要是調用全部image
的Initalizer
方法進行初始化。先爲全部插入並連接完成的動態庫執行初始化操做
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
複製代碼
再爲主程序可執行文件執行初始化操做
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
複製代碼
具體流程爲: ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization
詳細代碼以下:
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo) {
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.imagesAndPaths[0] = { this, this->getPath() };
// 重點
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}
複製代碼
調用 processInitializers
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread, InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images) {
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
for (uintptr_t i=0; i < images.count; ++i) {
// 重點
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
複製代碼
在這裏,對鏡像表中的全部鏡像執行recursiveInitialization
,建立一個未初始化的向上依賴新表。若是依賴中未初始化完畢,則繼續執行processInitializers
,直到所有初始化完畢。
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps) {
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order
if ( this->needsTermination() )
context.terminationRecorder(this);
// 重點 1: let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// 重點 2: initialize this image
bool hasInitializers = this->doInitialization(context);
// 重點 3: let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock();
}
複製代碼
在 recursiveInitialization 函數中,咱們重點關注
- context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);,
- doInitialization(context)
- context.notifySingle(dyld_image_state_initialized, this, NULL);
通知objc咱們要初始化這個鏡像,這裏 經過 notifySingle
函數對sNotifyObjCInit
進行函數調用。
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo) {
......
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
......
}
複製代碼
獲取鏡像文件的真實地址 【*sNotifyObjCInit)(image->getRealPath(), image->machHeader() 】,而 sNotifyObjCInit
是 經過 registerObjCNotifiers
中傳遞的參數(_dyld_objc_notify_init)進行賦值的。
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped) {
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
......
}
複製代碼
繼而找到,registerObjCNotifiers
的 拉起函數 _dyld_objc_notify_register
.
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
複製代碼
_dyld_objc_notify_register
函數是供 objc runtime 使用的,當objc鏡像被映射,取消映射,和初始化時 被調用的註冊處理器。咱們能夠在 libobjc.A.dylib 庫裏,_objc_init
函數中找到其調用。
/*********************************************************************** * _objc_init * Bootstrap initialization. Registers our image notifier with dyld. * Called by libSystem BEFORE library initialization time **********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init(); // 環境變量
tls_init();
static_init(); // C++
runtime_init(); // runtime 初始化
exception_init(); // 異常初始化
cache_init(); // 緩存初始化
_imp_implementationWithBlock_init(); //
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
複製代碼
runtime初始化後,在_objc_init
中註冊了幾個通知,從dyld
這裏接手了幾個活,其中包括負責初始化相應依賴庫裏的類結構,調用依賴庫裏全部的load方法等。
就拿sMainExcuatable來講,它的initializer方法是最後調用的,當initializer方法被調用前dyld
會通知runtime進行類結構初始化,而後再通知調用load
方法,這些目前還發生在main函數前,但因爲lazy bind機制,依賴庫多數都是在使用時才進行bind
,因此這些依賴庫的類結構初始化都是發生在程序裏第一次使用到該依賴庫時才進行的。
當全部的依賴庫的lnitializer都調用完後,dyld::main 函數會返回程序的main()
函數地址,main函數被調用,從而代碼來到了咱們熟悉的程序入口。
那麼 _objc_init
又是如何被調用的呢?
看調用堆棧,在 ImageLoader::recursiveInitialization
函數中,咱們以前關注的重點2: doInitialization
// 重點 2: initialize this image
bool hasInitializers = this->doInitialization(context);
複製代碼
bool ImageLoaderMachO::doInitialization(const LinkContext& context) {
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
複製代碼
在 doModInitFunctions以後 會 先執行 libSystem_initializer
,保證系統庫優先初始化完畢,在這裏初始化 libdispatch_init
,進而在_os_object_init
中 調用 _objc_init
。
因爲 runtime 向 dyld 綁定了回調,當 image 加載到內存後,dyld 會通知 runtime 進行處理
runtime 接手後調用 map_images
作解析和處理,接下來 load_images
中調用 call_load_methods
方法,遍歷
全部加載進來的 Class,按繼承
層級依次調用 Class 的 +load
方法和其 Category 的 +load
方法。
至此,可執行文件和動態庫中全部的符號(Class,Protocol,Selector,IMP,…)都已經按格式成功加載到內存中,被 runtime
所管理,在這以後,runtime 的那些方法(動態添加 Class、swizzle 等等才能生效)
APP是由內核引導啓動的,kernel內核作好全部準備工做後會獲得線程入口及main入口,可是線程不會立刻進入main入口,由於還要加載動態連接器(dyld),dyld會將入口點保存下來,等dyld加載完全部動態連接庫等工做以後,再開始執行main函數。
系統kernel作好啓動程序的初始準備後,交給dyld負責。
dyld接手後,系統先讀取 App 的可執行文件(Mach-O
文件),從裏面獲取dyld
的路徑,而後加載dyld
,dyld
去初始化運行環境,開啓緩存策略,配合 ImageLoader 將二進制文件按格式加載到內存,加載程序相關依賴庫(其中也包含咱們的可執行文件),並對這些庫進行連接
,最後調用每一個依賴庫的初始化
方法,在這一步,runtime
被初始化。當全部依賴庫初始化後,輪到最後一位(程序可執行文件)進行初始化,在這時runtime會對項目中全部類進行類結構初始化
,而後調用全部的load
方法。最後dyld
返回main()
函數地址,main()
函數被調用。
這個過程遠比寫出來的要複雜,這裏只提到了 runtime 這個分支,還有像 GCD、XPC 等重頭的系統庫初始化分支沒有說起(固然,有緩存機制在,它們也不會玩命初始化),總結起來就是 main 函數執行以前,系統作了茫茫多的加載和初始化工做,最終引入那個熟悉的main函數。