Mach-O探索

Mach-O探索

前言

我們都知道在iOS應用程序中的可執行文件的格式是Mach-O,那么Mach-O到底存儲了哪些數據,又是怎么工作的呢?下面我們來探索一下。

1.Mach-O簡介

維基百科對于Mach-O的描述:

Mach-OMach Object文件格式的縮寫,它是一種用于可執行文件,目標代碼,動態庫,內核轉儲的文件格式。作為a.out格式的替代,Mach-O提供了更強的擴展性,并提升了符號表中信息的訪問速度。

Mach-O曾經為大部分基于Mach核心的操作系統所使用。NeXTSTEPDarwinMac OS X等系統使用這種格式作為其原生可執行文件,庫和目標代碼的格式。而同樣使用GNU Mach作為其微內核的GNU Hurd系統則使用ELF而非Mach-O作為其標準的二進制文件格式。

Mach-O蘋果官方圖片.jpg
  • Header 包含了 Mach-O 文件的基本信息,如 CPU 架構,文件類型,加載指令數量等
  • Load Commands 是跟在 Header 后面的加載命令區,包含文件的組織架構和在虛擬內存中的布局方式,在調用的時候知道如何設置和加載二進制數據
  • Data 包含 Load Commands 中需要的各個 Segment 的數據。

絕大多數 Mach-O 文件包括以下三種 Segment:

  • __TEXT: 代碼段,包括頭文件、代碼和常量。只讀不可修改。
  • __DATA:數據段,包括全局變量, 靜態變量等。可讀可寫。
  • __LINKEDIT: 如何加載程序, 包含了方法和變量的元數據(位置,偏移量),以及代碼簽名等信息。只讀不可修改。

以下內容參考自:WWDC 2016 Optimizing App Startup Time

1.1 Mach-O 的幾種類型

Mach-O的幾種類型.jpg
  • Executable類型

Executable 主程序的二進制文件,就是我們iOS應用程序顯示包內容的MachO文件
可以通過Products->xxx.app->Show in Finder->顯示包內容 查看

  • Dylib 類型

Dylib 動態庫,在其他平臺上也叫DSO或者DLL

  • Bundle 類型

Bundle 不能被連接的Dylib,只能在Runtime運行時通過dlopen函數來加載它,它可以在macOS上用于插件。

  • Image 類型

Image 是一種可執行的二進制文件或者包,包含了上述三種文件類型

  • Framework 類型

Framework其實也是一種dylib, 它周圍有一個特殊的目錄結構來保存該dylib所需的文件。

那么這些都有什么區別和應用呢,請參考我的另一篇文章iOS開發中『庫』的區別應用

1.2 Mach-O結構分析

1.2.1 segment 段

segment.jpg

Mach-O 文件是由 segment 段組成的,分別是TEXT段、DATA段、LINKEDIT段

  • 段的名稱為大寫格式
  • 所有段都是page size的倍數
  • arm64上段的大小為16K
  • 其他架構均為4K

此處實際上是指的虛擬內存的一頁

1.2.2 section

section.jpg

segment段內部還有許多section節,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實際上只是segment段的子范圍,它們沒有頁面大小的限制,也不會重疊在一起。

通過MachOView也可以看出上述的結構:

MachOView.jpg

1.2.3 常見的 segment 與作用

  • __TEXT 代碼段,包括頭文件、代碼和常量以及mach header。 read-only(只讀的)
__TEXT.jpg
  • __DATA數據段,包括全局變量、靜態變量,是可讀可寫的。
__DATA.jpg
  • __LINKEDIT 如何加載程序,包括了方法和變量的元數據(位置、偏移量),以及代碼簽名等信息。只讀不可修改。
__LINKEDIT.jpg

1.2.4 Mach-O Universal Files

Universal Files.jpg

因為有時候我們需要構建多種架構的Mach-O文件,這個時候的做法是通過Mach-O Universal Files來實現的,Xcode會重新生成不同架構的二進制文件,然后合并到一起,簡稱Fat(胖)二進制文件。它通過header來記錄不同架構在文件中的偏移量,segment占多個分頁,header占用一頁空間,那么header占用一頁是不是浪費了很多空間?答案是肯定的,那么為什么還要占用一頁空間呢?所有東西都基于頁面的好處是什么呢?下面我們通過虛擬內存來解釋它。

