Mach-O簡(jiǎn)介與App加載流程

Mach-O

【Mach-O】 為 Mach Object 文件格式的縮寫,是 iOS 系統(tǒng)不同運(yùn)行時(shí)期 可執(zhí)行文件 的文件類型統(tǒng)稱。它是一種用于 可執(zhí)行文件、目標(biāo)代碼、動(dòng)態(tài)庫(kù)、內(nèi)核轉(zhuǎn)儲(chǔ)的文件格式。

【Mach-O】 的三種文件類型:Executable、Dylib、Bundle
Executable 是 app 的二進(jìn)制主文件。
Dylib 是動(dòng)態(tài)庫(kù),動(dòng)態(tài)庫(kù)分為 動(dòng)態(tài)鏈接庫(kù) 和 動(dòng)態(tài)加載庫(kù)。

動(dòng)態(tài)鏈接庫(kù):在沒(méi)有被加載到內(nèi)存的前提下,當(dāng)可執(zhí)行文件被加載,動(dòng)態(tài)庫(kù)也隨著被加載到內(nèi)存中?!倦S著程序啟動(dòng)而啟動(dòng)】
動(dòng)態(tài)加載庫(kù):當(dāng)需要的時(shí)候再使用 dlopen 等通過(guò)代碼或者命令的方式加載?!境绦騿?dòng)之后】

Bundle 是一種特殊類型的Dylib,你無(wú)法對(duì)其進(jìn)行鏈接。所能做的是在Runtime運(yùn)行時(shí)通過(guò)dlopen來(lái)加載它,它可以在macOS 上用于插件。
Image (鏡像文件)包含了上述的三種類型。
Framework 可以理解為動(dòng)態(tài)庫(kù)。

Mach-O的結(jié)構(gòu)

mach-o結(jié)構(gòu).png

Header:保存【Mach-O】的一些基本信息,包括運(yùn)行平臺(tái)、文件類型、LoadCommands指令的個(gè)數(shù)、指令總大小,dyld標(biāo)記Flags等等。
Load Commands:緊跟Header,這些加載指令清晰地告訴加載器如何處理二進(jìn)制數(shù)據(jù),有些命令是由內(nèi)核處理的,有些是由動(dòng)態(tài)鏈接器處理的。加載【Mach-O】文件時(shí)會(huì)使用這部分?jǐn)?shù)據(jù)確定內(nèi)存分布以及相關(guān)的加載命令,對(duì)系統(tǒng)內(nèi)核加載器和動(dòng)態(tài)連接器起指導(dǎo)作用。比如我們的main()函數(shù)的加載地址、程序所需的dyld的文件路徑、以及相關(guān)依賴庫(kù)的文件路徑。
Data:每個(gè)segment的具體數(shù)據(jù)保存在這里,包含具體的代碼、數(shù)據(jù)等等。
segment:【Mach-O】 鏡像文件 是由 segments 段組成的。段的名稱為大寫格式。所有的段都是 page size 的倍數(shù),在arm64上為 16kB,其它架構(gòu)為 4KB。

常見(jiàn)的segments:
__TEXT:代碼段,包含頭文件、代碼和只讀常量。只讀不可修改
__DATA:數(shù)據(jù)段,包含全局變量,靜態(tài)變量等。可讀可寫
_LINKEDIT:如何加載程序,包含了方法和變量的元數(shù)據(jù)(位置,偏移量),以及代碼簽名等信息。只讀不可修改。

有兩種主要的技術(shù)來(lái)保證應(yīng)用的安全:ASLR 和 Code Sign

【ASLR】的全稱是Address space layout randomization,翻譯過(guò)來(lái)就是“地址空間布局隨機(jī)化”。App被啟動(dòng)的時(shí)候,程序會(huì)被映射到邏輯的地址空間,這個(gè)邏輯的地址空間有一個(gè)起始地址,而【ASLR】技術(shù)使得這個(gè)起始地址是隨機(jī)的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函數(shù)的地址。

【Code Sign】相信大多數(shù)開(kāi)發(fā)者都知曉,這里要提一點(diǎn)的是,為了在運(yùn)行時(shí) 驗(yàn)證【Mach-O】 文件的簽名,在進(jìn)行【Code Sign】的時(shí)候,加密哈希不是針對(duì)于整個(gè)文件,而是針對(duì)于每一個(gè)Page的。并存儲(chǔ)在 __LINKEDIT 中。這就保證了在dyld進(jìn)行加載的時(shí)候,可以對(duì)每一個(gè)page進(jìn)行獨(dú)立的驗(yàn)證。

dyld

