iOS 底層探索 - 應(yīng)用加載

iOS 底層探索 - 應(yīng)用加載.png

iOS 底層探索系列

App 從被用戶在主屏幕上點(diǎn)擊之后就開啟了它的生命周期粱栖,那么在這之中舞吭,究竟發(fā)生了什么呢?讓我們從 App 啟動開始探索谨垃。在探索之前,我們需要熟悉一些前導(dǎo)知識點(diǎn)龄章。

一袜香、前導(dǎo)知識

以下參考自 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 系統(tǒng)不同運(yùn)行時期可執(zhí)行的文件的文件類型統(tǒng)稱撕予。

維基百科上關(guān)于 Mach-O 的描述:

Mach-O 是 Mach object 文件格式的縮寫,它是一種用于記錄可執(zhí)行文件蜈首、對象代碼实抡、共享庫、動態(tài)加載代碼和內(nèi)存轉(zhuǎn)儲的文件格式疾就。作為 a.out 格式的替代品澜术,Mach-O 提供了更好的擴(kuò)展性,并提升了符號表中信息的訪問速度猬腰。
大多數(shù)基于 Mach 內(nèi)核的操作系統(tǒng)都使用 Mach-O。NeXTSTEP猜敢、OS X 和 iOS 是使用這種格式作為本地可執(zhí)行文件姑荷、庫和對象代碼的例子盒延。

image.png

Mach-O 有三種文件類型: ExecutableDylib鼠冕、Bundle

  • Executable 類型

So the first executable, that's the main binary in an app, it's also the main binary in an app extension.
executableapp 的二進(jìn)制主文件添寺,同時也是 app extension 的二進(jìn)制主文件

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

image.png
image.png
image.png

如上圖箭頭所示计露,App加載流程 就是我們 App 的二進(jìn)制主文件蚕礼。

  • Dylib 類型

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