1.3 virtual memory 虛擬內存

virtual memory.jpg

PS: 軟件工程格言
every problem can be solved by adding a level of indirection.
每個問題都可以通過添加中間層來解決

所以說虛擬內存是通過中間層間接尋址的一種技術

虛擬內存解決是管理所有進程使用物理內存的問題。通過添加間接層來讓每個進程使用邏輯地址空間,它可以映射到RAM上的某個物理頁面上,這種映射不是一對一的,邏輯地址也有可能映射不到RAM上,也有可能有多個邏輯地址映射到同一個物理RAM上。

virtual memory 應用:

  1. 一個邏輯地址不映射任何物理RAM時,進程要訪問該地址時時會觸發page fault頁面錯誤,內核將停止該線程,并試圖找出解決方案,或者通過CPU調度去物理磁盤讀取缺失的內容,或者其他處理
  2. 多個邏輯地址映射到同一物理RAM時,兩個進程共享一樣比特的RAM,通常就是我們說的共享緩存技術,比如說我們的多個APP同時訪問UIKit
  3. 另一個就是文件的映射,不用把整個文件讀入RAM,而是可以調用mmap()函數告訴虛擬內存系統,我想把這部分文件映射到進程里的這段地址,為什么要這樣做呢?不用讀取整個文件,通過設置該映射第一次訪問這些不同的地址時,如果已經在內存里讀過,每次訪問未訪問過的地址時,都會觸發page fault,內核會處理該page fault,時間文件的懶加載
  4. 通過以上的介紹我們可以知道任意一個dylib或者image的TEXT段都可以映射到多個進程中,并且可以實現懶加載,也可以實現進程間共享。
  5. 那么DATA段呢?有一個策略叫寫入時復制,這個和APP的文件系統的克隆很相似,寫入時復制所做的是它積極地在所有進程里共享DATA頁面,只要進程只讀有共享內容的全局變量,但是一旦有進程想要寫入其DATA頁面,寫入時復制就是內核會把該頁面進行復制,放入另一個物理RAM并重定向映射,所以該進程有了該頁面的副本,這把我們帶入了干凈頁面,該副本被認為是臟頁面。臟頁面是指含有進程特定信息,干凈頁面是指內核可以按照需要重新建立頁面,比如重新讀取磁盤,所以臟頁面比干凈頁面要昂貴許多。
  6. 頁面的權限界限,這指的是可以標記一個頁面可讀可寫可執行,或者它們的任何組合。

1.4 virtual memory & Mach-O 之間的映射

首先我們擁有一個Dylib文件,我們還沒有把他讀取到物理內存中,只是先進行了映射。這時候靜態鏈接器會把所有值為0的全局變量都移動到了尾端。

15984323319905.jpg

當我們第一次訪問的時候,虛擬內存會觸發page fault,這個時候內核意識到它被映射到了一個文件,這個時候內核會讀取這個文件將它放入物理RAM設置其映射。

first read.jpg

當我們還需要讀取其他頁面的時候,比如讀取LINKEDITDATA的時候也是同樣的流程。

read other.jpg

但是當我們要在DATA段寫入一些內容的時候,就會觸發寫入時復制,這個時候DATA這個頁面就變為臟頁面了,這個時候我們只有一個臟頁面和兩個干凈的頁面,如果一開始就加載全部,可能就都是臟頁面了。

臟頁面.jpg

此時如果另一個進程也要加載該Dylib,就可以復用RAM1和RAM2,內核只是簡單的把映射重定向,不需要任何IO操作,如果DATA頁面那個RAM3沒有變成臟頁面也可以直接復用,如果變成臟頁面內核會查看RAM3的副本是否在內存中,如果還在就可以治截止使用,如果不在就會重新讀取。

15984334097424.jpg

這就實現了不同進程共享這些Dylib,當進程都不需要使用某一段時比如LINKEDIT,在別的進程需要RAM時,就會將其釋放。

1.5 安全如何影響DYLD

Security.jpg

1.5.1 ASLR

ASLR (Address Space Layout Randomization) 地址空間布局隨機化,鏡像會在隨機的地址上加載。內存偏移量還需要計算ASLR的位置。

1.5.1 Code Signing