當(dāng)內(nèi)核完成映射進(jìn)程的工作后,會(huì)將名字為 dyld 的 Mach-O 文件映射到進(jìn)程中的隨機(jī)地址,它將PC 寄存器設(shè)為 dyld 的地址并運(yùn)行。dyld 在應(yīng)用進(jìn)程中運(yùn)行的工作是加載應(yīng)用依賴的所有動(dòng)態(tài)鏈接庫(kù),準(zhǔn)備好運(yùn) 行所需的一切,它擁有的權(quán)限跟應(yīng)用程序一樣。

dyld(the dynamic link editor),【動(dòng)態(tài)鏈接器】是蘋果操作系統(tǒng)一個(gè)重要部分,在 iOS / macOS 系統(tǒng)中,僅有很少的進(jìn)程只需內(nèi)核就可以完成加載,基本上所有的進(jìn)程都是動(dòng)態(tài)鏈接的,所以 Mach-O 鏡像文件中會(huì)有很多對(duì)外部的庫(kù)和符號(hào)的引用,但是這些引用并不能直接用,在啟動(dòng)時(shí)還必須要通過(guò)這些引用進(jìn)行內(nèi)容填充,這個(gè)填充的工作就是由 dyld 來(lái)完成的。

【動(dòng)態(tài)鏈接加載器】在系統(tǒng)中以一個(gè)用戶態(tài)的可執(zhí)行文件形式存在,一般應(yīng)用程序會(huì)在Mach-O文件部分指定一個(gè) LC_LOAD_DYLINKER 的加載命令,此加載命令指定了dyld的路徑,通常它的默認(rèn)值是“/usr/lib/dyld”。系統(tǒng)內(nèi)核在加載Mach-O文件時(shí),會(huì)使用該路徑指定的程序作為動(dòng)態(tài)庫(kù)的加載器來(lái)加載dylib。

dyld 流程

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

Load dylibs:
從主執(zhí)行文件header獲取到需要加載的所依賴的動(dòng)態(tài)庫(kù)列表,而header早就被內(nèi)核映射過(guò)。然后它需要找到每個(gè)dylib,然后打開(kāi)文件,讀取文件起始位置,確保它是Mach-O文件。接著會(huì)找到代碼簽名并將其注冊(cè)到內(nèi)核。然后在dylib文件的每個(gè)segment上調(diào)用mmap()。應(yīng)用所依賴的dylib文件可能會(huì)再依賴其他dylib,所以dyld所需要加載的是動(dòng)態(tài)庫(kù)列表一個(gè)遞歸依賴的集合。一般應(yīng)用會(huì)加載100到400 個(gè)dylib文件,但大部分都是系統(tǒng)的dylib,它們會(huì)被預(yù)先計(jì)算和緩存起來(lái),加載速度很快。

Fix-ups:
在加載所有的動(dòng)態(tài)鏈接庫(kù)之后,它們只是處在相互獨(dú)立的狀態(tài),需要將它們綁定起來(lái),這就是Fix-ups。代碼簽名使得我們不能修改指令,那樣就不能讓一個(gè)dylib 調(diào)用另一個(gè) dylib,這是就需要很多間接層。

Mach-O中有很多符號(hào),有指向當(dāng)前 Mach-O 的,也有指向其他 dylib 的,比如printf。那么,在運(yùn)行時(shí),代碼如何準(zhǔn)確的找到printf的地址呢?

Mach-O中采用了PIC技術(shù),全稱是Position Independ code。意味著代碼可以被加載到間接的地址上。當(dāng)你的程序要調(diào)用printf的時(shí)候,會(huì)先在 __DATA 段中建立一個(gè)指針指向printf,在通過(guò)這個(gè)指針實(shí)現(xiàn)間接調(diào)用。dyld這時(shí)候需要做一些fix-up工作,即幫助應(yīng)用程序找到這些符號(hào)的實(shí)際地址。主要包括兩部分:rebasing和binding。

Rebasing:在鏡像內(nèi)部調(diào)整指針的指向。
Binding: 將指針指向鏡像外部的內(nèi)容。
之所以需要Rebase,是因?yàn)閯倓偺岬降?ASLR 使得地址隨機(jī)化,導(dǎo)致起始地址不固定,另外由于 Code Sign,導(dǎo)致不能直接修改 Image。Rebase的時(shí)候只需要增加對(duì)應(yīng)的偏移量即可。(待Rebase的數(shù)據(jù)都存放在__LINKEDIT中,可以通過(guò)MachOView查看:Dynamic Loader Info -> Rebase Info)

Binding就是將這個(gè)二進(jìn)制調(diào)用的外部符號(hào)進(jìn)行綁定的過(guò)程。 比如我們objc代碼中需要使用到NSObject, 即符號(hào)OBJC_CLASS$_NSObject,但是這個(gè)符號(hào)又不在我們的二進(jìn)制中,在系統(tǒng)庫(kù) Foundation.framework中,因此就需要Binding這個(gè)操作將對(duì)應(yīng)關(guān)系綁定到一起。