對于接觸 iOS 開發(fā)比較早的同學(xué)华望,可能知道我們在 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 文件是項(xiàng)目中真正使用到的二進(jìn)制庫文件欧引,它位于用戶設(shè)備上的 /usr/lib 目錄下膊升。而 .tbd 文件,只是位于你項(xiàng)目中的一個文本文件谭企,它扮演的是鏈接到真正的 .dylib 二進(jìn)制文件的角色廓译。因?yàn)槲谋疚募拇笮∵h(yuǎn)遠(yuǎn)小于二進(jìn)制文件的大小,所以讓 Xcode 的SDK` 的下載大小更小债查。

這里再插一句非区,那么有動態(tài)庫,肯定就有靜態(tài)庫盹廷,它們的區(qū)別是什么呢征绸?

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

image.png

當(dāng)然俄占,這個過程中間其實(shí)還設(shè)計(jì)到編譯器前端的 詞法分析管怠、語法分析語義分析缸榄、優(yōu)化 等流程渤弛,我們在后面探索 LLVMClang 的時候會詳細(xì)介紹。

回到剛才的話題碰凶,靜態(tài)庫和動態(tài)庫的區(qū)別:

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

靜態(tài)庫和動態(tài)庫都是編譯好的二進(jìn)制文件暮芭,只是用法不同。那為什么要分動態(tài)和靜態(tài)庫呢欲低?

image.png
image.png

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

  • 靜態(tài)庫表現(xiàn)為:在鏈接階段會將匯編生成的目標(biāo)文件與引用的庫一起鏈接打包進(jìn)可執(zhí)行文件中辕宏。
  • 動態(tài)庫表現(xiàn)為:程序編譯并不會鏈接到目標(biāo)代碼中,在程序可執(zhí)行文件里面會保留對動態(tài)庫的引用砾莱。其中瑞筐,動態(tài)庫分為動態(tài)鏈接庫和動態(tài)加載庫。
    • 動態(tài)鏈接庫:在沒有被加載到內(nèi)存的前提下腊瑟,當(dāng)可執(zhí)行文件被加載聚假,動態(tài)庫也隨著被加載到內(nèi)存中块蚌。在 Linked Framework and Libraries 設(shè)置的一些 share libraries”旄瘢【隨著程序啟動而啟動】
    • 動態(tài)加載庫:當(dāng)需要的時候再使用 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.
現(xiàn)階段 Bundle 是一種特殊類型的 dylib瘪贱,你是無法對其進(jìn)行鏈接的纱控。你所能做的是在 Runtime 運(yùn)行時去通過 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球昨,它周圍有一個特殊的目錄結(jié)構(gòu)來保存該 dylib 所需的文件尔店。

1.1.1 Mach-O 結(jié)構(gòu)分析

1.1.1.1 segment 段

image.png

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

  • 段的名稱為大寫格式<br />
    所有的段都是 page size 的倍數(shù)主慰。
  • arm64 上段大小為 16 字節(jié)
  • 其它架構(gòu)為 4 字節(jié)

這里再普及一下虛擬內(nèi)存內(nèi)存頁的知識:

具有 VM 機(jī)制的操作系統(tǒng)嚣州,會對每個運(yùn)行的進(jìn)程創(chuàng)建一個邏輯地址空間 logical address space 或者叫虛擬地址空間 virtual address space;該空間的大小由操作系統(tǒng)位數(shù)決定:32 位的操作系統(tǒng)河哑,其邏輯地址空間的大小為 4GB避诽,64位的操作系統(tǒng)為 18 exabyes(其計(jì)算方式是 2^32 || 2^64)。

image.png

虛擬地址空間(或者邏輯地址空間)會被分為相同大小的塊璃谨,這些塊被稱為內(nèi)存頁(page)沙庐。計(jì)算機(jī)處理器和它的內(nèi)存管理單元(MMU - memory management uinit)維護(hù)著一張將程序的邏輯地址空間映射到物理地址上的分頁表 page table

masOS 和早版本的 iOS 中佳吞,分頁的大小為 4kB拱雏。在之后的基于 A7A8 的系統(tǒng)中,虛擬內(nèi)存(64 位的地址空間)地址空間的分頁大小變?yōu)榱?16KB底扳,而物理RAM上的內(nèi)存分頁大小仍然維持在 4KB铸抑;基于A9及之后的系統(tǒng),虛擬內(nèi)存和物理內(nèi)存的分頁都是16KB衷模。

1.1.1.2 section

image.png

segment 段內(nèi)部還有許多的 section 區(qū)鹊汛。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 節(jié)實(shí)際上只是一個 segment 段的子范圍阱冶,它們沒有頁面大小的任何限制刁憋,但是它們是不重疊的。?

通過 MachOView 工具查看 app 的二進(jìn)制可執(zhí)行文件可以查看到:

image.png

1.1.1.3 常見的 segments

  • __TEXT:代碼段木蹬,包括頭文件至耻、代碼和常量。只讀不可修改
image.png
  • __DATA:數(shù)據(jù)段,包括全局變量, 靜態(tài)變量等尘颓∽叽ィ可讀可寫。
image.png
  • __LINKEDIT:如何加載程序, 包含了方法和變量的元數(shù)據(jù)(位置疤苹,偏移量)互广,以及代碼簽名等信息。只讀不可修改痰催。
image.png

1.1.2 Mach-O Universal Files

image.png

Mach-O 通用文件兜辞,將多種架構(gòu)的 Mach-O 文件合并而成迎瞧。它通過 header 來記錄不同架構(gòu)在文件中的偏移量夸溶,segement 占多個分頁,header占一頁的空間凶硅》觳茫可能有人會覺得 header 單獨(dú)占一頁會浪費(fèi)空間,但這有利于虛擬內(nèi)存的實(shí)現(xiàn)足绅。

1.2 虛擬內(nèi)存

image.png

虛擬內(nèi)存是一層間接尋址捷绑。

虛擬內(nèi)存解決的是管理所有進(jìn)程使用物理 RAM 的問題。通過添加間接層來讓每個進(jìn)程使用邏輯地址空間氢妈,它可以映射到 RAM 上的某個物理頁上粹污。這種映射不是一對一的,邏輯地址可能映射不到 RAM 上首量,也可能有多個邏輯地址映射到同一個物理 RAM 上壮吩。

  • 針對第一種情況,當(dāng)進(jìn)程要存儲邏輯地址內(nèi)容時會觸發(fā) page fault加缘。
  • 而第二種情況就是多進(jìn)程共享內(nèi)存鸭叙。
  • 對于文件可以不用一次性讀入整個文件,可以使用分頁映射 mmap() 的方式讀取拣宏。也就是把文件某個片段映射到進(jìn)程邏輯內(nèi)存的某個頁上沈贝。當(dāng)某個想要讀取的頁沒有在內(nèi)存中,就會觸發(fā) page fault勋乾,內(nèi)核只會讀入那一頁宋下,實(shí)現(xiàn)文件的懶加載。也就是說 Mach-O 文件中的 __TEXT 段可以映射到多個進(jìn)程辑莫,并可以懶加載学歧,且進(jìn)程之間共享內(nèi)存
  • __DATA 段是可讀寫的摆昧。這里使用到了 Copy-On-Write 技術(shù)撩满,簡稱 COW。也就是多個進(jìn)程共享一頁內(nèi)存空間時,一旦有進(jìn)程要做寫操作伺帘,它會先將這頁內(nèi)存內(nèi)容復(fù)制一份出來昭躺,然后重新映射邏輯地址到新的 RAM 頁上。也就是這個進(jìn)程自己擁有了那頁內(nèi)存的拷貝伪嫁。這就涉及到了 clean/dirty page 的概念领炫。dirty page 含有進(jìn)程自己的信息,而 clean page 可以被內(nèi)核重新生成(重新讀磁盤)张咳。所以 dirty page 的代價大于 clean page帝洪。

1.3 多進(jìn)程加載 Mach-O 鏡像

image.png
  • 所以在多個進(jìn)程加載 Mach-O 鏡像時 __TEXT__LINKEDIT 因?yàn)橹蛔x,都是可以共享內(nèi)存的脚猾,讀取速度就會很快葱峡。
  • __DATA 因?yàn)榭勺x寫,就有可能會產(chǎn)生 dirty page龙助,如果檢測到有 clean page 就可以直接使用砰奕,反之就需要重新讀取 DATA page。一旦產(chǎn)生了 dirty page提鸟,當(dāng) dyld 執(zhí)行結(jié)束后军援,__LINKEDIT 需要通知內(nèi)核當(dāng)前頁面不再需要了,當(dāng)別人需要的使用時候就可以重新 clean 這些頁面称勋。
image.png

1.4 ASLR

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

1.5 Code Signing

可能我們認(rèn)為 Xcode 會把整個文件都做加密 hash 并用做數(shù)字簽名赡鲜。其實(shí)為了在運(yùn)行時驗(yàn)證 Mach-O 文件的簽名空厌,并不是每次重復(fù)讀入整個文件,而是把每頁內(nèi)容都生成一個單獨(dú)的加密散列值蝗蛙,并存儲在 __LINKEDIT 中蝇庭。這使得文件每頁的內(nèi)容都能及時被校驗(yàn)確并保不被篡改。

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() 是一個系統(tǒng)調(diào)用捡硅。系統(tǒng)內(nèi)核把應(yīng)用映射到新的地址空間哮内,且每次起始位置都是隨機(jī)的(因?yàn)槭褂?ASLR)。并將起始位置到 0x000000 這段范圍的進(jìn)程權(quán)限都標(biāo)記為不可讀寫不可執(zhí)行壮韭。如果是 32 位進(jìn)程北发,這個范圍至少是 4KB;對于 64 位進(jìn)程則至少是 4GB 喷屋。NULL 指針引用和指針截?cái)嗾`差都是會被它捕獲琳拨。這個范圍也叫做 PAGEZERO

