虛擬內(nèi)存 & ASLR
在早期計(jì)算機(jī)中數(shù)據(jù)是直接通過(guò)物理地址訪問(wèn)的,這就造成了下面兩個(gè)問(wèn)題
1.內(nèi)存不夠用
2.數(shù)據(jù)安全問(wèn)題
內(nèi)存不夠 ---> 虛擬內(nèi)存
虛擬內(nèi)存就是通過(guò)創(chuàng)建一張物理地址和虛擬地址的映射表來(lái)管理內(nèi)存,提高了CPU利用率,使多個(gè)進(jìn)程可以同時(shí)/按需加載。
在iOS中,每個(gè)進(jìn)程都有獨(dú)立的
虛擬內(nèi)存,存放物理內(nèi)存中,其地址是從0開(kāi)始的,大小固定4G,每個(gè)虛擬內(nèi)存又會(huì)按頁(yè)劃分,每頁(yè)16K,以頁(yè)為單位加載,每個(gè)進(jìn)程是相互獨(dú)立的,保證進(jìn)程間的數(shù)據(jù)安全當(dāng)一個(gè)進(jìn)程只有部分功能使用時(shí),系統(tǒng)會(huì)自動(dòng)將使用部分加載到物理內(nèi)存中
CPU在進(jìn)行數(shù)據(jù)訪問(wèn)時(shí),會(huì)先訪問(wèn)虛擬內(nèi)存,通過(guò)虛擬內(nèi)存和物理內(nèi)存的映射關(guān)系,去對(duì)應(yīng)的物理內(nèi)存中查找,在CPU上有專(zhuān)門(mén)處理映射的硬件
當(dāng)CPU訪問(wèn)數(shù)據(jù)時(shí),如果虛擬內(nèi)存中的數(shù)據(jù)沒(méi)有物理內(nèi)存綁定,會(huì)發(fā)生
缺頁(yè)異常(pagefault),會(huì)阻塞當(dāng)前進(jìn)程,直到數(shù)據(jù)加載到物理內(nèi)存中,虛擬內(nèi)存和物理內(nèi)存綁定-
當(dāng)內(nèi)存條中的內(nèi)存全部用完后,系統(tǒng)會(huì)將新的數(shù)據(jù)
覆蓋到很久沒(méi)使用的內(nèi)存上
數(shù)據(jù)安全 ---> ASLR
因?yàn)樘摂M內(nèi)存的起始地址(0x000000)和大小(4G)是固定的,這就意味著我們的數(shù)據(jù)地址也是固定的,所以在iOS4.3引進(jìn)了ASLR。
ASLR:英文全稱(chēng)Address Space Layout Randomization,也叫地址空間配置隨機(jī)加載,是一種針對(duì)緩沖區(qū)溢出的安全保護(hù)技術(shù)。通過(guò)對(duì)堆、棧、共享庫(kù)映射等線(xiàn)性區(qū)布局的隨機(jī)化,增強(qiáng)了攻擊者找到目標(biāo)物理內(nèi)存的難度,防止攻擊者直接定位攻擊代碼位置,達(dá)到阻止溢出攻擊的目的的一種技術(shù)。
由于ASLR的存在,導(dǎo)致可執(zhí)行文件和動(dòng)態(tài)鏈接庫(kù)在虛擬內(nèi)存中的地址每次都是不固定的,所以需要在編譯時(shí)來(lái)修復(fù)鏡像中的資源指針,來(lái)指向正確的地址。即正確的內(nèi)存地址 = ASLR地址 + 偏移值
iOS 系統(tǒng)架構(gòu)
Mac系統(tǒng)是基于Unix內(nèi)核的圖形化操作系統(tǒng)。Mac OS 和 iOS 系統(tǒng)架構(gòu)的對(duì)比分析發(fā)現(xiàn),Mac OS和iOS的系統(tǒng)架構(gòu)層次只有最上面一層不同,Mac是Cocoa框架,而iOS是Cocoa Touch框架,其余的架構(gòu)層次都是一樣的。

Core OS是用FreeBSD和Mach所改寫(xiě)的一個(gè)名叫Darwin的開(kāi)放原始碼操作系統(tǒng), 是開(kāi)源、符合POSIX標(biāo)準(zhǔn)的一個(gè)Unix核心。這一層包含并提供了整個(gè)iPhone OS的一些基礎(chǔ)功能,比如:硬件驅(qū)動(dòng), 內(nèi)存管理,程序管理,線(xiàn)程管理(POSIX),文件系統(tǒng),網(wǎng)絡(luò)(BSD Socket),以及標(biāo)準(zhǔn)輸入輸出等,所有這些功能都會(huì)通過(guò)C語(yǔ)言的API來(lái)提供。

