iOS 應用程序加載

1. 理論基礎速成

1.1 靜態庫與動態庫

庫是已寫好的、供使用的 可複用代碼,每一個程序都要依賴不少基礎的底層庫。c++

從本質上,庫是一種可執行代碼的二進制形式。能夠被操做系統載入內存執行。庫分爲兩種:靜態庫(.a .lib)和 動態庫 (framework .so .dll)。bootstrap

所謂的靜態、動態指的是 連接的過程windows

將一個程序編譯成可執行程序的步驟以下:緩存

1.1.1 靜態庫

之因此稱之爲【靜態庫】,是由於在連接階段,會將彙編生成的目標文件.o 與 引用的庫一塊兒連接到可執行文件中。對應的連接方式稱爲 靜態連接安全

若是多個進程須要引用到【靜態庫】,在內存中就會存在多份拷貝,如上圖中進程1 用到了靜態庫一、5,進程2也用到了靜態庫一、5,那麼靜態庫一、5在編譯期就分別被連接到了進程1和進程2中,假設靜態庫1佔用2M內存,若是有20個這樣的進程須要用到靜態庫1,將佔用40M的空間。markdown

【靜態庫】的特色以下:閉包

  • 靜態庫對函數庫的連接是在編譯期完成的。執行期間代碼裝載速度快。
  • 使可執行文件變大,浪費空間和資源(佔空間)。
  • 對程序的更新、部署與發佈不方便,須要全量更新。若是 某一個靜態庫更新了,全部使用它的應用程序都須要從新編譯、發佈給用戶。

1.1.2 動態庫

【動態庫】在程序編譯時並不會連接到目標代碼中,而是在運行時才被載入。不一樣的應用程序若是調用相同的庫,那麼在內存中只須要有一份該共享庫的實例,避免了空間浪費問題。同時也解決了靜態庫對程序的更新的依賴,用戶只需更新動態庫便可。架構

【動態庫】在內存中只存在一份拷貝,若是某一進程須要用到動態庫,只需在運行時動態載入便可。app

【動態庫】的特色:dom

  • 動態庫把對一些庫函數的連接載入推遲到程序運行時期(佔時間)。
  • 能夠實現進程之間的資源共享。(所以動態庫也稱爲共享庫)
  • 將一些程序升級變得簡單,不須要從新編譯,屬於增量更新

 

1.2 Mach-O

程序想要運行起來,它的可執行文件格式就要被操做系統所理解,好比 ELF(Executable and Linking Format) 是 Linux 下可執行文件的格式,PE32/PE32+(Portable Executable) 是 windows 的可執行文件的格式,那麼對於 OS XiOS 來講 Mach-O 是其可執行文件的格式。

Mach-O】 爲 Mach Object 文件格式的縮寫,是 iOS 系統不一樣運行時期 可執行文件 的文件類型統稱。它是一種用於 可執行文件、目標代碼、動態庫、內核轉儲的文件格式。

Mach-O】 的三種文件類型:Executable、Dylib、Bundle

  • Executable

Executableapp 的二進制主文件,咱們能夠在 Xcode 項目中的 products 文件中找到它:

  • Dylib

Dylib 是動態庫,動態庫分爲 動態連接庫動態加載庫

動態連接庫:在沒有被加載到內存的前提下,當可執行文件被加載,動態庫也隨着被加載到內存中。【隨着程序啓動而啓動】
動態加載庫:當須要的時候再使用 dlopen 等經過代碼或者命令的方式加載。【程序啓動以後】

  • Bundle

Bundle 是一種特殊類型的Dylib,你沒法對其進行連接。所能作的是在Runtime運行時經過dlopen來加載它,它能夠在macOS 上用於插件。

  • Image 和 Framework

Image (鏡像文件)包含了上述的三種類型;
Framework 能夠理解爲動態庫。

1.2.1 Mach-O的結構

【Mach-O】是一個以數據塊分組的二進制字節流,每一個【Mach-O】文件包括一個Mach-O頭,而後是一系列的載入命令,再是一個或多個段,每一個段包括0到255個塊。

  • Header結構

保存【Mach-O】的一些基本信息,包括運行平臺、文件類型、LoadCommands指令的個數、指令總大小,dyld標記Flags等等。

  • Load Commands

緊跟Header,這些加載指令清晰地告訴加載器如何處理二進制數據,有些命令是由內核處理的,有些是由動態連接器處理的。加載【Mach-O】文件時會使用這部分數據肯定內存分佈以及相關的加載命令,對系統內核加載器和動態鏈接器起指導做用。好比咱們的main()函數的加載地址、程序所需的dyld的文件路徑、以及相關依賴庫的文件路徑。

  • Data

