五種 Mach-O 型別的淺要分析

陳超邦發表於2019-03-04

0x01 用 Xcode 生成二進位制檔案

Mach-O_Type_Xcode-c450

Xcode 入手,在 Build Setting -> Mach-O Type 這個選項下,有著這五個型別。如果建立一個 Framework,不手動修改的預設配置即為 Dynamic Library

檔案型別 描述
Executable 可執行二進位制檔案
Dynamic Library 動態庫
Bundle 非獨立二進位制檔案,顯式載入
Static Library 靜態庫
Relocatable Object File 可重定位的目標檔案,中間結果

一般情況下,無論是 SDK 還是開源類庫,首先考慮的是要使用 Dynamic Library(動態庫) 還是 Static Library(靜態庫)。很有人真的考慮過是否要使用另外的三種型別來做。

先問可不可以,再問為什麼。現在就來試試分別使用 Executable, Bundle, Relocatable Object File 這三種型別來製作 Framework,並進行程式碼共享,看看是否具有可操作性。

Executable

建立一個 Framework 專案,更改 Build Setting -> Mach-O Type 為 Executable。CMD + B 編譯專案。可以看到如下兩個錯誤。

clang: error: invalid argument `-compatibility_version 1` only allowed with `-dynamiclib`
複製程式碼

前面說了,建立 framework 型別的專案,預設的 Mach-O 型別為 Dynamic Library,既 動態庫。提示明確說明了,-compatibility_version 1 這個修飾屬性只適用於 dynamiclib 型別的檔案,在 Building Setting 裡面搜尋 compatibility_version ,刪除預設值即可。

clang: error: invalid argument `-current_version 1` only allowed with `-dynamiclib`
複製程式碼

同上說明

根據提示去除預設的 -compatibility_version-current_version 兩個配置,繼續編譯,還是出現瞭如下錯誤。

ld: entry point (_main) undefined. for architecture x86_64
複製程式碼

注意:因為是使用的模擬器進行編譯,所以是 x86_64 架構。

對於 Executable 型別,這裡基本是終點了。可執行檔案會直接被系統執行,所以需要一個 _main 執行入口。

Bundle

建立一個 Framework 專案,更改 BuildSetting -> Mach-O Type 為 Bundle。同樣修改了 -compatibility_version-current_version 兩個配置之後,CMD + B 編譯成功。

當然,這樣還是不知道生成的 Framework 是否可以使用。所以我們建立對應的 Target 來進行測試。

建立 Demo 並將生成的 Framework 嵌入進應用後,CMD + B 出現如下的錯誤:

