iOS 底層探索 - 應用加載

在這裏插入圖片描述
App 從被用戶在主屏幕上點擊之後就開啓了它的生命週期,那麼在這之中,究竟發生了什麼呢?讓我們從 App 啓動開始探索。在探索之前,我們需要熟悉一些前導知識點。

一、前導知識

以下參考自 WWDC 2016 Optimizing App Startup Time

1.1 Mach-O

image.png

Mach-O is a bunch of file types for different run time executables.
Mach-OiOS 系統不同運行時期可執行的文件的文件類型統稱。

維基百科上關於 Mach-O 的描述:

Mach-O 是 Mach object 文件格式的縮寫,它是一種用於記錄可執行文件、對象代碼、共享庫、動態加載代碼和內存轉儲的文件格式。作爲 a.out 格式的替代品,Mach-O 提供了更好的擴展性,並提升了符號表中信息的訪問速度。
大多數基於 Mach 內核的操作系統都使用 Mach-O。NeXTSTEP、OS X 和 iOS 是使用這種格式作爲本地可執行文件、庫和對象代碼的例子。

image.png

Mach-O 有三種文件類型: ExecutableDylibBundle

  • Executable 類型

So the first executable, that’s the main binary in an app, it’s also the main binary in an app extension.
executableapp 的二進制主文件,同時也是 app extension 的二進制主文件

我們一般可以在 Xcode 項目中的 Products 文件夾中找到它:

image.png

image.png

image.png

如上圖箭頭所示,App加載流程 就是我們 App 的二進制主文件。

  • Dylib 類型

A dylib is a dynamic library, on other platforms meet, you may know those as DSOs or DLLs.
dylib 是動態庫,在其他平臺也叫 DSO 或者 DLL

對於接觸 iOS 開發比較早的同學,可能知道我們在 Xcode 7 之前添加一些比如 sqlite 的庫的時候,其後綴名爲 dylib,而 Xcode 7 之後後綴名都改成了 tbd

這裏引用 StackoverFlow 上的一篇回答。