在Xcode中 Code Signing是指對整個文件運行一個加密哈希算法,然后對文件進行一個簽名。為了在運行時進行驗證,整個文件都必須要重新讀取,所以在編譯階段,在每個Mach-O文件的每一個頁面都進行自己的加密哈希算法,所有哈希都存儲在LINKEDIT里,這使得你的每個未被修改的頁面,在被讀取的過程中都能得到及時驗證。

1.6 exec()

exec.jpg

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 是一個系統調用,當你進入內核,我想把這個進程換成這個新程序,內核會抹去整個地址,映射指定的可執行程序,ASLR把它映射到一個隨機地址,下一步是從該隨機地址回溯到0地址把整個區域標記為不可訪問,就是不可讀,不可寫,不可執行,該區域在32位處理器下至少4KB大小,64位處理器下至少4GB大小,這樣就可以捕獲任何空指針引用,捕獲任何指針截斷。

2. dyld

2.1 dyld 簡介

dyld.jpg

Unix 誕生初期一切都很簡單,我只需映射一個程序,把指針引用指向它,開始運行即可,后來人們有發明了共享緩存庫,那么誰來加載Dylibs呢?這是件很復雜的事情,人們意識到不能讓內核來做這件事,所以幫助程序就誕生了在Unix平臺人們叫它LD.SO,在iOS上他被叫做DYLD

當內核完成進程的映射,它現在映射到另一個Mach-O文件,調用Dyld進入該進程到另一個隨機地址,把指針引用指向Dyld,讓Dyld完成進程的啟動,Dyld的工作是加載所有依賴的Dylib,讓它們全部準備好,開始運行。

2.2 dyld加載Mach-O流程

2.2.1 加載主流程(時間軸)

dyld加載流程.jpg

2.2.2 Load dylibs

Load dylibs done.jpg
  1. Dyld首先要根據內核映射好的主可執行文件的頭文件,該頭文件里有一個所有依賴的庫的列表,根據這個列表映射所有Dylib
  2. 找到所有Dylib后,確定它是一個MachO文件后,通過代碼簽名對他進行驗證并注冊到內核。
  3. 然后它可以在該Dylib里的每一段調用mmap(),將其讀入內存。
  4. Dyld還會對每個Dylib進行遞歸加載,因為每個不同的Dylib還有可能依賴Dylib(已加載的或者未加載的),直到全部加載完畢。
  5. 其實我們需要加載的Dylib有很多,大約有100400個,但是大部分都是OS Dylib,這里系統為我們做了足夠多的優化,以確保加載速度非常非常的快。

2.2.3 Fix-Ups

現在Dylibs都已經加載完畢了,但是它們都是彼此獨立的,我們下一條把它們綁定在一起。這就是Fix-Ups(修復)。

code sign.jpg

由于代碼簽名的存在,我們無法修改指令,那么dylib該如何調用另一dylib呢?這個時候我們使用code-gen,即動態PIC(Position Independent Code) 地址無關代碼,代碼可以加載到該地址,并且是動態的,也就是說地址間接的被分配,為了一個調用另一個,code-gen實際上在DATA段新建了一個指向被調用者的指針,任何加載該指針并跳轉過去。

Rebasing and Binding.jpg

所以所有的dyld都在修復指針和數據,修復有兩種,一種是重設基址,另一種是綁定重設基址是指如果有一個指針指向Image內,需要作出所有修改;綁定是指 如果指針指向Image范圍外,也需要進行不同的修復。

dyldinfo還有很多選項參數,我們可以在任何二進制文件上運行,就可以看到所有的修復。

fixup.jpg

過去你可以為每一個dylib指定首選加載地址,該首選加載地址是一個靜態指針和dyld一起合作,比如若把它加載到該首選加載地址,所有指針和數據本應該是內部編碼的,都是正確的,那么dyld就不用做任何修復。現在有了ASLR ,dylib被加載到隨機地址上,它偏移到了其他的地址,也就是說所有的指針和數據都依然指向舊地址,所以為了修復它們,我們需要計算偏移值,并且對每一個內部指針都添加該偏移值,所以重設基址就是指遍歷所有內部數據指針,然后為它們添加一個偏移值。概念非常簡單,就是讀取一個指針,添加偏移值,在寫入新值。那么這些數據指針都在哪里呢?這些指針都在LINKEDIT段里存儲著。此時所有的映射都已經結束,當我們開始重設基址的時候實際上所有DATA頁面上都產生了錯誤,然后對頁面進行修改,觸發寫入時復制,所有的重設基址有時會非常昂貴,由于這些都需要I/O操作,但是有一個技巧,就是按順序操作,從內核的角度來看,它認為數據錯誤順序按照產生,當它如此認為時,內核會進行預讀,這樣就會降低很多I/O成本