ld: can`t link with bundle (MH_BUNDLE) only dylibs (MH_DYLIB) file `/Users/cbangchen/Library/Developer/Xcode/DerivedData/SDK_Mach_O_Type_Bundle_Demo-gzjgmzohehdzvvbknavovstdtyfm/Build/Products/Debug-iphonesimulator/SDK_Mach_O_Type_Bundle_Demo.framework/SDK_Mach_O_Type_Bundle_Demo` for architecture x86_64
複製程式碼

提示明確的說明了,系統不支援連結 MH_BUNDLE 型別的檔案。

這樣就結束了嗎?不如掙扎一下。

回頭看看上面那個表格,對應 Bundle 型別的描述寫的是 —— 非獨立二進位制檔案,顯式載入。既然是 Bundle 檔案,就來試著用資源載入的方式 load 一下。

首先將 Bundle 用資源載入的方式(Build Phases -> Copy Bundle Resources)進行新增。然後在 VC 裡面寫入下面的語句。

NSString *bundleString = [[NSBundle mainBundle] pathForResource:@"SDK_Mach_O_Type_Bundle_Demo" ofType:@"framework"];
NSBundle *SDKBundle = [NSBundle bundleWithPath:bundleString];
複製程式碼

使用上面的語句獲取到 Bundle 檔案。好像還缺了一點東西。

[SDKBundle load];
複製程式碼

把它給 load 掉。再用 Runtime 來看看有沒有 load 成功。

Class justForTestClass = NSClassFromString(@"JustForTest");
[justForTestClass performSelector:@selector(justForTestMethod)];
複製程式碼
SDK_Mach_O_Type_Bundle_Log

輸出了預設的語句。還是跑起來了。從這裡開始就變得有點意思了,接著來看下一種型別。

Relocatable Object File

建立一個 Framework 專案,更改 BuildSetting -> Mach-O Type 為 Relocatable Object File。同樣修改了 -compatibility_version-current_version 兩個配置之後。CMD + B 編譯出現如下錯誤:

ld: -r and -dead_strip cannot be used together
複製程式碼

移除 Dead Code Stripping,CMD + B,如下:

-Dead Code Stripping 這個屬性用於刪除檔案中不需要載入的符號,減小二進位制檔案大小。

d: -rpath can only be used when creating a dynamic final linked image
複製程式碼

-rpath 這個符號用來連結其他依賴的二進位制檔案,Relocatable Object File 類似於中間檔案,不能帶有依賴關係。

-rpath 移除,CMD + B 編譯成功。開一個 Target 測試一下,也可以正常連結使用。

0x04 五種二進位制檔案的結構

Executable

格式並不是決定一個檔案是否是可執行檔案的原因,在 Windows 作業系統下,可執行程式可以是 .exe 檔案, .sys 檔案或者 .com 等型別的檔案。可以簡單的理解為是作業系統允許的,可以單獨執行的檔案

當然,前面我們也說了 Executable 需要一個執行入口,這也導致了這種型別只適合用來製作程式本身。

Dynamic Library

動態庫對於 iOS 系統的構成有著極其重要的意義。把共同的程式碼抽離出來,保證系統執行過程中,相同內容的庫只被載入一次。解耦重用就是動態庫出現的直接理由。

注意
@ValiantCat 同學指正。新增補充如下:
只有相同簽名的動態庫,才可以進行系統級別的共享。

而為了保證這一點,動態庫生成的二進位制檔案並沒有被合併到可執行二進位制檔案中。

否則。

SDK_Mach_O_Type_ Redundant_Library-c450

試想,幾乎每一個應用都引用到了 FoundationUIKit 兩個類庫,來構建介面或者進行基本的資料處理,如果每一個應用都嵌入了這樣的兩個庫,那整個系統中就有多餘的 2(n – 1) 個庫了。比較浪費資源。

再來看看這個動態庫的結構:

Mach-O_Type_Library_Dynamic_MachO_Str_PNG-c450

左邊的一些欄位,和動態庫的使用有著較密切的關係。例如 LC_LOAD_DYLIB 的幾個欄位。描述的是,這個動態庫所依賴的其他類庫。使用 MachOView 開啟其中一行,可以看到下面的資訊。

-c450

主要的 Name 欄位指示了依賴庫的連結位置。在載入目標動態庫的時候就會到連結位置,對依賴庫進行載入。另外同時有一個欄位,在功能上和 LC_LOAD_DYLIB 十分相似,就是 LC_RPATH,這個欄位開啟來看是這樣的。

-c450

path 這個欄位有一個連結地址。同樣是依賴庫載入的路徑,和 LC_LOAD_DYLIB 不同的是,path 目錄下的依賴庫並不原本儲存於系統,它們之間的依賴關係是開發者主動新增的。

動態庫的結構裡面,有相當一部分用來記錄和其他類庫之間的依賴關係,由動態庫的特性決定。

Bundle

Bundle 的結構和 Dynamic Library 的結構十分相似。

Mach-O_Type_Library_Dynamic_Bundle_MachO_Different_PNG-c450

看起來幾乎是一樣的。

但其實一樣嗎?至少有一樣東西是不一樣的。我們使用型別為 Dynamic Library 這種型別的二進位制檔案,需要在 Xcode 中進行嵌入,直接使用 Bundle 則要求手動載入。而蘋果已經明確禁止在程式執行的時候手動載入可執行程式碼,這種行為,是具有絕對的危險的,我們一定要規避這樣的風險。

Static Library && Relocatable Object File

Mach-O_Type_Library_Static_ROF_MachO_Different_PNG-c450

這張圖比較寡淡,只可以看出 JustForTest.o 的結構和 Relocatable Object File 的整個很像。

當然,那是因為 JustForTest.o 的內容就是 Relocatable Object File 中的內容了,.o 檔案經過進一步的 ar 操作可以歸檔成靜態庫檔案。

Mach-O_Type_Merge_PNG-c450

可以簡單的理解 Relocatable Object File 是組裝靜態庫和動態庫的零件,而靜態庫和動態庫就是可執行二進位制檔案的元件。這裡用了零件和元件的概念,零件是不可缺少的,元件則是可選的。正好形容 .o 檔案,靜動態庫和可執行二進位制檔案之間的關係。

0x03 Static Library,Relocatable Object File 以及 Dynamic Library 的對比

BundleExecutable 都因為自身的侷限性而不適合用來製作包括 SDK 和開源類庫。這裡不深入來講。而Dynamic Library 相對 Relocatable Object File 以及 Static Library 來說,區別是十分明顯的。

在使用上,Dynamic Library 更靈活;複用性更強;且就安全來說,統一放置在 Payload/Framework 目錄下的自建的動態庫,不參與應用的加殼操作,安全性稍遜一籌。而 Relocatable Object File 以及 Static Library 都是在編譯後直接合併到最後的可執行檔案中的,缺點相對不夠靈活,但安全性稍強。

值得注意的是,動態庫是應用在啟動的時候進行載入的,所以會適當減慢啟動速度。這一點,很值得考慮,權衡。

那如果要偏向靜態的方案,應該選擇 Relocatable Object File 還是 Static Library ?兩種型別的檔案在使用上沒有區別,但在大小上,則稍有差距。來做個實驗。

// .h
+ (void)justForTestMethod;

// .m
+ (void)justForTestMethod {
    NSLog(@"? 淺談 SDK 開發");
}
複製程式碼

內容設定為上面的幾行程式碼。分別製作 Relocatable Object FileStatic Library 型別的 Framework,架構選擇為 x86_64

檔案型別 二進位制包的大小 檔案描述截圖
Static Library 21KB
Mach-O_Type_Library_Static_Size
Relocatable Object File 18KB
Mach-O_Type_Library_ROF_Size

可以看到有較小的差別。可能是因為我們用來編譯生成的檔案內容也很少的原因。稍微新增多一些程式碼,用 AFNetworking 來做個例子,原始檔有 273KB。

檔案型別 二進位制包的大小 不容置疑的證據
Static Library 780KB
五種 Mach-O 型別的淺要分析
Relocatable Object File 601KB
五種 Mach-O 型別的淺要分析

這一次的差距就稍微大了一些。而這樣的檔案大小差距,也會隨著程式碼量的增多而越發明顯。如果有縮小應用體積的需求,更改 Mach-O 型別是一種可行的方案。

0x06 結論

如果使用動態庫,需要考慮的是:1. 對於啟動速度的影響。2. 對於保密要求高的線下渠道 SDK,可能會被從 .app/ 中單獨拿出來,反編譯研究具體實現。靜態庫則比較安全一點。而如果要使用靜態庫,建議使用 Relocatable Object File 來減少二進位制檔案的大小。

最後,還要說的是,最好是同時採取 Dynamic Library 和 Relocatable Object File 兩種方式來進行程式碼分發。提供多樣的選擇,滿足不同的需求。

完。

相關文章