1.7 dyld

image.png

Unix 的前二十年很安逸屯曹,因?yàn)槟菚r還沒有發(fā)明動態(tài)鏈接庫狱庇。有了動態(tài)鏈接庫后惊畏,一個用于加載鏈接庫的幫助程序被創(chuàng)建。在蘋果的平臺里是 dyld密任,其他 Unix 系統(tǒng)也有 ld.so颜启。 當(dāng)內(nèi)核完成映射進(jìn)程的工作后會將名字為 dyldMach-O 文件映射到進(jìn)程中的隨機(jī)地址,它將 PC 寄存器設(shè)為 dyld 的地址并運(yùn)行浪讳。dyld 在應(yīng)用進(jìn)程中運(yùn)行的工作是加載應(yīng)用依賴的所有動態(tài)鏈接庫缰盏,準(zhǔn)備好運(yùn)行所需的一切,它擁有的權(quán)限跟應(yīng)用一樣淹遵。

1.8 dyld 流程

image.png
  • Load dylibs

從主執(zhí)行文件的 header 獲取到需要加載的所依賴動態(tài)庫列表口猜,而 header 早就被內(nèi)核映射過。然后它需要找到每個 dylib透揣,然后打開文件讀取文件起始位置济炎,確保它是 Mach-O 文件。接著會找到代碼簽名并將其注冊到內(nèi)核淌实。然后在 dylib 文件的每個 segment 上調(diào)用 mmap()冻辩。應(yīng)用所依賴的 dylib 文件可能會再依賴其他 dylib,所以 dyld 所需要加載的是動態(tài)庫列表一個遞歸依賴的集合拆祈。一般應(yīng)用會加載 100400dylib 文件,但大部分都是系統(tǒng) dylib倘感,它們會被預(yù)先計(jì)算和緩存起來放坏,加載速度很快。

  • Fix-ups