每一個segment的具體數據保存在這裏,包含具體的代碼、數據等等。

1.2.1.1 segment段:

【Mach-O】 鏡像文件 是由 segments 段組成的。

  • 段的名稱爲大寫格式

全部的段都是 page size 的倍數。

  • 在arm64上爲 16kB
  • 其它架構爲 4KB

這裏在普及一下 虛擬內存內存頁 的知識:

具備 VM 機制的操做系統,會對每一個運行的進程建立一個邏輯地址空間 logical address space 或者叫 虛擬地址空間 virtual address space;該空間的大小由操做系統位數決定。

虛擬地址空間 會被分爲相同大小的塊,這些塊被稱爲內存頁(page)。計算機處理器和它的內存管理單元(MMU - memory management unit)維護着一張將程序的 虛擬地址空間 映射到 物理地址 上的分頁表 page table

macOS 和早版本的 iOS 中,分頁大小爲 4kb。在以後的基於A7A8 的系統中,虛擬內存(64位的地址空間)地址空間的分頁大小變爲了16kb,而物理RAM上的內存分頁大小仍然維持在 4kb;基於 A9 及之後的系統,虛擬內存和物理內存的分頁都是16kb

1.2.1.2 section

segment 段內部還有許多的 section 區。section 名稱爲小寫格式。 section 節 實際上只是一個 segment 段的子範圍,它們沒有頁面大小的任何限制,可是它們是不重疊的。

1.2.1.3 常見的segments
  • __TEXT:代碼段,包含頭文件、代碼和只讀常量只讀不可修改

  • __DATA:數據段,包含全局變量,靜態變量等。可讀可寫

  • __LINKEDIT:如何加載程序,包含了方法和變量的元數據(位置,偏移量),以及代碼簽名等信息。只讀不可修改。
1.2.2 Mach-O Universal Files

【Mach-O】 通用文件,將多種架構的 Mach-O 文件合併而成。它經過 header 來記錄不一樣架構在文件中的偏移量,segment 佔多個分頁,header 佔一頁的空間。header 單獨佔一頁 有利於 虛擬內存 的實現。

1.3 虛擬內存

虛擬內存是一層 間接尋址

【虛擬內存】是在物理內存上創建的一個邏輯地址空間。創建在進程物理內存之間的中間層,它向上(應用)提供了一個連續的邏輯地址空間,向下隱藏了物理內存的細節。

虛擬內存被劃分爲一個個大小相同的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

1.4 多進程加載Mach-O 鏡像

  • 因此在多個進程加載【Mach-O】鏡像時,__TEXT__LINKEDIT 由於是隻讀的,都是能夠共享內存的,讀取速度就會很快。
  • __DATA 由於是可讀寫的,就有可能產生dirty page,若是檢測有 clean page 就能夠直接使用,反之就須要從新讀取 DATA page。一旦產生了 dirty page,當dyld執行結束後,__LINKEDIT 須要通知內核當前頁面再也不須要了,當別人須要使用的時候就能夠從新 clean 這些頁面。

1.5 ASLR

有兩種主要的技術來保證應用的安全:ASLRCode Sign

【ASLR】的全稱是Address space layout randomization,翻譯過來就是「地址空間佈局隨機化」App被啓動的時候,程序會被映射到邏輯的地址空間,這個邏輯的地址空間有一個起始地址,而【ASLR】技術使得這個起始地址是隨機的。若是是固定的,那麼黑客很容易就能夠由起始地址+偏移量找到函數的地址。

1.6 Code Signing

【Code Sign】相信大多數開發者都知曉,這裏要提一點的是,爲了在運行時 驗證【Mach-O】 文件的簽名,在進行【Code Sign】的時候,加密哈希不是針對於整個文件,而是針對於每個Page的。並存儲在 __LINKEDIT 中。這就保證了在dyld進行加載的時候,能夠對每個page進行獨立的驗證

1.7 exec()

exec() 是一個系統調用。系統內核把應用程序映射到新的地址空間,且每次起始位置都是隨機的(由於ASLR)。並將起始位置到0x000000 這段範圍的進程權限都標記爲不可讀寫不可執行。若是是32 位進程,這個範圍至少是4kb ;若是是64位進程則至少是4GBNULL指針引用和指針截斷偏差都是會被它捕獲,這個範圍也叫作 PAGEZERO