So it appears that the .dylib file is the actual library of binary code that your project is using and is located in the /usr/lib/ directory on the user’s device. The .tbd file, on the other hand, is just a text file that is included in your project and serves as a link to the required .dylib binary. Since this text file is much smaller than the binary library, it makes the SDK’s download size smaller.
看起來 .dylib 文件是項目中真正使用到的二進制庫文件,它位於用戶設備上的 /usr/lib 目錄下。而 .tbd 文件,只是位於你項目中的一個文本文件,它扮演的是鏈接到真正的 .dylib 二進制文件的角色。因爲文本文件的大小遠遠小於二進制文件的大小,所以讓 Xcode 的SDK` 的下載大小更小。

這裏再插一句,那麼有動態庫,肯定就有靜態庫,它們的區別是什麼呢?

我們先梳理一下整個的編譯過程。

image.png

當然,這個過程中間其實還設計到編譯器前端的 詞法分析語法分析語義分析優化 等流程,我們在後面探索 LLVMClang 的時候會詳細介紹。

回到剛纔的話題,靜態庫和動態庫的區別:

Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime.

靜態庫和動態庫都是編譯好的二進制文件,只是用法不同。那爲什麼要分動態和靜態庫呢?

image.png

image.png

通過上面兩幅圖我們可以知道:

  • 靜態庫表現爲:在鏈接階段會將彙編生成的目標文件與引用的庫一起鏈接打包進可執行文件中。
  • 動態庫表現爲:程序編譯並不會鏈接到目標代碼中,在程序可執行文件裏面會保留對動態庫的引用。其中,動態庫分爲動態鏈接庫和動態加載庫。
    • 動態鏈接庫:在沒有被加載到內存的前提下,當可執行文件被加載,動態庫也隨着被加載到內存中。在 Linked Framework and Libraries 設置的一些 share libraries。【隨着程序啓動而啓動】
    • 動態加載庫:當需要的時候再使用 dlopen 等通過代碼或者命令的方式來加載。【在程序啓動之後】
  • Bundle 類型

Now a bundle’s a special kind of dylib that you cannot link against, all you can do is load it at run time by an dlopen and that’s used on a Mac OS for plug-ins.
現階段 Bundle 是一種特殊類型的 dylib,你是無法對其進行鏈接的。你所能做的是在 Runtime 運行時去通過 dlopen 來加載它,它可以在 macOS 上用於插件。

  • ImageFramework

Image refers to any of these three types.
鏡像文件包含了上述的三種文件類型

a framework is a dylib with a special directory structure around it to holds files needed by that dylib.
有很多東西都叫做 Framework,但在本文中,Framework 指的是一個 dylib,它周圍有一個特殊的目錄結構來保存該 dylib 所需的文件。

1.1.1 Mach-O 結構分析

1.1.1.1 segment 段

image.png

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

  • 段的名稱爲大寫格式

    所有的段都是 page size 的倍數。
  • arm64 上段大小爲 16 字節
  • 其它架構爲 4 字節

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

具有 VM 機制的操作系統,會對每個運行的進程創建一個邏輯地址空間 logical address space 或者叫虛擬地址空間 virtual address space;該空間的大小由操作系統位數決定:32 位的操作系統,其邏輯地址空間的大小爲 4GB,64位的操作系統爲 18 exabyes(其計算方式是 2^32 || 2^64)。

image.png

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

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

1.1.1.2 section

image.png

segment 段內部還有許多的 section 區。section 名稱爲小寫格式。

But sections are really just a subrange of a segment, they don’t have any of the constraints of being page size, but they are non-overlapping.
但是 sections 節實際上只是一個 segment 段的子範圍,它們沒有頁面大小的任何限制,但是它們是不重疊的。

通過 MachOView 工具查看 app 的二進制可執行文件可以查看到:

image.png

1.1.1.3 常見的 segments

  • __TEXT:代碼段,包括頭文件、代碼和常量。只讀不可修改

image.png

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

image.png

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

image.png

1.1.2 Mach-O Universal Files

image.png

Mach-O 通用文件,將多種架構的 Mach-O 文件合併而成。它通過 header 來記錄不同架構在文件中的偏移量,segement 佔多個分頁,header佔一頁的空間。可能有人會覺得 header 單獨佔一頁會浪費空間,但這有利於虛擬內存的實現。

1.2 虛擬內存

image.png

虛擬內存是一層間接尋址

虛擬內存解決的是管理所有進程使用物理 RAM 的問題。通過添加間接層來讓每個進程使用邏輯地址空間,它可以映射到 RAM 上的某個物理頁上。這種映射不是一對一的,邏輯地址可能映射不到 RAM 上,也可能有多個邏輯地址映射到同一個物理 RAM 上。

  • 針對第一種情況,當進程要存儲邏輯地址內容時會觸發 page fault
  • 而第二種情況就是多進程共享內存。
  • 對於文件可以不用一次性讀入整個文件,可以使用分頁映射 mmap() 的方式讀取。也就是把文件某個片段映射到進程邏輯內存的某個頁上。當某個想要讀取的頁沒有在內存中,就會觸發 page fault,內核只會讀入那一頁,實現文件的懶加載。也就是說 Mach-O 文件中的 __TEXT 段可以映射到多個進程,並可以懶加載,且進程之間共享內存
  • __DATA 段是可讀寫的。這裏使用到了 Copy-On-Write 技術,簡稱 COW。也就是多個進程共享一頁內存空間時,一旦有進程要做寫操作,它會先將這頁內存內容複製一份出來,然後重新映射邏輯地址到新的 RAM 頁上。也就是這個進程自己擁有了那頁內存的拷貝。這就涉及到了 clean/dirty page 的概念。dirty page 含有進程自己的信息,而 clean page 可以被內核重新生成(重新讀磁盤)。所以 dirty page 的代價大於 clean page

1.3 多進程加載 Mach-O 鏡像

image.png

  • 所以在多個進程加載 Mach-O 鏡像時 __TEXT__LINKEDIT 因爲只讀,都是可以共享內存的,讀取速度就會很快。
  • __DATA 因爲可讀寫,就有可能會產生 dirty page,如果檢測到有 clean page 就可以直接使用,反之就需要重新讀取 DATA page。一旦產生了 dirty page,當 dyld 執行結束後,__LINKEDIT 需要通知內核當前頁面不再需要了,當別人需要的使用時候就可以重新 clean 這些頁面。

image.png

1.4 ASLR

ASLR (Address Space Layout Randomization) 地址空間佈局隨機化,鏡像會在隨機的地址上加載。

1.5 Code Signing

可能我們認爲 Xcode 會把整個文件都做加密 hash 並用做數字簽名。其實爲了在運行時驗證 Mach-O 文件的簽名,並不是每次重複讀入整個文件,而是把每頁內容都生成一個單獨的加密散列值,並存儲在 __LINKEDIT 中。這使得文件每頁的內容都能及時被校驗確並保不被篡改。

1.6 exec()

image.png

Exec is a system call. When you trap into the kernel, you basically say I want to replace this process with this new program.

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

1.7 dyld

image.png

Unix 的前二十年很安逸,因爲那時還沒有發明動態鏈接庫。有了動態鏈接庫後,一個用於加載鏈接庫的幫助程序被創建。在蘋果的平臺裏是 dyld,其他 Unix 系統也有 ld.so。 當內核完成映射進程的工作後會將名字爲 dyldMach-O 文件映射到進程中的隨機地址,它將 PC 寄存器設爲 dyld 的地址並運行。dyld 在應用進程中運行的工作是加載應用依賴的所有動態鏈接庫,準備好運行所需的一切,它擁有的權限跟應用一樣。

1.8 dyld 流程

image.png

  • Load dylibs

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

  • Fix-ups

在加載所有的動態鏈接庫之後,它們只是處在相互獨立的狀態,需要將它們綁定起來,這就是 Fix-ups。代碼簽名使得我們不能修改指令,那樣就不能讓一個 dylib 的調用另一個 dylib。這時需要加很多間接層。
現代 code-gen 被叫做動態 PIC(Position Independent Code),意味着代碼可以被加載到間接的地址上。當調用發生時,code-gen 實際上會在 __DATA 段中創建一個指向被調用者的指針,然後加載指針並跳轉過去。所以 dyld 做的事情就是修正(fix-up)指針和數據。Fix-up 有兩種類型,rebasingbinding

  • Rebasing 和 Binding

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

dyld 的時間線由上圖可知爲:

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

1.9 dyld2 && dyld3

image.png

iOS 13 之前,所有的第三方 App 都是通過 dyld 2 來啓動 App 的,主要過程如下:

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

dyld3 被分爲了三個組件

  • 一個進程外的 MachO 解析器
    • 預先處理了所有可能影響啓動速度的 search path@rpaths 和環境變量
    • 然後分析 Mach-OHeader 和依賴,並完成了所有符號查找的工作
    • 最後將這些結果創建成了一個啓動閉包
    • 這是一個普通的 daemon 進程,可以使用通常的測試架構
  • 一個進程內的引擎,用來運行啓動閉包
    • 這部分在進程中處理
    • 驗證啓動閉包的安全性,然後映射到 dylib 之中,再跳轉到 main 函數
    • 不需要解析 Mach-OHeader 和依賴,也不需要符號查找。
  • 一個啓動閉包緩存服務
    • 系統 App 的啓動閉包被構建在一個 Shared Cache 中, 我們甚至不需要打開一個單獨的文件
    • 對於第三方的 App,我們會在 App 安裝或者升級的時候構建這個啓動閉包。
    • iOStvOSwatchOS中,這這一切都是 App 啓動之前完成的。在 macOS 上,由於有 Side Load App,進程內引擎會在首次啓動的時候啓動一個 daemon 進程,之後就可以使用啓動閉包啓動了。

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

好了,先導知識就總結到這裏,接下來讓我們調整呼吸進入下一章~

二、App 加載分析

我們在探索 iOS 底層的時候,對於對象、類、方法有了一定的認知哦,接下來我們就一起來探索一下應用是怎麼加載的。

我們直接新建一個 Single View App 的項目,然後在 main.m 中打一個斷點:

image.png

然後我們可以看到在 main 方法執行前有一步 start,而這一流程是由 libdyld.dylib 這個動態庫來執行的。

image.png

這個現象說明了什麼呢?說明我們的 appmain 函數執行之前其實還通過 dyld 做了很多事情。那爲了搞清楚具體的流程,我們不妨從 Apple OpenSource 上下載 dyld 的源碼來進行探索。

我們選擇最新的 655.1.1 版本:

image.png

三、dyld 源碼分析

面對 dyld 的源碼,我們不可能一行一行的去分析。我們不妨在剛纔創建的項目中斷點一下 load 方法,看下調用堆棧:

image.png

這一次我們發現,load 方法的調用要早於 main 函數的調用,其次,我們得到了一個非常有價值的線索: _dyld_start

3.1 _dyld_start

我們直接在 dyld 655.1.1 中全局搜索這個 _dyld_start,我們可以來到 dyldStartup.s 這個彙編文件,然後我們聚焦於 arm64 架構下的彙編代碼:

image.png

對於這裏的彙編代碼,我們肯定也沒必要逐行分析,我們直接定位到 bl 語句後面(bl 在彙編層面是跳轉的意思):

bl	__ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm

我們可以看到這裏有一行註釋:

// call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)

這行註釋的意思是調用位於 dyldbootstrap 命名空間下的 start 方法,我們繼續搜索一下這個 start 方法,結果位於 dyldInitialization.cpp 文件(從文件名我們可以看出該文件主要是用來初始化 dyld),這裏查找 start 的時候可能會有很多結果,我們其實可以先搜索命名空間,再搜索 start 方法。

3.2 dyldbootstrap::start

start 方法源碼如下:

//
//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
				intptr_t slide, const struct macho_header* dyldsMachHeader,
				uintptr_t* startGlue)
{
	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    slide = slideOfMainExecutable(dyldsMachHeader);
    bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
    shouldRebase = true;
#endif
    if ( shouldRebase ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

	// allow dyld to use mach messaging
	mach_init();

	// 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(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

	// now that we are done bootstrapping dyld, call dyld's main
	uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
	return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

我們剛纔探索到了 start 方法,具體流程如下:

image.png

  • 根據 dyldMach-O 文件的 header 判斷是否需要對 dyld 這個 Mach-O 進行 rebase 操作

image.png

  • 初始化 mach,使得 dyld 可以進行 mach 通訊。

image.png

  • 內核將 env 指針設置爲剛好超出 agv 數組的末尾;內核將 apple 指針設置爲剛好超出 envp 數組的末尾

image.png

  • 棧溢出保護

image.png

  • 讀取 app 主二進制文件 Mach-Oheader 來得到偏移量 appSlide,然後調用 dyld 命名空間下的 _main 方法。

3.3 dyldbootstrap::_main

我們通過搜索來到 dyld.cpp 文件下的 _main 方法:

image.png

_main方法 官方的註釋如下:

dyld 的入口。內核加載了 dyld 然後跳轉到 __dyld_start 來設置一些寄存器的值然後調用到了這個方法。
返回 __dyld_start 所跳轉到的目標程序的 main 函數地址。

我們乍一看,這個方法有四五百行,所以我們不能老老實實的一行一行來看,這樣太累了。我們應該着重於有註釋的地方。

image.png

  • 我們首先可以看到這裏是從環境變量中獲取主要可執行文件的 cdHash 值。這個哈希值 mainExecutableCDHash 在後面用來校驗 dyld3 的啓動閉包。

image.png

  • 上圖代碼作用是追蹤 dyld 的加載。然後判斷當前是否爲模擬器環境,如果不是模擬器,則追蹤主二進制可執行文件的加載。

image.png

  • 顯示宏定義判斷是否爲 macOS 執行環境,如果是則判斷 DYLD_ROOT_PATH 環境變量是否存在,如果存在,然後判斷模擬器是否有自己的 dyld,如果有就使用,如果沒有,則返回錯誤信息。

image.png

  • 打印日誌:dyld 啓動開始
  • 根據傳入 dyldbootstrap::_main 方法的參數來設置上下文
  • 拾取指向 exec 路徑的指針
  • dyl d移除臨時 apple [0] 過渡代碼
  • 判斷 exec 路徑是否爲絕對路徑,如果爲相對路徑,使用 cwd 轉化爲絕對路徑
  • 爲了後續的日誌打印從 exec 路徑中取出進程的名稱 (strrchr 函數是獲取第二個參數出現的最後的一個位置,然後返回從這個位置開始到結束的內容)
  • 根據 App 主二進制可執行文件 Mach-OHeader 的內容配置進程的一些限制條件

image.png

  • 判斷是否爲 macOS 執行環境,如果是的話,再判斷上下文的一些配置屬性是否被設置了,如果沒有被設置,則再次進行一次 setContext 上下文配置操作。
  • 根據傳入的參數 envp 檢查環境變量
  • 默認未初始化的後備路徑
  • 判斷是否爲 macOS 執行環境,如果是的話,再判斷當前 appMach-O 可執行文件是否爲 iOSMac 類型且不爲 macOS 類型的話,則重置上下文的根路徑,然後再判斷 DYLD_FALLBACK_LIBRARY_PATHDYLD_FALLBACK_FRAMEWORK_PATH 這兩個環境變量是否都是默認後備路徑,如果是的話賦值爲受限的後備路徑。

image.png

  • 根據環境變量 DYLD_PRINT_OPTSDYLD_PRINT_ENV 來判斷是否需要打印
  • 通過當前 appMach-O 可執行文件的 headerASLR 之後的偏移量來獲取架構信息。在這裏會判斷如果是 GC 的程序則會禁用掉共享緩存。

image.png

image.png

  • 判斷共享緩存是否開啓,如果開啓了就將共享緩存映射到當前進程的邏輯內存空間內

image.png

  • 檢查共享緩存這裏會先判斷 appMach-O 二進制可執行文件是否有段覆蓋了共享緩存區域,如果覆蓋了則禁用共享緩存。但是這裏的前提是 macOS,在 iOS 中,共享緩存是必需的。

image.png

這裏爲了方便查看,我們可以摺疊一些分支條件。

  • 通過共享緩存中的頭的版本信息來判斷是走 dyld 2 還是 dyld 3 的流程

3.4 dyld3 的處理

image.png

  • 由於 dyld3 會創建一個啓動閉包,我們需要來讀取它,這裏會現在緩存中查找是否有啓動閉包的存在,前面我們已經說過了,系統級的 app 的啓動閉包是存在於共享緩存中,而我們自己開發的 app 的啓動閉包是在 app 安裝或者升級的時候構建的,所以這裏檢查 dyld 中的緩存是有意義的。

image.png

  • 宏定義判斷代碼執行條件爲真機。
  • 如果 dyld 緩存中沒有找到啓動閉包或者找到了啓動閉包但是驗證失敗(我們最開始提到的 cdHash 在這裏出現了)
    • 從啓動閉包緩存中查找
      • 如果還是沒有找到,那就創建一個新的啓動閉包

image.png

  • 打印日誌信息:dyld3 啓動開始
  • 嘗試通過啓動閉包進行啓動
    • 如果啓動失敗,則創建一個新的啓動閉包嘗試再次啓動
    • 如果啓動成功,由於 start() 是以函數指針的方式調用 _main 方法的返回的指針,需要進行簽名。

至此,dyld3 的流程就處理完畢,我們再接着往下分析 dyld2 的流程。

3.5 dyld2 的處理

image.png

  • 這裏會添加 dyld 的鏡像文件到 UUID 列表中,主要的目的是啓用堆棧的符號化。

image.png


reloadAllImages

ImageLoader 是一個用於加載可執行文件的基類,它負責鏈接鏡像,但不關心具體文件格式,因爲這些都交給子類去實現。每個可執行文件都會對應一個 ImageLoader實例。ImageLoaderMachO 是用於加載 Mach-O 格式文件的 ImageLoader 子類,而 ImageLoaderMachOClassicImageLoaderMachOCompressed 都繼承於 ImageLoaderMachO,分別用於加載那些 __LINKEDIT 段爲傳統格式和壓縮格式的 Mach-O 文件。

接下來就來到重頭戲了 reloadAllImages 了:

image.png


實例化主程序

這裏我們看到有一行代碼:

// instantiate ImageLoader for main executable
		sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

顯然,在這裏我們的主程序被實例化了,我們進入這個方法內部:

image.png

這裏相當於要爲已經映射到主可執行文件中的文件創建一個 ImageLoader*

從上面代碼我們不難看出這裏真正執行的邏輯是 ImageLoaderMachO::instantiateMainExecutable 方法:

image.png

我們再進入 sniiffLoadCommands 方法內部:

image.png

通過註釋不難看出:sniiffLoadCommands 會確定此 mach-o 文件是否具有原始的或壓縮的 LINKEDIT 以及 mach-o 文件的 segement 的個數。

sniiffLoadCommands 完成後,判斷 LINKEDIT 是壓縮的格式還是傳統格式,然後分別調用對應的 instantiateMainExecutable 方法來實例化主程序。


加載任何插入的動態庫
image.png


鏈接庫

image.png

先是鏈接主二進制可執行文件,然後鏈接任何插入的動態庫。這裏都用到了 link 方法,在這個方法內部會執行遞歸的 rebase 操作來修正 ASLR 偏移量問題。同時還會有一個 recursiveApplyInterposing 方法來遞歸的將動態加載的鏡像文件插入。


運行所有初始化程序

image.png

完成鏈接之後需要進行初始化了,這裏會來到 initializeMainExecutable:

image.png

這裏注意執行順序:

  • 先爲所有插入並鏈接完成的動態庫執行初始化操作
  • 然後再爲主程序可執行文件執行初始化操作

image.png

runInitializers 內部我們繼續探索到 processInitializers:

image.png

然後我們來到 recursiveInitialization:

image.png

然後我們來到 notifySingle:

image.png

箭頭所示的地方是獲取鏡像文件的真實地址。

我們全局搜索一下 sNotifyObjcInit 可以來到 registerObjCNotifiers

image.png

接着搜索 registerObjCNotifiers

image.png

此時,我們打開 libObjc 的源碼可以看到:

image.png

上面這一連串的跳轉,結果很顯然:dyld 註冊了回調才使得 libobjc 能知道鏡像何時加載完畢。

image.png

ImageLoader::recursiveInitialization 方法中還有一個 doInitialization 值得注意,這裏是真正做初始化操作的地方。

image.png

doInitialization 主要有兩個操作,一個是 doImageInit,一個是 doModInitFunctions:

image.png

doImageInit 內部會通過初始地址 + 偏移量拿到初始化器 func,然後進行簽名的驗證。驗證通過後還要判斷初始化器是否在鏡像文件中以及 libSystem 庫是否已經初始化,最後才執行初始化器。


通知監聽 dyld 的 main

image.png

一切工作做完後通知監聽 dyldmain,然後爲主二進制可執行文件找到入口,最後對結果進行簽名。

四、探索 _objc_init

image.png

我們直接通過 LLDB 大法來斷點調試 libObjc 中的 _objc_init,然後通過 bt 命令打印出當前的調用堆棧,根據上一節我們探索 dyld 的源碼,此刻一切的一切都是那麼的清晰明瞭:

image.png

我們可以看到 dyld 的最後一個流程是 doModInitFunctions 方法的執行。

我們打開 libSystem 的源碼,全局搜索 libSystem_initializer 可以看到:

image.png

然後我們打開 libDispatch 的源碼,全局搜索 libdispatch_init 可以看到:

image.png

我們再搜索 _os_object_init:

image.png

完美~,_objc_init 在這裏就被調用了。所以 _objc_init 的流程是

dyld -> libSystem -> libDispatch -> libObc -> _objc_init

五、總結

本文主要探索了 app 啓動之後 dyld 的流程,整個分析過程確實比較複雜,但在探索的過程中,我們不僅對底層源碼有了新的認知,同時對於優化我們 app 啓動也是有很多好處的。下一章,我們會對 objc_init 內部的 map_imagesload_images 進行更深入的分析,敬請期待~

六、參考資料

Optimizing App Startup Time

優化 App 啓動

iOS 開發中的『庫』(一)

iOS應用的內存管理(二)

優化 App 的啓動時間

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章