在加載所有的動態(tài)鏈接庫之后老玛,它們只是處在相互獨(dú)立的狀態(tài)淤年,需要將它們綁定起來,這就是 Fix-ups蜡豹。代碼簽名使得我們不能修改指令麸粮,那樣就不能讓一個 dylib 的調(diào)用另一個 dylib。這時需要加很多間接層镜廉。
現(xiàn)代 code-gen 被叫做動態(tài) PIC(Position Independent Code)弄诲,意味著代碼可以被加載到間接的地址上。當(dāng)調(diào)用發(fā)生時娇唯,code-gen 實(shí)際上會在 __DATA 段中創(chuàng)建一個指向被調(diào)用者的指針齐遵,然后加載指針并跳轉(zhuǎn)過去。所以 dyld 做的事情就是修正(fix-up)指針和數(shù)據(jù)塔插。Fix-up 有兩種類型梗摇,rebasingbinding

  • Rebasing 和 Binding

Rebasing:在鏡像內(nèi)部調(diào)整指針的指向
Binding:將指針指向鏡像外部的內(nèi)容

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

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

1.9 dyld2 && dyld3

image.png

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

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

dyld3 被分為了三個組件

  • 一個進(jìn)程外的 MachO 解析器
    • 預(yù)先處理了所有可能影響啟動速度的 search path糜烹、@rpaths 和環(huán)境變量
    • 然后分析 Mach-OHeader 和依賴逢倍,并完成了所有符號查找的工作
    • 最后將這些結(jié)果創(chuàng)建成了一個啟動閉包
    • 這是一個普通的 daemon 進(jìn)程,可以使用通常的測試架構(gòu)
  • 一個進(jìn)程內(nèi)的引擎景图,用來運(yùn)行啟動閉包
    • 這部分在進(jìn)程中處理
    • 驗(yàn)證啟動閉包的安全性较雕,然后映射到 dylib 之中,再跳轉(zhuǎn)到 main 函數(shù)
    • 不需要解析 Mach-OHeader 和依賴挚币,也不需要符號查找亮蒋。
  • 一個啟動閉包緩存服務(wù)
    • 系統(tǒng) App 的啟動閉包被構(gòu)建在一個 Shared Cache 中, 我們甚至不需要打開一個單獨(dú)的文件
    • 對于第三方的 App妆毕,我們會在 App 安裝或者升級的時候構(gòu)建這個啟動閉包慎玖。
    • iOStvOS笛粘、watchOS中趁怔,這這一切都是 App 啟動之前完成的。在 macOS 上薪前,由于有 Side Load App润努,進(jìn)程內(nèi)引擎會在首次啟動的時候啟動一個 daemon 進(jìn)程,之后就可以使用啟動閉包啟動了示括。