1.8 dyld

內核完成映射進程的工做後,會將名字爲 dyldMach-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

1.9 共享緩存

dyld加載時,爲了優化程序啓動,啓用了共享緩存(shared cache)技術。共享緩存會在進程啓動時被dyld映射到內存中,以後,當任何Mach-O鏡像加載時,dyld首先會檢查該Mach-O鏡像與所需的動態庫是否在共享緩存中,若是存在,則直接將它在共享內存中的內存地址映射到進程的內存地址空間。在程序依賴的系統動態庫不少的狀況下,這種作法對程序啓動性能是有明顯提高的。

1.10 dyld 流程

  • Load dylibs

從主執行文件header獲取到須要加載的所依賴的動態庫列表,而header早就被內核映射過。而後它須要找到每一個dylib,而後打開文件,讀取文件起始位置,確保它是Mach-O文件。接着會找到代碼簽名並將其註冊到內核。而後在dylib文件的每一個segment 上調用 mmap()。應用所依賴的dylib文件可能會再依賴其餘dylib,因此dyld所須要加載的是動態庫列表一個遞歸依賴的集合。通常應用會加載100到400 個dylib文件,但大部分都是系統的dylib,它們會被預先計算和緩存起來,加載速度很快。

  • Fix-ups

在加載全部的動態連接庫以後,它們只是處在相互獨立的狀態,須要將它們綁定起來,這就是Fix-ups。代碼簽名使得咱們不能修改指令,那樣就不能讓一個dylib 調用另外一個 dylib,這是就須要不少間接層。

Mach-O中有不少符號,有指向當前 Mach-O 的,也有指向其餘 dylib 的,好比printf。那麼,在運行時,代碼如何準確的找到printf的地址呢?

Mach-O中採用了PIC技術,全稱是Position Independ code。意味着代碼能夠被加載到間接的地址上。當你的程序要調用printf的時候,會先在 __DATA 段中創建一個指針指向printf,在經過這個指針實現間接調用。dyld這時候須要作一些fix-up工做,即幫助應用程序找到這些符號的實際地址。主要包括兩部分:rebasingbinding

  • Rebasing 和 Binding

Rebasing:在鏡像內部調整指針的指向。 Binding: 將指針指向鏡像外部的內容。

之因此須要Rebase,是由於剛剛提到的 ASLR 使得地址隨機化,致使起始地址不固定,另外因爲 Code Sign,致使不能直接修改 ImageRebase的時候只須要增長對應的偏移量便可。(待Rebase的數據都存放在 __LINKEDIT中,能夠經過MachOView查看:Dynamic Loader Info -> Rebase Info)

Binding就是將這個二進制調用的外部符號進行綁定的過程。 好比咱們objc代碼中須要使用到NSObject, 即符號_OBJC_CLASS_$_NSObject,可是這個符號又不在咱們的二進制中,在系統庫 Foundation.framework中,所以就須要Binding這個操做將對應關係綁定到一塊兒。

Rebase解決了內部的符號引用問題,而外部的符號引用則是由Bind解決。在解決Bind的時候,是根據字符串匹配的方式查找符號表,因此這個過程相對於Rebase來講是略慢的。

1.11 dyld 2 和 dyld 3

iOS 13以前,全部的第三方App都是經過dyld 2來啓動 App 的,主要過程以下:

  • 解析 Mach-OHeaderLoad Commands,找到其依賴的庫,並遞歸找到全部依賴的庫
  • 加載Mach-O文件
  • 進行符號查找
  • 綁定和變基
  • 運行初始化程序

dyld 3被分爲了三個組件:

  • 一個進程外的Mach-O 解析器

預先處理了全部可能影響啓動速度的search path、@rpaths 和環境變量 而後分析Mach-OHeader和依賴,並完成了全部符號查找的工做 最後將這些結果建立成一個啓動閉包 這是一個普通的daemon進程,可使用一般的測試架構

  • 一個進程內的引擎,用來運行啓動閉包

這部分在進程中處理 驗證啓動閉包的安全性,而後映射到dylib之中,再跳轉到main函數 不須要解析Mach-OHeader 和依賴,也不須要符號查找。

  • 一個啓動閉包緩存服務

系統App的啓動閉包被構建在一個Shared Cache 中,咱們甚至不須要打開一個單獨的文件 對於第三方的App,咱們會在App安裝或者升級的時候構建這個啓動閉包。 在iOS、tvOS、watchOS中,這一切都是App啓動以前完成的。在macOS上,因爲有Side Load App,進程內引擎會在首次啓動的時候啓動一個daemon進程,以後就可使用啓動閉包啓動了。