核心OS層的驅(qū)動(dòng)提供了硬件和系統(tǒng)框架之間的接口。然而,由于安全的考慮,只有有限的系統(tǒng)框架類(lèi)能訪問(wèn)內(nèi)核和驅(qū)動(dòng)。iPhone OS提供了許多訪問(wèn)操作系統(tǒng)低層功能的接口集,iPhone 應(yīng)用通過(guò)LibSystem庫(kù)來(lái)訪問(wèn)這些功能,這些接口集有線(xiàn)程(POSIX線(xiàn)程)、網(wǎng)絡(luò)(BSD sockets)、文件系統(tǒng)訪問(wèn)、標(biāo)準(zhǔn)I/O、Bonjour和DNS服務(wù)、現(xiàn)場(chǎng)信息(Locale Information)、內(nèi)存分配和數(shù)學(xué)計(jì)算等。
Core Services在Core OS基礎(chǔ)上提供了更為豐富的功能, 它包含了Foundation.Framework和Core Foundation.Framework。之所以叫Foundation,就是因?yàn)樗峁┝艘幌盗刑幚碜址?,排列、組合、日歷、時(shí)間等等的基本功能。
Foundation是屬于Objective-C的API,Core Fundation是屬于C的API。另外Core servieces還提供了如Security(用來(lái)處理認(rèn)證,密碼管理,安全性管理等), Core Location, SQLite和Address Book等功能。
核心基礎(chǔ)框架(CoreFoundation.framework)是基于C語(yǔ)言的接口集,提供iPhone應(yīng)用的基本數(shù)據(jù)管理和服務(wù)功能。該框架支持Collection數(shù)據(jù)類(lèi)型(Arrays、 Sets等)、Bundles、字符串管理、日期和時(shí)間管理、原始數(shù)據(jù)塊管理、首選項(xiàng)管理、URL和Stream操作、線(xiàn)程和運(yùn)行循環(huán)(Run Loops)、端口和Socket通信。
核心基礎(chǔ)框架與基礎(chǔ)框架是緊密相關(guān)的,它們?yōu)橄嗤幕竟δ芴峁┝薕bjective-C接口。如果開(kāi)發(fā)者混合使用Foundation Objects 和Core Foundation類(lèi)型,就能充分利用存在兩個(gè)框架中的"toll-free bridging"技術(shù)(橋接)。toll-free bridging使開(kāi)發(fā)者能使用這兩個(gè)框架中的任何一個(gè)的核心基礎(chǔ)和基礎(chǔ)類(lèi)型。
程序啟動(dòng)之前
從應(yīng)用圖標(biāo)被用戶(hù)點(diǎn)擊開(kāi)始,直到應(yīng)用可以開(kāi)始響應(yīng)發(fā)生了很多事情。

很多文章中大家都提到說(shuō)dyld加載了主程序和動(dòng)態(tài)庫(kù),這個(gè)理解明顯是錯(cuò)誤的,在XNU加載Mach-O和dyld過(guò)程中,是內(nèi)核加載了主程序,dyld只會(huì)負(fù)責(zé)動(dòng)態(tài)庫(kù)的加載。雖然主程序也會(huì)作為鏡像形式被dyld來(lái)管理起來(lái)。當(dāng)一個(gè)App啟動(dòng)時(shí),dyld把App需要的dylib加載進(jìn)App的內(nèi)存空間。App運(yùn)行所需要的信息,一般都存放在其MachO頭部44中,其中dylib的信息是由Load Commands指定的,App得到執(zhí)行時(shí),dyld會(huì)查看其MachO頭部中的Load Commands,并把里面LC_LOAD_DYLIB相關(guān)的dylib給加載到進(jìn)程的內(nèi)存空間。
一般來(lái)說(shuō),逆向工程會(huì)在dyld階段入手,之前版本的dyld中確實(shí)存在一些漏洞,使App能夠繞過(guò)代碼簽名,例如dyld-353.2.1版本,漏洞編號(hào)CVE-2015-5876,漏洞存在于Mach-O頭的處理過(guò)程中,一個(gè)畸形的Mach-O文件可以導(dǎo)致內(nèi)存段被替換,從而導(dǎo)致任意代碼執(zhí)行,當(dāng)然目前dyld的已知漏洞都已修復(fù)。監(jiān)控啟動(dòng)崩潰會(huì)在Objc階段之后,具體方法具體分析。
總結(jié)來(lái)說(shuō),大體分為如下步驟:
(1) 系統(tǒng)為程序啟動(dòng)做好準(zhǔn)備。
(2) 系統(tǒng)將控制權(quán)交給 Dyld,Dyld 會(huì)負(fù)責(zé)后續(xù)的工作。
(3) Dyld 加載程序所需的動(dòng)態(tài)庫(kù)。
(3) Dyld 對(duì)程序進(jìn)行 rebase 以及 bind 操作。
(4) Objc SetUp。
(5) 運(yùn)行初始化函數(shù)。
(6) 執(zhí)行程序的 main 函數(shù)。
需要注意的是,dyld2和dyld3的加載方式略有不同。dyld2是純粹的in-process,也就是在程序進(jìn)程內(nèi)執(zhí)行的,也就意味著只有當(dāng)應(yīng)用程序被啟動(dòng)的時(shí)候,dyld2才能開(kāi)始執(zhí)行任務(wù)。dyld3則是部分out-of-process,部分in-process。
dyld2的過(guò)程是:加載dyld到App進(jìn)程,加載動(dòng)態(tài)庫(kù)(包括所依賴(lài)的所有動(dòng)態(tài)庫(kù)),Rebase,Bind,初始化Objective C Runtime和其它的初始化代碼。
dyld3的out-of-process會(huì)做如下事情:分析Mach-O Headers,分析依賴(lài)的動(dòng)態(tài)庫(kù),查找需要Rebase & Bind之類(lèi)的符號(hào),把上述結(jié)果寫(xiě)入緩存。這樣,在應(yīng)用啟動(dòng)的時(shí)候,就可以直接從緩存中讀取數(shù)據(jù),加快加載速度。
從exec()開(kāi)始
Mach-O是 OS X 系統(tǒng)的可執(zhí)行文件,Mach-O有多種文件類(lèi)型,比如MH_DYLIB文件、MH_BUNDLE文件、MH_EXECUTE文件,MH_OBJECT(內(nèi)核加載)等??蓤?zhí)行文件離不開(kāi)進(jìn)程,在 Linux 中,我們會(huì)通過(guò) Fork()來(lái)新創(chuàng)建子進(jìn)程,然后執(zhí)行鏡像通過(guò)exec()來(lái)替換為另一個(gè)可執(zhí)行程序。在用戶(hù)態(tài)會(huì)通過(guò)exec*系列函數(shù)來(lái)加載一個(gè)可執(zhí)行文件。