dyld 3 把很多耗時的查找铺浇、計(jì)算和 I/O 的事前都預(yù)先處理好了,這使得啟動速度有了很大的提升垛膝。

好了鳍侣,先導(dǎo)知識就總結(jié)到這里,接下來讓我們調(diào)整呼吸進(jìn)入下一章~

二吼拥、App 加載分析

我們在探索 iOS 底層的時候倚聚,對于對象、類凿可、方法有了一定的認(rèn)知哦惑折,接下來我們就一起來探索一下應(yīng)用是怎么加載的。

我們直接新建一個 Single View App 的項(xiàng)目矿酵,然后在 main.m 中打一個斷點(diǎn):

image.png

然后我們可以看到在 main 方法執(zhí)行前有一步 start唬复,而這一流程是由 libdyld.dylib 這個動態(tài)庫來執(zhí)行的。

image.png

這個現(xiàn)象說明了什么呢全肮?說明我們的 appmain 函數(shù)執(zhí)行之前其實(shí)還通過 dyld 做了很多事情搁嗓。那為了搞清楚具體的流程仓洼,我們不妨從 Apple OpenSource 上下載 dyld 的源碼來進(jìn)行探索。

我們選擇最新的 655.1.1 版本:

image.png

三绩脆、dyld 源碼分析

面對 dyld 的源碼,我們不可能一行一行的去分析。我們不妨在剛才創(chuàng)建的項(xiàng)目中斷點(diǎn)一下 load 方法,看下調(diào)用堆棧:

image.png

這一次我們發(fā)現(xiàn),load 方法的調(diào)用要早于 main 函數(shù)的調(diào)用茵烈,其次,我們得到了一個非常有價值的線索: _dyld_start砌些。

3.1 _dyld_start

我們直接在 dyld 655.1.1 中全局搜索這個 _dyld_start呜投,我們可以來到 dyldStartup.s 這個匯編文件,然后我們聚焦于 arm64 架構(gòu)下的匯編代碼:

image.png

對于這里的匯編代碼存璃,我們肯定也沒必要逐行分析仑荐,我們直接定位到 bl 語句后面(bl 在匯編層面是跳轉(zhuǎn)的意思):

bl  __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm

我們可以看到這里有一行注釋:

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

這行注釋的意思是調(diào)用位于 dyldbootstrap 命名空間下的 start 方法,我們繼續(xù)搜索一下這個 start 方法纵东,結(jié)果位于 dyldInitialization.cpp 文件(從文件名我們可以看出該文件主要是用來初始化 dyld)粘招,這里查找 start 的時候可能會有很多結(jié)果,我們其實(shí)可以先搜索命名空間偎球,再搜索 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
  • 根據(jù) dyldMach-O 文件的 header 判斷是否需要對 dyld 這個 Mach-O 進(jìn)行 rebase 操作
image.png
  • 初始化 mach衰絮,使得 dyld 可以進(jìn)行 mach 通訊袍冷。
image.png
  • 內(nèi)核將 env 指針設(shè)置為剛好超出 agv 數(shù)組的末尾;內(nèi)核將 apple 指針設(shè)置為剛好超出 envp 數(shù)組的末尾
image.png
  • 棧溢出保護(hù)
image.png
  • 讀取 app 主二進(jìn)制文件 Mach-Oheader 來得到偏移量 appSlide岂傲,然后調(diào)用 dyld 命名空間下的 _main 方法难裆。

3.3 dyldbootstrap::_main

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

image.png

_main方法 官方的注釋如下:

dyld 的入口。內(nèi)核加載了 dyld 然后跳轉(zhuǎn)到 __dyld_start 來設(shè)置一些寄存器的值然后調(diào)用到了這個方法镊掖。
返回 __dyld_start 所跳轉(zhuǎn)到的目標(biāo)程序的 main 函數(shù)地址。