dyld 3 把不少耗時的查找、計算和I/O 的事件都預先處理好,這使得啓動速度有了很大的提高。

二、App 加載流程

有了前面的知識儲備,接下來將探索app的加載流程。

在應用程序的入口 main()函數以前斷點,查看堆棧信息 main( )函數以前

能夠看到,先於main函數調用的是 start,同時,這一流程是由libdyld.dylib庫執行的。dyld 是開源庫,能夠下載源碼探索。點擊下載dyld 源碼

爲了看到更詳細的調用過程,咱們在項目中的 ViewController+ (void) load 方法打斷點。詳細堆棧信息以下

2.1 _dyld_start

可見,調用流程是從 _dyld_start 開始的,咱們在下載好的源碼中搜索 _dyld_start 。在 dyldStartup.s 文件中找到了入口,這裏是用匯編實現的,儘管在不一樣架構下有所區別,但都是會調用 dyldbootstrap 命名空間下的start 方法,這和上面的堆棧順序也是相同的。

call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
複製代碼

2.2 dyldbootstrap::start

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-Oheader 獲得偏移量 appSlide,而後調用dyld 命名空間下的_main 方法。

2.3 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( ).

  • 注1: sMainExecutable = instantiateFromLoadedImage(....) 與 loadInsertedDylib(...)

這一步 dyld 將咱們可執行文件以及插入的 lib 加載進內存,生成對應的imagesMainExecutable 對應着咱們的可執行文件,裏面包含了咱們項目中全部新建的類。 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() 到鏡像列表。

  • 注2: link(sMainExecutable,...) 和 link(image,....)

對上面生成的 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);
}
複製代碼
  • 注2.1: recursiveLoadLibraries(context, preflightOnly, loaderRPaths)

遞歸加載全部依賴庫進內存。

  • 注2.2:recursiveRebase(context)

遞歸對本身以及依賴庫進行rebase操做。在之前,程序每次加載其在內存中的堆棧基地址都是同樣的,這意味着你的方法,變量等地址每次都同樣的,這使得程序很不安全,後面就出現 ASLR(Address space layout randomization,地址空間佈局隨機化),程序每次啓動後地址都會隨機變化,這樣程序裏全部的代碼地址都是錯的,須要從新對代碼地址進行計算修復才能正常訪問。

  • 注2.3:recursiveBindWithAccounting(context, forceLazysBound, neverUnload);

對庫中全部nolazy的符號進行bind,通常的狀況下多數符號都是lazybind的,他們在第一次使用的時候才進行bind。

2.4 initializeMainExecutable()

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]);
}
複製代碼

這一步主要是調用全部imageInitalizer方法進行初始化。先爲全部插入並連接完成的動態庫執行初始化操做

sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
複製代碼

再爲主程序可執行文件執行初始化操做

sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
複製代碼

具體流程爲: ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization

詳細代碼以下:

2.5 ImageLoader::runInitializers

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

2.6 ImageLoader::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,直到所有初始化完畢。

2.7 ImageLoader::recursiveInitialization

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);
  • context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

通知objc咱們要初始化這個鏡像,這裏 經過 notifySingle 函數對sNotifyObjCInit 進行函數調用。

2.7.1 context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo)

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

  • this->doInitialization(context);
// 重點 2: initialize this image   
bool hasInitializers = this->doInitialization(context);
複製代碼

2.7.2 ImageLoaderMachO::doInitialization

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的路徑,而後加載dylddyld去初始化運行環境,開啓緩存策略,配合 ImageLoader 將二進制文件按格式加載到內存,加載程序相關依賴庫(其中也包含咱們的可執行文件),並對這些庫進行連接,最後調用每一個依賴庫的初始化方法,在這一步,runtime被初始化。當全部依賴庫初始化後,輪到最後一位(程序可執行文件)進行初始化,在這時runtime會對項目中全部類進行類結構初始化,而後調用全部的load方法。最後dyld返回main()函數地址,main()函數被調用。

這個過程遠比寫出來的要複雜,這裏只提到了 runtime 這個分支,還有像 GCD、XPC 等重頭的系統庫初始化分支沒有說起(固然,有緩存機制在,它們也不會玩命初始化),總結起來就是 main 函數執行以前,系統作了茫茫多的加載和初始化工做,最終引入那個熟悉的main函數。

相關文章
相關標籤/搜索