iOS之武功秘籍⑦: dyld加載流程 -- 應(yīng)用程序的加載

iOS之武功秘籍 文章匯總

寫在前面

我們平時編寫的程序的入口函數(shù)都是main.m文件里面的main函數(shù),但是這就是App的生命起點了嗎?玩過逆向的iOSer都知道可以往+load方法注入代碼來進(jìn)行安全攻防,而+load方法先于main函數(shù)執(zhí)行,那么main函數(shù)之前都發(fā)生了哪些有趣的事呢?本文就將帶著大家來揭開這片神秘面紗!

本節(jié)可能用到的秘籍Demo

一、編譯過程與動靜態(tài)庫

我們先來看個??:

  • 創(chuàng)建一個project,在ViewController中重寫了load方法,在main.m中加了一個C++方法,即cjFunc,請問它們的打印先后順序是什么?

  • 運行程序,查看 loadcjFuncmain的打印順序,下面是打印結(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)文件中,這就需要鏈接器將它們與我們自己的代碼鏈接起來

FoundationUIKit這種可以共享代碼、實現(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)用了dyldmain函數(shù),其中macho_headerMach-O的頭部,而dyld加載的文件就是Mach-O類型的,即Mach-O類型可執(zhí)行文件類型,由四部分組成:Mach-O頭部Load CommandsectionOther 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)

只要設(shè)置了這兩個環(huán)境變量參數(shù),在App啟動時就會打印相關(guān)參數(shù)、環(huán)境變量信息(自行嘗試研究)

③.2 共享緩存

  • checkSharedRegionDisable檢查是否開啟共享緩存(在iOS中必須開啟)
  • mapSharedCache加載共享緩存庫,其中調(diào)用loadDyldCache函數(shù)有這么幾種情況:
    • 僅加載到當(dāng)前進(jìn)程mapCachePrivate(模擬器僅支持加載到當(dāng)前進(jìn)程)
    • 共享緩存是第一次被加載,就去做加載操作mapCacheSystemWide
    • 共享緩存不是第一次被加載,那么就不做任何處理


③.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_PATHLD_LIBRARY_PATHDYLD_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的初始化必須先運行

可以通過測試程序的堆棧信息來驗證,在C++方法出加一個斷點

走到這里,還是沒有找到_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ù)2sNotifyObjcInit()調(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í)行,來到cjFuncC++函數(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í),不急不躁.我還是我,顏色不一樣的煙火.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,643評論 2 380

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