我們乍一看褂痰,這個方法有四五百行亩进,所以我們不能老老實(shí)實(shí)的一行一行來看,這樣太累了缩歪。我們應(yīng)該著重于有注釋的地方归薛。

image.png
  • 我們首先可以看到這里是從環(huán)境變量中獲取主要可執(zhí)行文件的 cdHash 值。這個哈希值 mainExecutableCDHash 在后面用來校驗(yàn) dyld3 的啟動閉包匪蝙。
image.png
  • 上圖代碼作用是追蹤 dyld 的加載主籍。然后判斷當(dāng)前是否為模擬器環(huán)境,如果不是模擬器逛球,則追蹤主二進(jìn)制可執(zhí)行文件的加載千元。
image.png
  • 顯示宏定義判斷是否為 macOS 執(zhí)行環(huán)境,如果是則判斷 DYLD_ROOT_PATH 環(huán)境變量是否存在颤绕,如果存在幸海,然后判斷模擬器是否有自己的 dyld祟身,如果有就使用,如果沒有物独,則返回錯誤信息袜硫。
image.png
  • 打印日志:dyld 啟動開始
  • 根據(jù)傳入 dyldbootstrap::_main 方法的參數(shù)來設(shè)置上下文
  • 拾取指向 exec 路徑的指針
  • dyl d移除臨時 apple [0] 過渡代碼
  • 判斷 exec 路徑是否為絕對路徑,如果為相對路徑挡篓,使用 cwd 轉(zhuǎn)化為絕對路徑
  • 為了后續(xù)的日志打印從 exec 路徑中取出進(jìn)程的名稱 (strrchr 函數(shù)是獲取第二個參數(shù)出現(xiàn)的最后的一個位置婉陷,然后返回從這個位置開始到結(jié)束的內(nèi)容)
  • 根據(jù) App 主二進(jìn)制可執(zhí)行文件 Mach-OHeader 的內(nèi)容配置進(jìn)程的一些限制條件
image.png
  • 判斷是否為 macOS 執(zhí)行環(huán)境,如果是的話官研,再判斷上下文的一些配置屬性是否被設(shè)置了秽澳,如果沒有被設(shè)置,則再次進(jìn)行一次 setContext 上下文配置操作阀参。
  • 根據(jù)傳入的參數(shù) envp 檢查環(huán)境變量
  • 默認(rèn)未初始化的后備路徑
  • 判斷是否為 macOS 執(zhí)行環(huán)境肝集,如果是的話,再判斷當(dāng)前 appMach-O 可執(zhí)行文件是否為 iOSMac 類型且不為 macOS 類型的話蛛壳,則重置上下文的根路徑杏瞻,然后再判斷 DYLD_FALLBACK_LIBRARY_PATHDYLD_FALLBACK_FRAMEWORK_PATH 這兩個環(huán)境變量是否都是默認(rèn)后備路徑,如果是的話賦值為受限的后備路徑衙荐。<br />
image.png
  • 根據(jù)環(huán)境變量 DYLD_PRINT_OPTSDYLD_PRINT_ENV 來判斷是否需要打印
  • 通過當(dāng)前 appMach-O 可執(zhí)行文件的 headerASLR 之后的偏移量來獲取架構(gòu)信息捞挥。在這里會判斷如果是 GC 的程序則會禁用掉共享緩存。<br />
image.png
image.png
  • 判斷共享緩存是否開啟忧吟,如果開啟了就將共享緩存映射到當(dāng)前進(jìn)程的邏輯內(nèi)存空間內(nèi)
image.png
  • 檢查共享緩存這里會先判斷 appMach-O 二進(jìn)制可執(zhí)行文件是否有段覆蓋了共享緩存區(qū)域砌函,如果覆蓋了則禁用共享緩存。但是這里的前提是 macOS溜族,在 iOS 中讹俊,共享緩存是必需的。
image.png

這里為了方便查看煌抒,我們可以折疊一些分支條件仍劈。

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

3.4 dyld3 的處理