main()函數(shù)是整個(gè)程序的入口,在程序啟動(dòng)之前,系統(tǒng)會(huì)調(diào)用exec()函數(shù)。在Unix中exec和system的不同在于,system是用shell來(lái)調(diào)用程序,相當(dāng)于fork+exec+waitpid,fork 函數(shù)創(chuàng)建子進(jìn)程后通常都會(huì)調(diào)用 exec 函數(shù)來(lái)執(zhí)行一個(gè)新程序;而exec是直接讓你的程序代替原來(lái)的程序運(yùn)行。
system 是在單獨(dú)的進(jìn)程中執(zhí)行命令,完了還會(huì)回到你的程序中。而exec函數(shù)是直接在你的進(jìn)程中執(zhí)行新的程序,新的程序會(huì)把你的程序覆蓋,除非調(diào)用出錯(cuò),否則你再也回不到exec后面的代碼,也就是當(dāng)前的程序變成了exec調(diào)用的那個(gè)程序了。
UNIX 提供了 6 種不同的 exec 函數(shù)供我們使用。
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);
int execvp(cosnt char *filename, char *const argv[]);
通過(guò)分析我們發(fā)現(xiàn),含有 l 和 v 的 exec 函數(shù)的參數(shù)表傳遞方式是不同的。含有 e 結(jié)尾的 exec 函數(shù)會(huì)傳遞一個(gè)環(huán)境變量列表。含有 p 結(jié)尾的 exec 函數(shù)取的是新程序的文件名作為參數(shù),而其他exec 函數(shù)取的是新程序的路徑。
如果函數(shù)出錯(cuò)則返回-1,若成功則沒(méi)有返回值。其中只有execve是真正意義上的系統(tǒng)調(diào)用,其它都是在此基礎(chǔ)上經(jīng)過(guò)包裝的庫(kù)函數(shù)。
exec函數(shù)族的作用是根據(jù)指定的文件名找到可執(zhí)行文件,并用它來(lái)取代調(diào)用進(jìn)程的內(nèi)容,換句話(huà)說(shuō),就是在調(diào)用進(jìn)程內(nèi)部執(zhí)行一個(gè)可執(zhí)行文件。這里的可執(zhí)行文件既可以是二進(jìn)制文件,也可以是任何Unix下可執(zhí)行的腳本文件。
Dyld
Dyld 是 iOS 系統(tǒng)的動(dòng)態(tài)鏈接器, 在dyldStartup.s 文件中有個(gè)名為 __dyld_start 的方法,它會(huì)去調(diào)用 dyldbootstrap::start() 方法,然后進(jìn)一步調(diào)用 dyld::_main() 方法,里面包含 App 的整個(gè)啟動(dòng)流程,該函數(shù)最終返回應(yīng)用程序 main 函數(shù)的地址,最后 Dyld 會(huì)去調(diào)用它。
之后會(huì)去加載可執(zhí)行文件,二進(jìn)制文件常被稱(chēng)為 image,包括可執(zhí)行文件、動(dòng)態(tài)庫(kù)等,ImageLoader 的作用就是將二進(jìn)制文件加載進(jìn)內(nèi)存。dyld::_main() 方法在設(shè)置好運(yùn)行環(huán)境后,會(huì)調(diào)用instantiateFromLoadedImage 函數(shù)將可執(zhí)行文件加載進(jìn)內(nèi)存中,加載過(guò)程分為三步:
1.合法性檢查。主要是檢查可執(zhí)行文件是否合法,是否能在當(dāng)前的 CPU 架構(gòu)下運(yùn)行。
2.選擇 ImageLoader 加載可執(zhí)行文件。系統(tǒng)會(huì)去判斷可執(zhí)行文件的類(lèi)型,選擇相應(yīng)的 ImageLoader 將其加載進(jìn)內(nèi)存空間中。
3.注冊(cè) image 信息??蓤?zhí)行文件加載完成后,系統(tǒng)會(huì)調(diào)用 addImage 函數(shù)將其管理起來(lái),并更新內(nèi)存分布信息。
以上三步完成后,Dyld 會(huì)調(diào)用 link 函數(shù)開(kāi)始之后的處理流程。
靜態(tài)鏈接庫(kù)與動(dòng)態(tài)鏈接庫(kù)
iOS中的相關(guān)文件有如下幾種:Dylib,動(dòng)態(tài)鏈接庫(kù)(又稱(chēng) DSO 或 DLL);Bundle,不能被鏈接的 Dylib,只能在運(yùn)行時(shí)使用 dlopen() 加載,可當(dāng)做 macOS 的插件。Framework,包含 Dylib 以及資源文件和頭文件的文件夾。
動(dòng)態(tài)鏈接庫(kù)是一組源代碼的模塊,每個(gè)模塊包含一些可供應(yīng)用程序或者其他動(dòng)態(tài)鏈接庫(kù)調(diào)用的函數(shù),在應(yīng)用程序調(diào)用一個(gè)動(dòng)態(tài)鏈接庫(kù)里面的函數(shù)的時(shí)候,操作系統(tǒng)會(huì)將動(dòng)態(tài)鏈接庫(kù)的文件映像映射到進(jìn)程的地址空間中,這樣進(jìn)程中所有的線(xiàn)程就可以調(diào)用動(dòng)態(tài)鏈接庫(kù)中的函數(shù)了。動(dòng)態(tài)鏈接庫(kù)加載完成后,這個(gè)時(shí)候動(dòng)態(tài)鏈接庫(kù)對(duì)于進(jìn)程中的線(xiàn)程來(lái)說(shuō)只是一些被放在地址進(jìn)程空間附加的代碼和數(shù)據(jù),操作系統(tǒng)為了節(jié)省內(nèi)存空間,同一個(gè)動(dòng)態(tài)鏈接庫(kù)在內(nèi)存中只有一個(gè),操作系統(tǒng)也只會(huì)加載一次到內(nèi)存中。
因?yàn)榇a段在內(nèi)存中的權(quán)限都是為只讀的,所以當(dāng)多個(gè)應(yīng)用程序加載同一個(gè)動(dòng)態(tài)鏈接庫(kù)的時(shí)候,不用擔(dān)心應(yīng)用程序會(huì)修改動(dòng)態(tài)鏈接庫(kù)的代碼段。當(dāng)線(xiàn)程調(diào)用動(dòng)態(tài)鏈接庫(kù)的一個(gè)函數(shù),函數(shù)會(huì)在線(xiàn)程棧中取得傳遞給他的參數(shù),并使用線(xiàn)程棧來(lái)存放他需要的變量,動(dòng)態(tài)鏈接庫(kù)函數(shù)創(chuàng)建的任何對(duì)象都為調(diào)用線(xiàn)程或者調(diào)用進(jìn)程擁有,動(dòng)態(tài)鏈接庫(kù)不會(huì)擁有任何對(duì)象。如果動(dòng)態(tài)鏈接庫(kù)中的一個(gè)函數(shù)調(diào)用了VirtualAlloc,系統(tǒng)會(huì)從調(diào)用進(jìn)程的地址空間預(yù)定地址,即使撤銷(xiāo)了對(duì)動(dòng)態(tài)鏈接庫(kù)的映射,調(diào)用進(jìn)程的預(yù)定地址依然會(huì)存在,直到用戶(hù)取消預(yù)定或者進(jìn)程結(jié)束。
靜態(tài)鏈接庫(kù)與動(dòng)態(tài)鏈接庫(kù)都是共享代碼的方式,如果采用靜態(tài)鏈接庫(kù),則無(wú)論你愿不愿意,lib 中的指令都全部被直接包含在最終生成的包文件中了。但是若使用動(dòng)態(tài)鏈接庫(kù),該動(dòng)態(tài)鏈接庫(kù)不必被包含在最終包里,包文件執(zhí)行時(shí)可以“動(dòng)態(tài)”地引用和卸載這個(gè)與安裝包獨(dú)立的動(dòng)態(tài)鏈接庫(kù)文件。靜態(tài)鏈接庫(kù)和動(dòng)態(tài)鏈接庫(kù)的另外一個(gè)區(qū)別在于靜態(tài)鏈接庫(kù)中不能再包含其他的動(dòng)態(tài)鏈接庫(kù)或者靜態(tài)庫(kù),而在動(dòng)態(tài)鏈接庫(kù)中還可以再包含其他的動(dòng)態(tài)或靜態(tài)鏈接庫(kù)。
Linux中靜態(tài)函數(shù)庫(kù)的名字一般是libxxx.a,利用靜態(tài)函數(shù)庫(kù)編譯成的文件比較大,因?yàn)檎麄€(gè)函數(shù)庫(kù)的所有數(shù)據(jù)都會(huì)被整合進(jìn)目標(biāo)代碼中。編譯后的執(zhí)行程序不需要外部的函數(shù)庫(kù)支持,因?yàn)樗惺褂玫暮瘮?shù)都已經(jīng)被編譯進(jìn)去了。當(dāng)然這也會(huì)成為他的缺點(diǎn),因?yàn)槿绻o態(tài)函數(shù)庫(kù)改變了,那么你的程序必須重新編譯。
動(dòng)態(tài)函數(shù)庫(kù)的名字一般是libxxx.so,相對(duì)于靜態(tài)函數(shù)庫(kù),動(dòng)態(tài)函數(shù)庫(kù)在編譯的時(shí)候并沒(méi)有被編譯進(jìn)目標(biāo)代碼中,你的程序執(zhí)行到相關(guān)函數(shù)時(shí)才調(diào)用該函數(shù)庫(kù)里的相應(yīng)函數(shù),因此動(dòng)態(tài)函數(shù)庫(kù)所產(chǎn)生的可執(zhí)行文件比較小。由于函數(shù)庫(kù)沒(méi)有被整合進(jìn)你的程序,而是程序運(yùn)行時(shí)動(dòng)態(tài)的申請(qǐng)并調(diào)用,所以程序的運(yùn)行環(huán)境中必須提供相應(yīng)的庫(kù)。動(dòng)態(tài)函數(shù)庫(kù)的改變并不影響你的程序,所以動(dòng)態(tài)函數(shù)庫(kù)的升級(jí)比較方便。
iOS開(kāi)發(fā)中靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)是相對(duì)編譯期和運(yùn)行期的。靜態(tài)庫(kù)在程序編譯時(shí)會(huì)被鏈接到目標(biāo)代碼中,程序運(yùn)行時(shí)將不再需要載入靜態(tài)庫(kù)。而動(dòng)態(tài)庫(kù)在程序編譯時(shí)并不會(huì)被鏈接到目標(biāo)代碼中,只是在程序運(yùn)行時(shí)才被載入,因?yàn)樵诔绦蜻\(yùn)行期間還需要?jiǎng)討B(tài)庫(kù)的存在。
iOS中靜態(tài)庫(kù)可以用.a或.Framework文件表示,動(dòng)態(tài)庫(kù)的形式有.dylib和.framework。系統(tǒng)的.framework是動(dòng)態(tài)庫(kù),一般自己建立的.framework是靜態(tài)庫(kù)。
.a是一個(gè)純二進(jìn)制文件,.framework中除了有二進(jìn)制文件之外還有資源文件。.a文件不能直接使用,至少要有.h文件配合。.framework文件可以直接使用,.a + .h + sourceFile = .framework。
動(dòng)態(tài)庫(kù)的一個(gè)重要特性就是即插即用性,我們可以選擇在需要的時(shí)候再加載動(dòng)態(tài)庫(kù)。如果不希望在軟件一啟動(dòng)就加載動(dòng)態(tài)庫(kù),需要將
Targets-->Build Phases-->Link Binary With Libraries
中 *.framework 對(duì)應(yīng)的Status由默認(rèn)的 Required 改成 Optional ;或者將 xx.framework 從 Link Binary With Libraries 列表中刪除。
可以使用dlopen加載動(dòng)態(tài)庫(kù),動(dòng)態(tài)庫(kù)中真正的可執(zhí)行代碼為 xx.framework/xx 文件。
- (IBAction)useDlopenLoad:(id)sender
{
NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/xx.framework/xx",NSHomeDirectory()];
[self dlopenLoadlib:documentsPath];
}
- (void)dlopenLoadlib:(NSString *)path
{
libHandle = NULL;
libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
if (libHandle == NULL) {
char *error = dlerror();
NSLog(@"dlopen error: %s", error);
} else {
NSLog(@"dlopen load framework success.");
}
}
也可以使用NSBundle來(lái)加載動(dòng)態(tài)庫(kù),實(shí)現(xiàn)代碼如下:
- (IBAction)useBundleLoad:(id)sender
{
NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/xx.framework",NSHomeDirectory()];
[self bundleLoadlib:documentsPath];
}
- (void)bundleLoadlib:(NSString *)path
{
_libPath = path;
NSError *err = nil;
NSBundle *bundle = [NSBundle bundleWithPath:path];
if ([bundle loadAndReturnError:&err]) {
NSLog(@"bundle load framework success.");
} else {
NSLog(@"bundle load framework err:%@",err);
}
}
可以為動(dòng)態(tài)庫(kù)的加載和移除添加監(jiān)聽(tīng)回調(diào),ImageLogger上有一個(gè)完整的示例代碼,從中可以發(fā)現(xiàn),一個(gè)工程軟件啟動(dòng)的時(shí)候會(huì)加載多達(dá)一百二十多個(gè)動(dòng)態(tài)庫(kù),即使是一個(gè)空白的項(xiàng)目。
但是,需要注意的一點(diǎn)是,不要在初始化方法中調(diào)用 dlopen(),對(duì)性能有影響。因?yàn)?dyld 在 App 開(kāi)始前運(yùn)行,由于此時(shí)是單線(xiàn)程運(yùn)行所以系統(tǒng)會(huì)取消加鎖,但 dlopen() 開(kāi)啟了多線(xiàn)程,系統(tǒng)不得不加鎖,這就嚴(yán)重影響了性能,還可能會(huì)造成死鎖以及產(chǎn)生未知的后果。所以也不要在初始化器中創(chuàng)建線(xiàn)程。
據(jù)說(shuō),iOS現(xiàn)在可以使用自定義的動(dòng)態(tài)庫(kù),低版本的需要手動(dòng)的使用dlopen()加載。動(dòng)態(tài)庫(kù)上架會(huì)有一些審核的規(guī)則,如不要把x86/i386的包和arm架構(gòu)的包lipo在一起使用。如:
lipo –create Release-iphoneos/libiphone.a Debig-iphonesimulator/libiphone.a –output libiphone.a
如此便將模擬器和設(shè)備的靜態(tài)庫(kù)文件合并成一個(gè)文件輸出了。
dylib加載調(diào)用
基于上面的分析,在exec()時(shí),系統(tǒng)內(nèi)核把應(yīng)用映射到新的地址空間,每次起始位置都是隨機(jī)的。然后使用dyld 加載 dylib 文件(動(dòng)態(tài)鏈接庫(kù)),dyld 在應(yīng)用進(jìn)程中運(yùn)行的工作就是加載應(yīng)用依賴(lài)的所有動(dòng)態(tài)鏈接庫(kù),準(zhǔn)備好運(yùn)行所需的一切,它擁有和應(yīng)用一樣的權(quán)限。
加載 Dylib時(shí),先從主執(zhí)行文件的 header 中獲取需要加載的所依賴(lài)動(dòng)態(tài)庫(kù)的列表,從中找到每個(gè) dylib,然后打開(kāi)文件讀取文件起始位置,確保它是 Mach-O 文件(針對(duì)不同運(yùn)行時(shí)可執(zhí)行文件的文件類(lèi)型)。然后找到代碼簽名并將其注冊(cè)到內(nèi)核。
應(yīng)用所依賴(lài)的 dylib 文件可能會(huì)再依賴(lài)其他 dylib,因此動(dòng)態(tài)庫(kù)列表是一個(gè)遞歸依賴(lài)的集合。一般應(yīng)用會(huì)加載 100 到 400 個(gè) dylib 文件,但大部分都是系統(tǒng) dylib,它們會(huì)被預(yù)先計(jì)算和緩存起來(lái),加載速度很快。但加載內(nèi)嵌(embedded)的 dylib 文件很占時(shí)間,所以盡可能把多個(gè)內(nèi)嵌 dylib 合并成一個(gè)來(lái)加載,或者使用 static archive。
在加載所有的動(dòng)態(tài)鏈接庫(kù)之后,它們只是處在相互獨(dú)立的狀態(tài),代碼簽名使得我們不能修改指令,那樣就不能讓一個(gè) dylib 調(diào)用另一個(gè) dylib。通過(guò)fix-up可以將它們結(jié)合起來(lái),dyld 所做的事情就是修正(fix-up)指針和數(shù)據(jù)。Fix-up 有兩種類(lèi)型,rebasing(在鏡像內(nèi)部調(diào)整指針的指向) 和 binding(將指針指向鏡像外部的內(nèi)容)。
因?yàn)榈刂房臻g加載隨機(jī)化的緣故,二進(jìn)制文件最終的加載地址與預(yù)期地址之間會(huì)存在偏移,所以需要進(jìn)行 rebase 操作,對(duì)那些指向文件內(nèi)部符號(hào)的指針進(jìn)行修正。rebase 完成之后,就會(huì)進(jìn)行 bind 操作,修正那些指向其他二進(jìn)制文件所包含的符號(hào)的指針。因?yàn)?dylib 之間有依賴(lài)關(guān)系,所以 動(dòng)態(tài)庫(kù)中的好多操作都是沿著依賴(lài)鏈遞歸操作的,Rebasing 和 Binding 分別對(duì)應(yīng)著 recursiveRebase() 和 recursiveBind() 這兩個(gè)方法。因?yàn)槭沁f歸,所以會(huì)自底向上地分別調(diào)用 doRebase() 和 doBind() 方法,這樣被依賴(lài)的 dylib 總是先于依賴(lài)它的 dylib 執(zhí)行 Rebasing 和 Binding。
Rebaing 消耗了大量時(shí)間在 I/O 上,在 Rebasing 和 Binding 前會(huì)判斷是否已經(jīng) 預(yù)綁定。如果已經(jīng)進(jìn)行過(guò)預(yù)綁定(Prebinding),那就不需要 Rebasing 和 Binding 這些 Fix-up 流程了,因?yàn)橐呀?jīng)在預(yù)先綁定的地址加載好了。
Binding 處理那些指向 dylib 外部的指針,它們實(shí)際上被符號(hào)(symbol)名稱(chēng)綁定,是一個(gè)字符串。dyld 需要找到 symbol 對(duì)應(yīng)的實(shí)現(xiàn),在符號(hào)表里查找時(shí)需要很多計(jì)算,找到后會(huì)將內(nèi)容存儲(chǔ)起來(lái)。Binding 看起來(lái)計(jì)算量比 Rebasing 更大,但其實(shí)需要的 I/O 操作很少,因?yàn)橹?Rebasing 已經(jīng)替 Binding 做過(guò)了。Objective-C 中有很多數(shù)據(jù)結(jié)構(gòu)都是靠 Rebasing 和 Binding 來(lái)修正(fix-up)的,比如 Class 中指向超類(lèi)的指針和指向方法的指針。
Objc
C++ 會(huì)為靜態(tài)創(chuàng)建的對(duì)象生成初始化器,與靜態(tài)語(yǔ)言不同,OC基于Runtime機(jī)制可以用類(lèi)的名字來(lái)實(shí)例化一個(gè)類(lèi)的對(duì)象。Runtime 維護(hù)了一張映射類(lèi)名與類(lèi)的全局表,當(dāng)加載一個(gè) dylib 時(shí),其定義的所有的類(lèi)都需要被注冊(cè)到這個(gè)全局表中。ObjC 在加載時(shí)可以通過(guò) fix-up 在動(dòng)態(tài)類(lèi)中改變實(shí)例變量的偏移量,利用這個(gè)技術(shù)可以在不改變dylib的情況下添加另一個(gè) dylib 中類(lèi)的方法,而非常見(jiàn)的通過(guò)定義類(lèi)別(Category)的方式改變一個(gè)類(lèi)的方法。
Dyld 在 bind 操作結(jié)束之后,會(huì)發(fā)出 dyld_image_state_bound 通知,然后與之綁定的回調(diào)函數(shù) map_2_images 就會(huì)被調(diào)用,它主要做以下幾件事來(lái)完成 Objc Setup:
1.讀取二進(jìn)制文件的 DATA 段內(nèi)容,找到與 objc 相關(guān)的信息。
2.注冊(cè) Objc 類(lèi)。
3.確保 selector 的唯一性。
4.讀取 protocol 以及 category 的信息。
除了 map_2_images,我們注意到 _objc_init 還注冊(cè)了 load_images 函數(shù),它的作用就是調(diào)用 Objc 的 + load 方法,它監(jiān)聽(tīng) dyld_image_state_dependents_initialized 通知。
dyld 是運(yùn)行在用戶(hù)態(tài)的, 這里由內(nèi)核態(tài)切到了用戶(hù)態(tài)。每當(dāng)有新的鏡像加載之后,都會(huì)執(zhí)行 load-images 方法進(jìn)行回調(diào),這里的回調(diào)是在整個(gè)Objc runtime 初始化時(shí) -objc-init 注冊(cè)的。有新的鏡像被 map 到 runtime 時(shí),調(diào)用 load-images 方法,并傳入最新鏡像的信息列表 infoList。調(diào)用 prepare-load-methods 對(duì) load 方法的調(diào)用進(jìn)行準(zhǔn)備(將需要調(diào)用 load 方法的類(lèi)添加到一個(gè)列表中),調(diào)用 -getObjc2NonlazyClassList 獲取所有的類(lèi)的列表之后,會(huì)通過(guò) remapClass 獲取類(lèi)對(duì)應(yīng)的指針,然后調(diào)用 schedule-class-load 遞歸地 將當(dāng)前類(lèi)和沒(méi)有調(diào)用 + load 父類(lèi)進(jìn)入列表。在執(zhí)行 add-class-to-loadable-list(cls) 將當(dāng)前類(lèi)加入加載列表之前,會(huì)先把父類(lèi)加入待加載的列表,保證父類(lèi)在子類(lèi)前調(diào)用 load 方法。在執(zhí)行 add-class-to-loadable-list(cls) 將當(dāng)前類(lèi)加入加載列表之前,會(huì)先把父類(lèi)加入待加載的列表,保證父類(lèi)在子類(lèi)前調(diào)用 load 方法。在將鏡像加載到運(yùn)行時(shí)、對(duì) load 方法的準(zhǔn)備就緒,執(zhí)行 call-load-methods,開(kāi)始調(diào)用 load 方法。

