寫在前面
我們平時編寫的程序的入口函數(shù)都是main.m
文件里面的main
函數(shù),但是這就是App
的生命起點了嗎?玩過逆向的iOSer
都知道可以往+load
方法注入代碼來進(jìn)行安全攻防,而+load
方法先于main
函數(shù)執(zhí)行,那么main
函數(shù)之前都發(fā)生了哪些有趣的事呢?本文就將帶著大家來揭開這片神秘面紗!
一、編譯過程與動靜態(tài)庫
我們先來看個??:
-
創(chuàng)建一個
project
,在ViewController
中重寫了load
方法,在main.m
中加了一個C++
方法,即cjFunc
,請問它們的打印先后順序是什么? -
運行程序,查看
load
、cjFunc
、main
的打印順序,下面是打印結(jié)果,通過結(jié)果可以看出其順序是load --> C++方法 --> main()
為什么是這么一個順序?按照常規(guī)的思維理解,main
不是入口函數(shù)嗎?為什么不是main
最先執(zhí)行?
下面根據(jù)這個問題,我們來探索在走到main
函數(shù)之前,到底還做了什么.
在探索分析app
啟動之前,我們需要先了解iOS中App代碼的編譯過程
以及動態(tài)庫
和靜態(tài)庫
.
① 編譯過程
在日常開發(fā)過程中,開發(fā)者會使用成千上萬次的Command + B/R
進(jìn)行開發(fā)調(diào)試,但可能很少有人關(guān)注過這個過程中 Xcode
幫我們做了哪些事情(iOS開發(fā)者往往會吐槽Xcode越來越難用了,但不得不承認(rèn)它越來越強(qiáng)了)
事實上,這個過程分解為4個步驟,分別是預(yù)處理(Prepressing)、編譯(Compilation)、匯編(Assembly)和鏈接(Linking).------ 摘自《程序員的自我修養(yǎng)-- 鏈接、裝載與庫》
在以上4個步驟中,IDE
主要做了以下幾件事:
-
預(yù)編譯
:處理代碼中的# 開頭
的預(yù)編譯指令,比如刪除#define
并展開宏定義,將#include
包含的文件插入到該指令位置等(即替換宏,刪除注釋,展開頭文件,產(chǎn)生.i文件
) -
編譯
:對預(yù)編譯處理過的文件進(jìn)行詞法分析、語法分析和語義分析,并進(jìn)行源代碼優(yōu)化,然后生成匯編代碼(即將.i文件
轉(zhuǎn)換為匯編語言,產(chǎn)生.s文件
) -
匯編
:通過匯編器將匯編代碼轉(zhuǎn)換為機(jī)器可以執(zhí)行的指令,并生成目標(biāo)文件.o文件
-
鏈接
:將目標(biāo)文件鏈接成可執(zhí)行文件.這一過程中,鏈接器將不同的目標(biāo)文件鏈接起來,因為不同的目標(biāo)文件之間可能有相互引用的變量或調(diào)用的函數(shù),如我們經(jīng)常調(diào)用Foundation
框架和UIKit
框架中的方法和變量,但是這些框架跟我們的代碼并不在一個目標(biāo)文件中,這就需要鏈接器將它們與我們自己的代碼鏈接起來
Foundation
和UIKit
這種可以共享代碼、實現(xiàn)代碼的復(fù)用統(tǒng)稱為庫
——它是可執(zhí)行代碼的二進(jìn)制文件,可以被操作系統(tǒng)寫入內(nèi)存,它又分為靜態(tài)庫
和動態(tài)庫
② 靜態(tài)庫
靜態(tài)庫
是指鏈接時完整的拷貝到可執(zhí)行文件,多次使用多次拷貝,造成冗余,使包變的更大
如.a
、.lib
都是靜態(tài)庫
③ 動態(tài)庫
動態(tài)庫
是指鏈接時不復(fù)制,程序運行時由系統(tǒng)加在到內(nèi)存中,供系統(tǒng)調(diào)用,系統(tǒng)只需加載一次,多次使用,共用節(jié)省內(nèi)存.
如.dylib
、.framework
都是動態(tài)庫
二、dyld
① dyld簡介
dyld
(The dynamic link editor
)是蘋果的動態(tài)鏈接器
,負(fù)責(zé)程序的鏈接及加載工作,是蘋果操作系統(tǒng)的重要組成部分,存在于MacOS
系統(tǒng)的(/usr/lib/dyld
)目錄下.在應(yīng)用被編譯打包成可執(zhí)行文件格式的Mach-O
文件之后 ,交由dyld
負(fù)責(zé)鏈接,加載程序.
所以 App
的啟動流程圖如下
② dyld_shared_cache
由于不止一個程序需要使用UIKit
系統(tǒng)動態(tài)庫,所以不可能在每個程序加載時都去加載所有的系統(tǒng)動態(tài)庫.為了優(yōu)化程序啟動速度和利用動態(tài)庫緩存,蘋果從iOS3.1
之后,將所有系統(tǒng)庫(私有與公有)編譯成一個大的緩存文件,這就是dyld_shared_cache
,該緩存文件存在iOS系統(tǒng)下的/System/Library/Caches/com.apple.dyld/
目錄下
三、dyld加載流程
在前文的Demo
中,在load方法
和main方法
處加一個斷點
點擊函數(shù)調(diào)用棧/使用LLDB——bt
指令打印,都能看到最初的起點_dyld_start
接下來怎么去研究dyld
呢,我們將通過dyld源碼展開分析
① 1._dyld_start
在源碼中全局搜索_dyld_start
,會發(fā)現(xiàn)它是由匯編實現(xiàn)的
在arm64
中,_dyld_start
調(diào)用了一個看不懂的方法
從注釋中得出可能是dyldbootstrap::start
方法(其實在“函數(shù)調(diào)用棧”那張圖中匯編代碼已經(jīng)把這個方法暴露出來了)
② dyldbootstrap::start
其實dyldbootstrap::start
是指dyldbootstrap
這個命名空間作用域里的 start
函數(shù)
源碼中搜索dyldbootstrap
找到命名作用空間
.
再在這個文件中查找start
方法,其核心是返回值調(diào)用了dyld
的main
函數(shù),其中macho_header
是Mach-O
的頭部,而dyld
加載的文件就是Mach-O類型
的,即Mach-O類型
是可執(zhí)行文件類型
,由四部分組成:Mach-O頭部
、Load Command
、section
、Other Data
,可以通過MachOView
查看可執(zhí)行文件信息
在start()
函數(shù)中主要做了一下幾件事:
- 根據(jù)
dyldsMachHeader
計算出slide
, 通過slide
判定是否需要重定位;這里的slide
是根據(jù)ASLR
技術(shù) 計算出的一個隨機(jī)值,使得程序每一次運行的偏移值都不一樣,防止攻擊者通過固定地址發(fā)起惡意攻擊 -
mach_init()
初始化(允許dyld使用mach消息傳遞) - 棧溢出保護(hù)
- 計算
appsMachHeader
的偏移,調(diào)用dyld::_main()
函數(shù)
③ dyld::_main()
dyld::_main()主要流程為:
-
環(huán)境變量配置
:根據(jù)環(huán)境變量設(shè)置相應(yīng)的值以及獲取當(dāng)前運行架構(gòu) -
共享緩存
:檢查是否開啟了共享緩存,以及共享緩存是否映射到共享區(qū)域,例如UIKit、CoreFoundation
等 -
主程序的初始化
:調(diào)用instantiateFromLoadedImage
函數(shù)實例化了一個ImageLoader
對象 -
插入動態(tài)庫
:遍歷DYLD_INSERT_LIBRARIES
環(huán)境變量,調(diào)用loadInsertedDylib
加載 link 主程序
link 動態(tài)庫
弱符號綁定
執(zhí)行初始化方法
-
尋找主程序入口
即main
函數(shù)
③.1 環(huán)境變量配置
- 平臺,版本,路徑,主機(jī)信息的確定
- 從環(huán)境變量中獲取主要可執(zhí)行文件的
cdHash
-
checkEnvironmentVariables(envp)
檢查設(shè)置環(huán)境變量 -
defaultUninitializedFallbackPaths(envp)
在DYLD_FALLBACK
為空時設(shè)置默認(rèn)值 -
getHostInfo(mainExecutableMH, mainExecutableSlide)
獲取程序架構(gòu)
③.2 共享緩存
-
checkSharedRegionDisable
檢查是否開啟共享緩存(在iOS中必須開啟) -
mapSharedCache
加載共享緩存庫,其中調(diào)用loadDyldCache
函數(shù)有這么幾種情況:- 僅加載到當(dāng)前進(jìn)程
mapCachePrivate
(模擬器僅支持加載到當(dāng)前進(jìn)程) - 共享緩存是第一次被加載,就去做加載操作
mapCacheSystemWide
-
共享緩存不是第一次被加載,那么就不做任何處理
- 僅加載到當(dāng)前進(jìn)程
③.3 主程序的初始化
- ①調(diào)用
instantiateFromLoadedImage
函數(shù)實例化了一個ImageLoader
對象
- ②進(jìn)入
instantiateFromLoadedImage
源碼,其中創(chuàng)建一個ImageLoader
實例對象,通過instantiateMainExecutable
方法創(chuàng)建
- ③進(jìn)入
instantiateMainExecutable
源碼,其作用是為主可執(zhí)行文件創(chuàng)建映像,返回一個ImageLoader
類型的image
對象,即主程序
.其中sniffLoadCommands
函數(shù)會獲取Mach-O
類型文件的Load Command
的相關(guān)信息,并對其進(jìn)行各種校驗
③.4 插入動態(tài)庫
遍歷DYLD_INSERT_LIBRARIES
環(huán)境變量,調(diào)用loadInsertedDylib
加載,通過該環(huán)境變量我們可以注入自定義的一些動態(tài)庫代碼從而完成安全攻防,loadInsertedDylib
內(nèi)部會從DYLD_ROOT_PATH
、LD_LIBRARY_PATH
、DYLD_FRAMEWORK_PATH
等路徑查找dylib
并且檢查代碼簽名,無效則直接拋出異常
③.5 link 主程序
③.6 link 動態(tài)庫
③.7 弱符號綁定
③.8 執(zhí)行初始化方法
先回顧一下函數(shù)調(diào)用棧- ①進(jìn)入
initializeMainExecutable
源碼,主要是循環(huán)遍歷,都會執(zhí)行runInitializers
方法
- ②全局搜索
runInitializers(cons
,找到如下源碼,其核心代碼是processInitializers
函數(shù)的調(diào)用為初始化做準(zhǔn)備
- ③進(jìn)入
processInitializers
函數(shù)的源碼實現(xiàn),其中對鏡像列表調(diào)用recursiveInitialization
函數(shù)進(jìn)行遞歸實例化
- ④全局搜索
recursiveInitialization(cons
函數(shù),其作用獲取到鏡像的初始化,其源碼實現(xiàn)如下
在這里,需要分成兩部分探索,一部分是notifySingle
函數(shù),一部分是doInitialization
函數(shù),首先探索notifySingle
函數(shù)
- ⑤全局搜索
notifySingle(
函數(shù),其重點是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
這句.
- ⑥全局搜索
sNotifyObjCInit
,發(fā)現(xiàn)沒有找到實現(xiàn),有賦值操作
- ⑦搜索
registerObjCNotifiers
在哪里調(diào)用了,發(fā)現(xiàn)在_dyld_objc_notify_register
進(jìn)行了調(diào)用,這個函數(shù)只在運行時提供給objc
使用
注意:_dyld_objc_notify_register
的函數(shù)需要在libobjc
源碼中搜索
- ⑧在
objc4
源碼中搜索_dyld_objc_notify_register
,發(fā)現(xiàn)在_objc_init
源碼中調(diào)用了該方法,并傳入了參數(shù),所以sNotifyObjCInit
的賦值的就是objc
中的load_images
,而load_images
會調(diào)用所有的+load
方法.所以綜上所述,notifySingle是一個回調(diào)函數(shù)
都到這了,那就順便看看load函數(shù)的加載吧
下面我們進(jìn)入load_images
的源碼看看其實現(xiàn),以此來證明load_images
中調(diào)用了所有的load
函數(shù)
- 通過
objc源碼
中_objc_init
源碼實現(xiàn),進(jìn)入load_images
的源碼實現(xiàn)
- 進(jìn)入
call_load_methods
源碼實現(xiàn),可以發(fā)現(xiàn)其核心是通過do-while
循環(huán)調(diào)用+load
方法
- 進(jìn)入
call_class_loads
源碼實現(xiàn),了解到這里調(diào)用的load方法
證實我們前文提及的類的load方法
所以,load_images
調(diào)用了所有的load
函數(shù),以上的源碼分析過程正好對應(yīng)堆棧的打印信息
那么問題又來了,_objc_init
是什么時候調(diào)用的呢?請接著往下看
- ⑨ 走到
objc
的_objc_init
函數(shù),發(fā)現(xiàn)走不通了,我們回退到recursiveInitialization
遞歸函數(shù)的源碼實現(xiàn),發(fā)現(xiàn)我們忽略了一個函數(shù)doInitialization
,進(jìn)入doInitialization
函數(shù)的源碼實現(xiàn)
這里也需要分成兩部分,一部分是doImageInit
函數(shù),一部分是doModInitFunctions
函數(shù)
-
⑩進(jìn)入
doImageInit
源碼實現(xiàn),其核心主要是for循環(huán)
加載方法的調(diào)用,這里需要注意的一點是,libSystem
的初始化必須先運行 -
?進(jìn)入
doModInitFunctions
源碼實現(xiàn),這個方法中加載了所有Cxx
文件,這里需要注意的一點是,libSystem
的初始化必須先運行
走到這里,還是沒有找到_objc_init
的調(diào)用?怎么辦呢?放棄嗎?當(dāng)然不行,我們還可以通過_objc_init
加一個符號斷點來查看調(diào)用_objc_init
前的堆棧信息
- ?在
libsystem
中查找libSystem_initializer
,查看其中的實現(xiàn)
- ?根據(jù)前面的堆棧信息,我們發(fā)現(xiàn)走的是
libSystem_initializer
中會調(diào)用libdispatch_init
函數(shù),而這個函數(shù)的源碼是在libdispatch
開源庫中的,在libdispatch
中搜索libdispatch_init
- ?進(jìn)入
_os_object_init
源碼實現(xiàn),其源碼實現(xiàn)調(diào)用了_objc_init
函數(shù)
結(jié)合上面的分析,從初始化_objc_init
注冊的_dyld_objc_notify_register
的參數(shù)2,即load_images
,到sNotifySingle --> sNotifyObjCInie=參數(shù)2
到sNotifyObjcInit()
調(diào)用,形成了一個閉環(huán)
也可以簡單的理解為sNotifySingle
這里是添加通知即addObserver
,_objc_init
中調(diào)用_dyld_objc_notify_register
相當(dāng)于發(fā)送通知,即push
,而sNotifyObjcInit
相當(dāng)于通知的處理函數(shù),即selector
.
③.9 尋找主程序入口
- ①在測試程序中匯編調(diào)試,可以看到顯示來到
+[ViewController load]
方法
- ②繼續(xù)執(zhí)行,來到
cjFunc
的C++
函數(shù)
- ③點擊
stepover
,繼續(xù)往下,跑完了整個流程,會回到_dyld_start
,然后調(diào)用main()
函數(shù),通過匯編完成main
的參數(shù)賦值等操作
- ④
dyld
匯編源碼實現(xiàn)
最后注意:main
是寫定的函數(shù),寫入內(nèi)存,讀取到dyld
,如果修改了main
函數(shù)的名稱,會報錯
所以,綜上所述,最終dyld
加載流程,如下圖所示,圖中也詮釋了前文中的問題:為什么是load-->Cxx-->main
的調(diào)用順序
寫在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.