image.png
  • 由于 dyld3 會創(chuàng)建一個啟動閉包,我們需要來讀取它寡壮,這里會現(xiàn)在緩存中查找是否有啟動閉包的存在贩疙,前面我們已經(jīng)說過了,系統(tǒng)級的 app 的啟動閉包是存在于共享緩存中况既,而我們自己開發(fā)的 app 的啟動閉包是在 app 安裝或者升級的時候構(gòu)建的这溅,所以這里檢查 dyld 中的緩存是有意義的。
image.png
  • 宏定義判斷代碼執(zhí)行條件為真機(jī)棒仍。
  • 如果 dyld 緩存中沒有找到啟動閉包或者找到了啟動閉包但是驗(yàn)證失敱ァ(我們最開始提到的 cdHash 在這里出現(xiàn)了)
    • 從啟動閉包緩存中查找
      • 如果還是沒有找到,那就創(chuàng)建一個新的啟動閉包
image.png
  • 打印日志信息:dyld3 啟動開始
  • 嘗試通過啟動閉包進(jìn)行啟動
    • 如果啟動失敗降狠,則創(chuàng)建一個新的啟動閉包嘗試再次啟動
    • 如果啟動成功对竣,由于 start() 是以函數(shù)指針的方式調(diào)用 _main 方法的返回的指針庇楞,需要進(jìn)行簽名。

至此否纬,dyld3 的流程就處理完畢吕晌,我們再接著往下分析 dyld2 的流程。

3.5 dyld2 的處理

image.png
  • 這里會添加 dyld 的鏡像文件到 UUID 列表中临燃,主要的目的是啟用堆棧的符號化睛驳。<br />
image.png

reloadAllImages

ImageLoader 是一個用于加載可執(zhí)行文件的基類,它負(fù)責(zé)鏈接鏡像膜廊,但不關(guān)心具體文件格式乏沸,因?yàn)檫@些都交給子類去實(shí)現(xiàn)。每個可執(zhí)行文件都會對應(yīng)一個 ImageLoader實(shí)例爪瓜。ImageLoaderMachO 是用于加載 Mach-O 格式文件的 ImageLoader 子類蹬跃,而 ImageLoaderMachOClassicImageLoaderMachOCompressed 都繼承于 ImageLoaderMachO,分別用于加載那些 __LINKEDIT 段為傳統(tǒng)格式和壓縮格式的 Mach-O 文件铆铆。

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

image.png

實(shí)例化主程序

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

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

顯然蝶缀,在這里我們的主程序被實(shí)例化了,我們進(jìn)入這個方法內(nèi)部:

image.png

這里相當(dāng)于要為已經(jīng)映射到主可執(zhí)行文件中的文件創(chuàng)建一個 ImageLoader*薄货。

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

image.png

我們再進(jìn)入 sniiffLoadCommands 方法內(nèi)部:

image.png

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

sniiffLoadCommands 完成后,判斷 LINKEDIT 是壓縮的格式還是傳統(tǒng)格式谅猾,然后分別調(diào)用對應(yīng)的 instantiateMainExecutable 方法來實(shí)例化主程序柄慰。


加載任何插入的動態(tài)庫<br />

image.png


鏈接庫

image.png

先是鏈接主二進(jìn)制可執(zhí)行文件,然后鏈接任何插入的動態(tài)庫税娜。這里都用到了 link 方法坐搔,在這個方法內(nèi)部會執(zhí)行遞歸的 rebase 操作來修正 ASLR 偏移量問題。同時還會有一個 recursiveApplyInterposing 方法來遞歸的將動態(tài)加載的鏡像文件插入敬矩。


運(yùn)行所有初始化程序

image.png

完成鏈接之后需要進(jìn)行初始化了薯蝎,這里會來到 initializeMainExecutable:

image.png

這里注意執(zhí)行順序:

  • 先為所有插入并鏈接完成的動態(tài)庫執(zhí)行初始化操作
  • 然后再為主程序可執(zhí)行文件執(zhí)行初始化操作
image.png

runInitializers 內(nèi)部我們繼續(xù)探索到 processInitializers:

image.png