由于iOS開(kāi)發(fā)時(shí)基于Cocoa Touch的,所以絕大多數(shù)的類(lèi)起始都是系統(tǒng)類(lèi),大多數(shù)的Runtime初始化起始在Rebase和Bind中已經(jīng)完成。
Initializers
Objc SetUp 結(jié)束后,Dyld 便開(kāi)始運(yùn)行程序的初始化函數(shù),該任務(wù)由 initializeMainExecutable 函數(shù)執(zhí)行。整個(gè)初始化過(guò)程是一個(gè)遞歸的過(guò)程,順序是先將依賴(lài)的動(dòng)態(tài)庫(kù)初始化,然后在對(duì)自己初始化。初始化需要做的事情包括:
1.調(diào)用 Objc 類(lèi)的 + load 函數(shù)。
2.調(diào)用 C++ 中帶有 constructor 標(biāo)記的函數(shù)。
3.非基本類(lèi)型的 C++ 靜態(tài)全局變量的創(chuàng)建。
所謂執(zhí)行監(jiān)控啟動(dòng)crash的思路都是在這里構(gòu)建的。下面是一些方法的執(zhí)行順序,initialize的順序可能在更早,但總是會(huì)在load和launch之間。

程序啟動(dòng)邏輯
主執(zhí)行文件和相關(guān)的 dylib的依賴(lài)關(guān)系構(gòu)成了一張巨大的有向圖,執(zhí)行初始化器先加載葉子節(jié)點(diǎn),然后逐步向上加載中間節(jié)點(diǎn),直至最后加載根節(jié)點(diǎn)。這種加載順序確保了安全性,加載某個(gè) dylib 前,其所依賴(lài)的其余 dylib 文件肯定已經(jīng)被預(yù)先加載。最后 dyld 會(huì)調(diào)用 main() 函數(shù)。main() 會(huì)調(diào)用 UIApplicationMain(),程序啟動(dòng)。
使用Xcode打開(kāi)一個(gè)項(xiàng)目,很容易會(huì)發(fā)現(xiàn)一個(gè)文件-main.m文件,此處就是應(yīng)用的入口了。程序啟動(dòng)時(shí),先執(zhí)行main函數(shù),main函數(shù)是ios程序的入口點(diǎn),內(nèi)部會(huì)調(diào)用UIApplicationMain函數(shù),UIApplicationMain里會(huì)創(chuàng)建一個(gè)UIApplication對(duì)象 ,然后創(chuàng)建UIApplication的delegate對(duì)象 —–(您的)AppDelegate ,開(kāi)啟一個(gè)消息循環(huán)(main runloop),每當(dāng)監(jiān)聽(tīng)到對(duì)應(yīng)的系統(tǒng)事件時(shí),就會(huì)通知AppDelegate。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
}
}
UIApplication對(duì)象是應(yīng)用程序的象征,每一個(gè)應(yīng)用都有自己的UIApplication對(duì)象,而且是單例的。通過(guò)[UIApplication sharedApplication]可以獲得這個(gè)單例對(duì)象,一個(gè)iOS程序啟動(dòng)后創(chuàng)建的第一個(gè)對(duì)象就是UIApplication對(duì)象,利用UIApplication對(duì)象,能進(jìn)行一些應(yīng)用級(jí)別的操作。
UIApplicationMain函數(shù)實(shí)現(xiàn)如下:
int UIApplicationMain{
int argc,
char *argv[],
NSString *principalClassName,
NSString *delegateClassName
}
第一個(gè)參數(shù)表示參數(shù)的個(gè)數(shù),第二個(gè)參數(shù)表示裝載函數(shù)的數(shù)組,第三個(gè)參數(shù),是UIApplication類(lèi)名或其子類(lèi)名,若是nil,則默認(rèn)使用UIApplication類(lèi)名。第四個(gè)參數(shù)是協(xié)議UIApplicationDelegate的實(shí)例化對(duì)象名,這個(gè)對(duì)象就是UIApplication對(duì)象監(jiān)聽(tīng)到系統(tǒng)變化的時(shí)候通知其執(zhí)行的相應(yīng)方法。
啟動(dòng)完畢會(huì)調(diào)用 didFinishLaunching方法,并在這個(gè)方法中創(chuàng)建UIWindow,設(shè)置AppDelegate的window屬性,并設(shè)置UIWindow的根控制器。如果有storyboard,會(huì)根據(jù)info.plist中找到應(yīng)用程序的入口storyboard并加載箭頭所指的控制器,顯示窗口。storyboard和xib最大的不同在于storyboard是基于試圖控制器的,而非視圖或窗口。展示之前會(huì)將添加rootViewController的view到UIWindow上面(在這一步才會(huì)創(chuàng)建控制器的view)
[window addSubview: window.rootViewControler.view];
每個(gè)應(yīng)用程序至少有一個(gè)UIWindow,這window負(fù)責(zé)管理和協(xié)調(diào)應(yīng)用程序的屏幕顯示,rootViewController的view將會(huì)作為UIWindow的首視圖。