Rebase解決了內(nèi)部的符號(hào)引用問(wèn)題,而外部的符號(hào)引用則是由Bind解決。在解決Bind的時(shí)候,是根據(jù)字符串匹配的方式查找符號(hào)表,所以這個(gè)過(guò)程相對(duì)于Rebase來(lái)說(shuō)是略慢的。

dyld 2 和 dyld 3
dyld區(qū)別.png

在 iOS 13之前,所有的第三方App都是通過(guò)dyld 2來(lái)啟動(dòng) App 的,主要過(guò)程如下:
解析 Mach-O的Header 和 Load Commands,找到其依賴的庫(kù),并遞歸找到所有依賴的庫(kù)
加載Mach-O文件
進(jìn)行符號(hào)查找
綁定和變基
運(yùn)行初始化程序

dyld 3被分為了三個(gè)組件:
一個(gè)進(jìn)程外的Mach-O 解析器
預(yù)先處理了所有可能影響啟動(dòng)速度的search path、@rpaths和環(huán)境變量
然后分析Mach-O的Header和依賴,并完成了所有符號(hào)查找的工作
最后將這些結(jié)果創(chuàng)建成一個(gè)啟動(dòng)閉包
這是一個(gè)普通的daemon進(jìn)程,可以使用通常的測(cè)試架構(gòu)

一個(gè)進(jìn)程內(nèi)的引擎,用來(lái)運(yùn)行啟動(dòng)閉包
這部分在進(jìn)程中處理
驗(yàn)證啟動(dòng)閉包的安全性,然后映射到dylib之中,再跳轉(zhuǎn)到main函數(shù)
不需要解析Mach-O的 Header 和依賴,也不需要符號(hào)查找。

一個(gè)啟動(dòng)閉包緩存服務(wù)
系統(tǒng)App的啟動(dòng)閉包被構(gòu)建在一個(gè)Shared Cache 中,我們甚至不需要打開(kāi)一個(gè)單獨(dú)的文件
對(duì)于第三方的App,我們會(huì)在App安裝或者升級(jí)的時(shí)候構(gòu)建這個(gè)啟動(dòng)閉包。
在iOS、tvOS、watchOS中,這一切都是App啟動(dòng)之前完成的。在macOS上,由于有Side Load App,進(jìn)程內(nèi)引擎會(huì)在首次啟動(dòng)的時(shí)候啟動(dòng)一個(gè)daemon進(jìn)程,之后就可以使用啟動(dòng)閉包啟動(dòng)了。

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

App加載流程

編譯過(guò)程

其中編譯過(guò)程如下圖所示,主要分為以下幾步:
源文件:載入.h、.m、.cpp等文件
預(yù)處理:替換宏,刪除注釋,展開(kāi)頭文件,產(chǎn)生.i文件
編譯:將.i文件轉(zhuǎn)換為匯編語(yǔ)言,產(chǎn)生.s文件
匯編:將匯編文件轉(zhuǎn)換為機(jī)器碼文件,產(chǎn)生.o文件


編譯過(guò)程.jpg

dyld加載流程分析

根據(jù)dyld源碼,以及l(fā)ibobjc、libSystem、libdispatch源碼協(xié)同分析
在load方法處加一個(gè)斷點(diǎn),通過(guò)bt堆棧信息查看app啟動(dòng)是從哪里開(kāi)始的

+ (void)load{
    NSLog(@"%s",__func__);  //此處加斷點(diǎn)
}


控制臺(tái)數(shù)據(jù)結(jié)果:
/*
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x000000010532ee17 002-應(yīng)用程加載分析`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
    frame #1: 0x00007fff201805e3 libobjc.A.dylib`load_images + 1442
    frame #2: 0x0000000105342e54 dyld_sim`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 425
    frame #3: 0x0000000105351887 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 437
    frame #4: 0x000000010534fbb0 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
    frame #5: 0x000000010534fc50 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
    frame #6: 0x00000001053432a9 dyld_sim`dyld::initializeMainExecutable() + 199
    frame #7: 0x0000000105347d50 dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4431
    frame #8: 0x00000001053421c7 dyld_sim`start_sim + 122
    frame #9: 0x000000010bea485c dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2308
    frame #10: 0x000000010bea24f4 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 837
    frame #11: 0x000000010be9d227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
    frame #12: 0x000000010be9d025 dyld`_dyld_start + 37
(lldb) 
*/

【app啟動(dòng)起點(diǎn)】:通過(guò)程序運(yùn)行發(fā)現(xiàn),是從dyld中的_dyld_start開(kāi)始的,所以需要去OpenSource下載一份dyld的源碼來(lái)進(jìn)行分析

dyld加載流程.png

參考資料:

[iOS-底層原理 15:dyld加載流程](http://www.lxweimin.com/p/db765ff4e36a
[iOS 應(yīng)用程序加載](http://www.lxweimin.com/p/bffb5bdb4f13

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

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