Rebasing.jpg

2.2.4 Binding

Binding.jpg

綁定是針對那些指向dylib范圍外的指針而言的,這些指針通過名稱就是綁定,實際就是字符串,本例中LINKEDIT段里的malloc,也就是說該數據指針需要指向malloc,所以在運行時dyld需要找到實現該符號的位置,這需要很多的計算,遍歷查找符號表,一旦找到就把值存到該數據指針里,計算復雜度比重設基址高的多。

2.2.5 Notify ObjC Runtime

Notify ObjC Runtime.jpg
  1. Objc有很多DATA結構,DATA結構類,也就是指向方法的指針,以及高光指針,幾乎都已經被修復,通過重設基址或者綁定。
  2. 但是在Objc運行時還需要一些額外的操作,首先Objc是一門動態語言,可以把一個類用名稱實例化,即Objc運行時需要維護一張表,包含所有名稱及其映射的類,每次加載的名稱都將定義一個類,名稱需要登記在一個全局表格里。
  3. 在C++中你可能聽說過脆弱的基類問題,但是在Objc中就不存在該問題,因為我們做的其中一種修復就是,在加載時動態改變所有ivar的偏移量。
  4. 在Objc里可以定義Categories,有時候它們在另一個dylib里,此時那些方法修復必須已經完成。
  5. Objc基于選擇器是唯一的,所以我們需要唯一的選擇器

2.2.6 Initializers

So 我們現在完成了所有所有靜態描述的DATA的修復,現在是進行動態DATA修復的時機。

Initializers.jpg
  1. 在C++中有一個叫做Initializers的初始化器,可以指定你想要的任何表達式,在這里我們可以通過運行初始化器來完成那些抽象表達式的初始化。
  2. 在Objc有一種方法叫+load方法,但是現在+load方法已經不在建議使用(建議使用+initialize),如果使用了它現在將開始運行
  3. 頂端是主可執行文件,所有的dylibs依照這張大圖,必須要運行初始化器從下往上運行,原因是當初始化器運行時可能會調用一些dylib,你需要確保那些dylib已經準備好被調用。從下往上一直到類,可以很安全的調用依賴的內容
  4. 但所有初始化器完成時,我們實際已經最終調用的主Dylib程序

dyld是一個幫助程序:

  1. 可以加載所有的依賴庫
  2. 修復所有DATA頁面
  3. 運行初始化器,跳轉到主函數

2.3 dyld2 && dyld3

詳見WWDC2017 - 413 - App Startup Time: Past, Present, and Future

dyld2 && dyld3.jpg

iOS 13之前,所有APP都是通過dyld2來啟動的,主要過程如下:

  1. 解析MachOHeaderLoad Commands,找到其依賴的庫,并遞歸找到所有依賴的庫
  2. 加載MachO文件
  3. 進行符號查找
  4. 綁定和重設基址
  5. 運行初始化程序

dyld3被分為了三個組件:

  • 一個進程外的MachO解析器
    • 預先處理了所有可能影響啟動速度的Search Path@rpaths和環境變量
    • 開始分析MachOHeader和依賴,并完成了所有符號查找的工作
    • 最后將這些結果創建成了一個啟動包
    • 這是一個普通的 daemon 進程,可以使用通常的測試架構
  • 一個進程內的引擎,用來運行啟動閉包
    • 這部分在進程中處理
    • 驗證啟動閉包的安全性,然后映射到dylib中,在跳轉到main函數
    • 不需要解析Mach-OHeader和依賴,也不需要符號查找
  • 一個啟動閉包緩存服務
    • 系統APP的啟動閉包被構建在一個Shared Cache 中,我們甚至不需要打開一個單獨的文件
    • 對于第三方的APP,我們會在APP安裝或者升級的時候構建這個啟動閉包
    • 在iOS、tvOS、watchOS中,這一切都是APP啟動之前完成的,在macOS上,由于有Side Load App,進程內引擎會在首次啟動的時候啟動一個daemon進程,之后就可以啟動閉包了。
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370