程序啟動(dòng)的完整過(guò)程如下:
1.main 函數(shù)。
2.UIApplicationMain
a.創(chuàng)建UIApplication對(duì)象。
b.創(chuàng)建UIApplication的delegate對(duì)象。
- delegate對(duì)象開(kāi)始處理(監(jiān)聽(tīng))系統(tǒng)事件(沒(méi)有storyboard)
a.程序啟動(dòng)完畢的時(shí)候, 就會(huì)調(diào)用代理的application:didFinishLaunchingWithOptions:方法。
b.在application:didFinishLaunchingWithOptions:中創(chuàng)建UIWindow。
c.創(chuàng)建和設(shè)置UIWindow的rootViewController。
d.顯示窗口。
4.根據(jù)Info.plist獲得最主要storyboard的文件名,加載最主要storyboard(有storyboard)。
a.創(chuàng)建UIWindow。
b.創(chuàng)建和設(shè)置UIWindow的rootViewController。
c.顯示窗口。
AppDelegate的代理方法
//app啟動(dòng)完畢后就會(huì)調(diào)用
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
}
//app程序失去焦點(diǎn)就會(huì)調(diào)用
- (void)applicationWillResignActive:(UIApplication *)application
{
}
//app進(jìn)入后臺(tái)的時(shí)候調(diào)用, 一般在這里保存應(yīng)用的數(shù)據(jù)(游戲數(shù)據(jù),比如暫停游戲)
- (void)applicationDidEnterBackground:(UIApplication *)application
{
}
//app程序程序從后臺(tái)回到前臺(tái)就會(huì)調(diào)用
- (void)applicationWillEnterForeground:(UIApplication *)application
{
}
//app程序獲取焦點(diǎn)就會(huì)調(diào)用
- (void)applicationDidBecomeActive:(UIApplication *)application
{
}
// 內(nèi)存警告,可能要終止程序,清除不需要再使用的內(nèi)存
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
}
// 程序即將退出調(diào)用
- (void)applicationWillTerminate:(UIApplication *)application
{
}
AppDelegate加載順序
1.application:didFinishLaunchingWithOptions:
2.applicationDidBecomeActive:
ViewController中的加載順序
1.loadView
2.viewDidLoad
3.viewWillAppear
4.viewWillLayoutSubviews
5.viewDidLayoutSubviews
6.viewDidAppear
View中的加載順序
1.initWithCoder(如果沒(méi)有storyboard就會(huì)調(diào)用initWithFrame,這里兩種方法視為一種)
2.awakeFromNib
3.layoutSubviews
4.drawRect
一些方法的使用時(shí)機(jī)
+ (void)load;
應(yīng)用程序啟動(dòng)就會(huì)調(diào)用的方法,在這個(gè)方法里寫(xiě)的代碼最先調(diào)用。
+ (void)initialize;
用到本類(lèi)時(shí)才調(diào)用,這個(gè)方法里一般設(shè)置導(dǎo)航控制器的主題等,如果在后面的方法設(shè)置導(dǎo)航欄主題就太遲了!
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions;
這個(gè)方法里面會(huì)創(chuàng)建UIWindow,設(shè)置根控制器并展現(xiàn),比如某些應(yīng)用程序要加載授權(quán)頁(yè)面也是在這加,也可以設(shè)置觀察者,監(jiān)聽(tīng)到通知切換根控制器等。
- (void)awakeFromNib;
在使用IB的時(shí)候才會(huì)涉及到此方法的使用,當(dāng).nib文件被加載的時(shí)候,會(huì)發(fā)送一個(gè)awakeFromNib的消息到.nib文件中的每個(gè)對(duì)象,每個(gè)對(duì)象都可以定義自己的awakeFromNib函數(shù)來(lái)響應(yīng)這個(gè)消息,執(zhí)行一些必要的操作。在這個(gè)方法里設(shè)置view的背景等一系列普通操作。
- (void)loadView;
創(chuàng)建視圖的層次結(jié)構(gòu),在沒(méi)有創(chuàng)建控制器的view的情況下不能直接寫(xiě) self.view 因?yàn)閟elf.view的底層是:
if(_view == nil){
_view = [self loadView]
}
這么寫(xiě)會(huì)直接造成死循環(huán)。
如果重寫(xiě)這個(gè)loadView方法里面什么都不寫(xiě),會(huì)顯示黑屏。
- (void)viewWillLayoutSubviews;
視圖將要布局子視圖,蘋(píng)果建議的設(shè)置界面布局屬性的方法,這個(gè)方法和viewWillAppear里,系統(tǒng)的底層都是沒(méi)有寫(xiě)任何代碼的,也就是說(shuō)這里面不寫(xiě)super 也是可以的。
- (void)layoutSubviews;
在這個(gè)方法里一般設(shè)置子控件的frame。
- (void)drawRect:(CGRect)rect;
UI控件都是畫(huà)上去的,在這一步就是把所有的東西畫(huà)上去。drawRect方法只能在加載時(shí)調(diào)用一次,如果后面還需要調(diào)用,比如下載進(jìn)度的圓弧,需要一直刷幀,就要使用setNeedsDisplay來(lái)定時(shí)多次調(diào)用本方法。
- (void)applicationDidBecomeActive:(UIApplication *)application;
這是AppDelegate的應(yīng)用程序獲取焦點(diǎn)方法,真正到了這里,才是所有東西全部加載完畢。
啟動(dòng)分析
應(yīng)用啟動(dòng)時(shí),會(huì)播放一個(gè)啟動(dòng)動(dòng)畫(huà)。iPhone上是400ms,iPad上是500ms。如果應(yīng)用啟動(dòng)過(guò)慢,用戶(hù)就會(huì)放棄使用,甚至永遠(yuǎn)都不再回來(lái)。為了防止一個(gè)應(yīng)用占用過(guò)多的系統(tǒng)資源,開(kāi)發(fā)iOS的蘋(píng)果工程師門(mén)設(shè)計(jì)了一個(gè)“看門(mén)狗”的機(jī)制。在不同的場(chǎng)景下,“看門(mén)狗”會(huì)監(jiān)測(cè)應(yīng)用的性能。如果超出了該場(chǎng)景所規(guī)定的運(yùn)行間,“看門(mén)狗”就會(huì)強(qiáng)制終結(jié)這個(gè)應(yīng)用的進(jìn)程。
iOS App啟動(dòng)時(shí)會(huì)鏈接并加載Framework和Static lib,執(zhí)行UIKit初始化,然后進(jìn)入應(yīng)用程序回調(diào),執(zhí)行Core Animation transaction等。每個(gè)Framework都會(huì)增加啟動(dòng)時(shí)間和占用的內(nèi)存,不要鏈接不必要的Framework,必要的Framework不要標(biāo)記為Optional。避免創(chuàng)建全局的C++對(duì)象。
初始化UIKit時(shí)字體、狀態(tài)欄、user defaults、Main.storyboard會(huì)被初始化。User defaults本質(zhì)上是一個(gè)plist文件,保存的數(shù)據(jù)是同時(shí)被反序列化的,不要在user defaults里面保存圖片等大數(shù)據(jù)。
對(duì)于 OC 來(lái)說(shuō)應(yīng)盡量減少 class,selector 和 category 這些元數(shù)據(jù)的數(shù)量。編碼原則和設(shè)計(jì)模式之類(lèi)的理論會(huì)鼓勵(lì)大家多寫(xiě)精致短小的類(lèi)和方法,并將每部分方法獨(dú)立出一個(gè)類(lèi)別,但這會(huì)增加啟動(dòng)時(shí)間。在調(diào)用的地方使用初始化器,不要使用atribute((constructor)) 將方法顯式標(biāo)記為初始化器,而是讓初始化方法調(diào)用時(shí)才執(zhí)行。比如使用 dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用時(shí)才初始化,推遲了一部分工作耗時(shí)。
建立網(wǎng)絡(luò)連接前需要做域名解析,如果網(wǎng)關(guān)出現(xiàn)問(wèn)題,dns解析不正常時(shí),dns的超時(shí)時(shí)間是應(yīng)用控制不了的。在程序設(shè)計(jì)時(shí)要考慮這些問(wèn)題,如果程序啟動(dòng)時(shí)有網(wǎng)絡(luò)連接,應(yīng)盡快的結(jié)束啟動(dòng)過(guò)程,網(wǎng)絡(luò)訪問(wèn)通過(guò)線(xiàn)程解決,而不阻塞主線(xiàn)程的運(yùn)行。