然后我們來到 recursiveInitialization:

image.png

然后我們來到 notifySingle:

image.png

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

我們?nèi)炙阉饕幌?sNotifyObjcInit 可以來到 registerObjCNotifiers

image.png

接著搜索 registerObjCNotifiers

image.png

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

image.png

上面這一連串的跳轉(zhuǎn),結(jié)果很顯然:dyld 注冊了回調(diào)才使得 libobjc 能知道鏡像何時加載完畢袒哥。

image.png

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

image.png

doInitialization 主要有兩個操作堡称,一個是 doImageInit瞎抛,一個是 doModInitFunctions:

image.png

doImageInit 內(nèi)部會通過初始地址 + 偏移量拿到初始化器 func,然后進(jìn)行簽名的驗(yàn)證却紧。驗(yàn)證通過后還要判斷初始化器是否在鏡像文件中以及 libSystem 庫是否已經(jīng)初始化桐臊,最后才執(zhí)行初始化器胎撤。


通知監(jiān)聽 dyld 的 main

image.png

一切工作做完后通知監(jiān)聽 dyldmain,然后為主二進(jìn)制可執(zhí)行文件找到入口断凶,最后對結(jié)果進(jìn)行簽名伤提。

四、探索 _objc_init

image.png

我們直接通過 LLDB 大法來斷點(diǎn)調(diào)試 libObjc 中的 _objc_init认烁,然后通過 bt 命令打印出當(dāng)前的調(diào)用堆棧肿男,根據(jù)上一節(jié)我們探索 dyld 的源碼,此刻一切的一切都是那么的清晰明了:

image.png

我們可以看到 dyld 的最后一個流程是 doModInitFunctions 方法的執(zhí)行却嗡。

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

image.png

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

image.png

我們再搜索 _os_object_init:

image.png

完美~窗价,_objc_init 在這里就被調(diào)用了如庭。所以 _objc_init 的流程是

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

五、總結(jié)

本文主要探索了 app 啟動之后 dyld 的流程撼港,整個分析過程確實(shí)比較復(fù)雜坪它,但在探索的過程中,我們不僅對底層源碼有了新的認(rèn)知餐胀,同時對于優(yōu)化我們 app 啟動也是有很多好處的哟楷。下一章,我們會對 objc_init 內(nèi)部的 map_imagesload_images 進(jìn)行更深入的分析否灾,敬請期待~

六卖擅、參考資料

Optimizing App Startup Time

優(yōu)化 App 啟動

iOS 開發(fā)中的『庫』(一)

iOS應(yīng)用的內(nèi)存管理(二)

優(yōu)化 App 的啟動時間

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市墨技,隨后出現(xiàn)的幾起案子惩阶,更是在濱河造成了極大的恐慌,老刑警劉巖扣汪,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件断楷,死亡現(xiàn)場離奇詭異,居然都是意外死亡崭别,警方通過查閱死者的電腦和手機(jī)冬筒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茅主,“玉大人舞痰,你說我怎么就攤上這事【饕Γ” “怎么了响牛?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我呀打,道長矢赁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任贬丛,我火速辦了婚禮撩银,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瘫寝。我一直安慰自己蜒蕾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布焕阿。 她就那樣靜靜地躺著咪啡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪暮屡。 梳的紋絲不亂的頭發(fā)上撤摸,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音褒纲,去河邊找鬼准夷。 笑死莺掠,一個胖子當(dāng)著我的面吹牛衫嵌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼留夜,長吁一口氣:“原來是場噩夢啊……” “哼慰枕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起即纲,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎博肋,沒想到半個月后低斋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜂厅,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年膊畴,在試婚紗的時候發(fā)現(xiàn)自己被綠了掘猿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡唇跨,死狀恐怖稠通,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情买猖,我是刑警寧澤改橘,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站玉控,受9級特大地震影響飞主,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜高诺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一碌识、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虱而,春花似錦筏餐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诅迷,卻和暖如春佩番,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背罢杉。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工趟畏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人滩租。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓赋秀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親律想。 傳聞我的和親對象是個殘疾皇子猎莲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容