一、前言
蘋果在 WWDC2015 大會上引入了 bitcode,隨后在 Xcode7 中添加了在二進制中嵌入 bitcode(Enable Bitcode) 的功能,並且默認設置為開啟狀態。
在 What is app thinning? (iOS, tvOS, watchOS) 一節中有以下定義:
Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the store.
bitcode 是被編譯程序的一種中間形式的代碼。包含 bitcode 配置的程序將會在 iTunes Connect 上被編譯和鏈接。bitcode 允許蘋果在后期重新優化我們程序的二進制文件,而不需要我們重新提交一個新的版本到 store 上。
Bitcode. When you archive for submission to the App Store, Xcode will compile your app into an intermediate representation. The App Store will then compile the bitcode down into the 64 or 32 bit executables as necessary.
當提交程序到 App Store 上時,Xcode 會將程序編譯為一個中間表現形式(bitcode)。然后 App Store 會再將這個 bitcode 編譯為可執行的 64 位或 32 位程序。
來個直觀圖就可以明白了:
這樣可以減少包的大小。
二、什么是 Bitcode
Bitcode 是由 LLVM 引入的一種中間代碼(Intermediate Representation,簡稱 IR),它是源代碼被編譯為二進制機器碼過程中的中間表示形態,它既不是源代碼,也不是機器碼。從代碼組織結構上看它比較接近機器碼,但是在函數和指令層面使用了很多高級語言的特性。
LLVM 是一套優秀的編譯器框架,目前 NDK/Xcode 均采用 LLVM 作為默認的編譯器。LLVM 的編譯過程可以簡單分為 3 個部分:
- 前端(Frontend)負責把各種類型的源代碼編譯為中間表示,也就是 Bitcode。在 LLVM 體系內,不同的語言有不同的編譯器前端,最常見的如 clang 負責 c/c++/oc 的編譯,flang 負責 fortran 的編譯,swiftc 負責 swift 的編譯等等;
- 優化(Optimizer)負責對 Bitcode 進行各種類型的優化,將 bitcode 代碼進行一些邏輯等價的轉換,使得代碼的執行效率更高,體積更小,比如 DeadStrip/SimplifyCFG。
- 后端(Backend)也叫 CodeGenerator,負責把優化后的 bitcode 編譯為指定目標架構的機器碼,比如 X86Backend 負責把 bitcode 編譯為 x86 指令集的機器碼。
在這個體系中,不同語言的源代碼將會被轉化為統一的 bitcode 格式,三個模塊可以充分復用,防止重復造輪子。如果要開發一門新的語言 x,只需要造一個 x 語言的前端,將 x 語言的源代碼編譯為 bitcode,優化和后端的事情完全不用管。同理,如果新的芯片架構問世,則只需要基於 LLVM 重新寫一套目標平台的后端,非常方便。
三、Bitcode 配置
You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64
要么讓所有引入的第三方庫都支持 bitcode,要么關閉 target 的 bitcode 選項。
在最新的 Xcode 中,新建項目默認就打開了 bitcode 設置,這導致在不知情的情況下出現項目編譯失敗,而這些因為 bitcode 而編譯失敗的的項目都鏈接了第三方二進制的庫或者框架(.a、.framework),而編譯失敗的原因就是,這些框架或者庫恰好沒有設置 bitcode。所以每當遇到這個情況時候大部分人都是直接設置 Xcode 關閉 bitcode 功能,全部不生成 bitcode。
平台 | bitcode |
---|---|
iOS | 可選的 |
watchOS | 必須的 |
Mac OS | 不支持 |
四、蘋果的要求
為什么蘋果默認要求 watchOS 和 tvOS 的 App 要上傳 bitcode?
因為把 bitcode 上傳到蘋果的中心服務器后,它可以為目標安裝 App 的設備進行優化二進制,減小安裝包的下載大小。當然 iOS 開發者也可以上傳多個版本而不是打包到單個包里,但這樣會占用更多的存儲空間。最重要的是允許蘋果可以在后台服務器對應用程序進行簽名,而不用導出任何密鑰到終端開發者那。
上傳到服務器的 bitcode 給蘋果帶來更好處是:以后新設計了新指令集的新 CPU,可以繼續從這份 bitcode 開始編譯出新 CPU 上執行的可執行文件,以供用戶下載安裝。
bitcode 給開發者帶來的不便之處:沒用 bitcode 之前,當應用程序崩潰后,開發者可以根據獲取的的崩潰日志再配上上傳到蘋果服務器的二進制文件的調試符號表信息,還原程序運行過程到崩潰時后調用棧信息,對問題進行定位排查。但用了 bitcode 之后,用戶安裝的二進制不是開發者這邊生成的,而是蘋果服務器經過優化后生成的,其對應的調試符號信息丟失了,也就無法進行前面說的還原崩潰現場找原因了。
打開 BitCode 時,在iTunes Connect 里"我的App"->項目->活動->所有構建版本->具體版本的"綜合信息"下"包含符號"那里可以下載到 dSYM 文件。(待驗證)
相當於在編譯的時候加一個標記:embed-bitcode-marker(調試構建) embed-bitcode(打包/真機構建)。這個在 clang 編譯器的參數是 -fembed-bitcode,swift 編譯器的參數是 -embed-bitcode。
五、拓展知識
目前蘋果采用的編譯器工具鏈是 LLVM,Bitcode 是 LLVM 編譯器的中間代碼的一種編碼。LLVM 的前端可以理解為 C/C++/OC/Swift 等編程語言,后端可以理解為各個芯片平台上的匯編指令或者可執行機器指令數據,那么 BitCode 就是位於這兩者之間的中間碼。
LLVM 的編譯工作原理是前端負責把項目程序源代碼翻譯成 Bitcode 中間碼,然后再根據不同目標機器芯片平台轉換為相應的匯編指令以及翻譯為機器碼。這樣設計就可以讓 LLVM 成為了一個編譯器架構,可以輕而易舉的在 LLVM 架構之上發明新的語言(前端),以及在 LLVM 架構下面支持新的 CPU(后端)指令輸出。雖然 Bitcode 僅僅只是一個中間碼,不能在任何平台上運行,但是它可以轉化為任何被支持的 CPU 架構,包括還未被發明的 CPU 架構,也就是說現在打開 Bitcode 功能提交一個 App 到應用商店,如果以后蘋果使用全新設計的 CPU,在蘋果后台服務器一樣可以從這個 App 的 Bitcode 開始編譯轉化為新 CPU 上的可執行程序,可供新手機用戶下載運行這個 App。
在 iPhone 出來之前,蘋果主要的編譯器技術是用經過稍微改進的 GCC 工具鏈來把 Objective-C 語言編寫的代碼編譯出所指定的機器處理器上原生的可執行程序。編譯器產生的可執行程序叫做"Fat Binaries"--類似於 Windows 中 PE 格式的 exe 和 Linux 下的 ELF 格式的二進制。
區別是,一個"Fat Binary"可以包含同一個程序的很多版本,所以同一個可執行文件可以在不同的處理器上運行。主要就是這個技術讓蘋果的硬件很容易的從 PowerPC 遷移到 PowerPC64 的處理器,以及后來再遷移到 Intel 和 Intel64 處理器。這個方案帶來的負面影響就是同一個文件中存了多份可執行代碼,除了當前機器可執行的那一份之外其他都是無用的,這個在市場上被稱為"Universal Binary"。在蘋果從 PowerPC 遷移到 Intel 處理器的事情開始存在的(一個二進制文件包含一份 PowerPC 版本和一份 Intel 版本)。慢慢的后來又支持同時包含 Intel 32bit 和 Intel 64bit。
在一個 Fat binary 中,有操作系統運行時根據處理器類型動態選擇正確的二進制版本來運行,但是應用程序要支持不同平台的處理器的話,應用程序本身要多占用一些空間。當然也有一些瘦身的工具,比如 lipo,可以用來移除那些當前機器中不被支持的或者多余的可執行代碼達到瘦身目的,lipo不會改變程序執行邏輯,僅僅只是文件的大小瘦身。
隨着移動設備移動互聯網的深入發展,程序大小變得越來越重要了。主要是因為移動設備中不會有電腦上那么大的硬盤驅動器,還有就是蘋果早就從原始的 ARM 處理器遷移到自家設計的 A4、A5、A5X、A6、A7、A8、A8X、A9、A9X 以及后續的 A10 處理器,它們的指令集已經發生了改變和原始 ARM 設計的有所區別,iOS 操作系統底層以及 Xcode/LLVM 編譯工具一定程度上透明了這些變化,編譯出來的程序會包含很多執行代碼版本。面對這個問題,蘋果投入大量成本遷移到 LLVM 編譯器架構並使用 bitcode 的必要性越來越大。從最開始的把 OPENGL 編譯為特定的 GPU 指令到把 Clang 編譯器(LLCM的C/OC編譯前端)支持 Objective-C 的改進並作為 Xcode 的默認編譯器。
LLVM 提供了一個虛擬指令集機制,它可以翻譯出指定的所支持的處理器架構的執行代碼(機器碼)。這個就使得為 iOS 應用程序的編譯開發一個完全基於 LLVM 架構的工具鏈成為可能。而 LLVM 的這個虛擬的通用的指令集可以用很多種表示格式:
- 叫做 IR 的文本表示的匯編格式(像匯編語言)
- 轉換為二進制數據表示的格式(像目標代碼),這個二進制格式就是我們所說的 bitcode。
Bitcode 和傳統的可執行指令集不同,它維護的是函數功能的類型和簽名。比如,傳統可執行指令集中,一系列(<=8)的布爾值可以壓縮存儲到單個字節中,但是在 bitcode 中它們是各自獨自表示的;此外,邏輯運算操作(比如寄存器清零操作)也有它們對應的邏輯表示方法($R=0),當這些 BitCode 要轉換為特定機器平台的指令集時,它可以用經過針對特定機器平台優化過的匯編指令來代替:xor eax, eax。(這個匯編指令同樣是寄存器
然而 bitcode 也不是完全獨立於處理器平台和調用約定的,寄存器的大小在指令集中是一個相當重要的特性。眾所周知,64bit 寄存器可以比 32bit 寄存器存儲更多的數據,生成 64bit 平台的 bitcode 和 32bit 平台的是明顯不同的;還有,調用約定可以根據函數定義或者函數調用來定義,這些可以確定函數的參數傳遞是傳寄存器值還是壓棧。一些編程語言還有如 sizeof(long) 這樣的預處理指令,這些將在 bitcode 生成之前前被翻譯。一般情況下,對於支持 fastcc(fast calling convention)調用的 64bit 平台會生成與其一致的 bitcode 代碼。
六、初探
既然 bitcode 是代碼的一種表示形式,因此它也會有自己的一套獨立的語法,可以通過一個簡單的例子來一探究竟,這里以 clang 為例,swift 的操作和結果可能稍有不同。
先編寫一段 c 語言代碼(test.c):
#include <stdio.h> int main(void)
{
printf("hello, world.\\n");
return 0;
}通過以下命令可以將源代碼編譯為 object 文件。
$ clang -c test.c -o test.o $ file test.o
test.o: Mach-O 64-bit object x86_64這個命令同時完成了前端、優化、后端,可以通過 -emit-llvm -c 將前端這一步單獨拆出來,這樣就可以看到 bitcode 了。
$ clang -emit-llvm -c test.c -o test.bc # 將源代碼編譯為 bitcode $ clang -c test.bc -o test.bc.o # 將 bitcode 編譯為 object
$ clang -emit-llvm -c test.c -o test.bc
$ file test.bc
test.bc: LLVM bitcode, wrapper x86_64
$ clang -c test.bc -o test.bc.o
$ file test.bc.o
test.bc.o: Mach-O 64-bit object x86_64
$ md5 test.bc.o test.o
MD5 (test.bc.o) = 9b90026b9c1d3fa0211e106ff921e9bd
MD5 (test.o) = 9b90026b9c1d3fa0211e106ff921e9bdbitcode 文件使用后綴名 .bc 表示。可以看到,將 bitcode 文件作為 clang 的輸入,編出的 object 文件跟直接編源代碼是相同的。
查看 bitcode 文件。
$ hexdump -C test.bc | head 00000000 de c0 17 0b 00 00 00 00 14 00 00 00 90 09 00 00 |................|
00000010 07 00 00 01 42 43 c0 de 35 14 00 00 07 00 00 00 |....BC..5.......|
00000020 62 0c 30 24 94 96 a6 a5 f7 d7 7f 4f d3 3e ed df |b.0$.......O.>..|
00000030 bd 6f ff b4 10 05 c8 14 00 00 00 00 21 0c 00 00 |.o..........!...|
00000040 58 02 00 00 0b 82 20 00 02 00 00 00 13 00 00 00 |X..... .........|
00000050 07 81 23 91 41 c8 04 49 06 10 32 39 92 01 84 0c |..#.A..I..29....|
00000060 25 05 08 19 1e 04 8b 62 80 10 45 02 42 92 0b 42 |%......b..E.B..B|
00000070 84 10 32 14 38 08 18 4b 0a 32 42 88 48 90 14 20 |..2.8..K.2B.H.. |
00000080 43 46 88 a5 00 19 32 42 e4 48 0e 90 11 22 c4 50 |CF....2B.H...".P|
00000090 41 51 81 8c e1 83 e5 8a 04 21 46 06 51 18 00 00 |AQ.......!F.Q...|通過 hexdump 可以看出它並非文本文件,全是亂碼,這樣的文件是很難分析的。其實 LLVM 提供了 llvm-dis/llvm-as 兩個工具,用於將 bitcode 在二進制格式和可讀的文本格式之間進行相互的轉化,但遺憾的是 Xcode 的編譯器工具鏈中並沒有附帶這個命令,因此需要另尋他法。
我們知道通過編譯器的 -S 參數可以將源代碼編譯為文本的 assembly 代碼,不進行最后一步 assembly 到機器碼的翻譯工作,而 assembly 和機器碼是等價的兩種表示形式,bitcode 同樣也是有文本和二進制(bitcode)兩種等價表示形式,clang 也為 bitcode 保留了這一特性,可以通過 -emit-llvm -S 將源代碼編譯為文本格式的 bitcode, 也叫做 LLVM Assembly Language,一般后綴名使用 .ll。
$ clang -emit-llvm -S test.c -o test.ll # 將源代碼編譯為 LLVM Assembly
test.ll 可用文本編輯器打開,全部內容:
; ModuleID = 'test.c' source_filename = "test.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.13.0"
@.str = private unnamed_addr constant \[15 x i8\] c"hello, world.\\0A\\00", align 1
; Function Attrs: noinline nounwind ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds (\[15 x i8\], \[15 x i8\]* @.str, i32 0, i32 0))
ret i32 0
}
declare i32 @printf(i8*, ...) #1
attributes #0 = { noinline nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"Apple LLVM version 9.0.0 (clang-900.0.39.2)"}這樣看上去就很清晰明了了,我們重點關注下函數定義這部分,加了一些注釋方便理解。
; 定義全局常量 @.str, 內容初始化為 'hello, world.\\n\\0' @.str = private unnamed_addr constant \[15 x i8\] c"hello, world.\\0A\\00", align 1
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 { ; 定義函數 @main,返回值為i32類型
%1 = alloca i32, align 4 ; 聲明變量 %1 = 分配i32的內存空間
store i32 0, i32* %1, align 4 ; 將 0 存入 %1 的內存空間
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds (\[15 x i8\], \[15 x i8\]* @.str, i32 0, i32 0)) ; 調用 @printf 函數,並將 @.str 的地址作為參數
ret i32 0 ; 返回 0
}
declare i32 @printf(i8*, ...) #1 ; 聲明一個外部函數 @printf這段代碼不難閱讀, 其含義和邏輯與我們所寫的源代碼基本一致,只是用了另外一種語法表示出來。因為沒有經過優化,函數中的前兩條語句其實是多余的,這在之后的優化階段會被消除(dead_strip)。bitcode 的具體語法在此不做展開,雖然這個例子看起來非常簡單易懂,但真實場景中,bitcode 的語法遠比這個復雜,有興趣的同學可以直接閱讀 LLVM Language Reference Manual。
七、代碼驗證
在 Objective-C 代碼中實現 c 方法
@implementation Test void Greeting(void)
{
NSLog(@"hello world!");
}
@end
@implementation Demo
void Demo(void)
{
NSLog(@"demo func!");
}
@end用 Clang 編譯成 ARM64 格式且帶 bitcode 的目標文件 Test.o Demo.o。
xcrun -sdk iphoneos clang -arch arm64 -fembed-bitcode -c Test.m Demo.m
把兩個目標文件打包為一個靜態庫文件。
xcrun -sdk iphoneos ar-r libTest.a Test.o Demo.o
用 Shell 命令 otool 查看靜態庫文件是否包含 bitcode 段。
otool -l Test.o | grep bitcode 或 otool -l libTest.a | grep bitcode
如果輸出了 2 行 sectname __bitcode,就是說明這個靜態庫中的兩個目標文件包含了 bitcode。
用下面的命令把 Demo.m 的 C 代碼轉換為 ARM64 匯編語言格式 demo.s。
xcrun -sdk iphoneos clang -arch arm64 -S Demo.m
刪除 Demo.m,僅留下 Test.m 和 Demo.s。
rm Demo.m 或者手動在目錄中刪除
把 Test.m 和 Dmeo.s 這個匯編源代碼來一起帶着 -fembed-bitcode 參數來生成目標代碼並打包為一個靜態庫。
xcrun -sdk iphoneos clang -arch arm64 -fembed-bitcode -c Test.m Demo.s xcrun -sdk iphoneos ar -r libTest.a Test.o Demo.o
再運行 otool 工具來檢查這個新的靜態庫中包含的 2 個目標文件是否都帶有 bitcode 段。
otool -l libTest.a | grep bitcode
從上可以看到,包含 Test.o、Demo.o 的靜態庫有兩行 sectname __bitcode 輸出,包含 Test.o、Demo.s 的靜態庫只有一行輸出。這就說明從 ARM64 匯編語言編譯過來的目標文件 Demo.s 不帶有 bitcode 段,哪怕在編譯的時候指定了參數 -fembed-bitcode 也沒有用。
結論:
bitcode 的生成是由匯編語言以上的上層語言編譯而來,它是上層語言與匯編語言(機器語言)之間的一個中間碼。
目前日常的 iOS 應用開發中,一般不會需要用到匯編層面去優化的代碼,所以我們主要關注第三方(開源)C代碼,尤其是音視頻編碼解碼這些計算密集型項目代碼,關鍵計算的代碼針對特定平台都有對應平台的匯編版本實現,當然也有 C 的實現,但是默認編譯一般都是用的匯編版本,這樣就會導致我們在編譯這個開源代碼的時候哪怕你帶了 -fembed-bitcode 參數也僅僅只是讓項目中的部分 C 代碼的目標文件帶了 bitcode 段,而那少數的匯編代碼的目標文件一樣不帶 bitcode 段,這樣編譯出這個庫交給上層開發者使用的時候,就會出現在打包上傳或者真機調試的時候因為 Xcode 默認開了 bitcode 功能而鏈接失敗,導致不能真機調試或者不能上傳應用到 AppStore。(需要再研究)
.s 也是支持 -fembed-bitcode 的,只是並非真正帶了 bitcode(通過 .s 無法編譯出 bitcode),只是在 .o 里做了標記以兼容 bitcode 模式。
八、Enable Bitcode
在對 bitcode 有了一個直觀的認識之后,再來看一下 Apple 圍繞 bitcode 做了什么。Xcode 中對 Enable Bitcode 這個配置的解釋是 Xcode Help:
Enable Bitcode (ENABLE_BITCODE)
Activating this setting indicates that the target or project should generate bitcode during compilation for platforms and architectures that support it. For Archive builds, bitcode will be generated in the linked binary for submission to the App Store. For other builds, the compiler and linker will check whether the code complies with the requirements for bitcode generation, but will not generate actual bitcode.
具體展開一下:
開啟此設置將會在支持的平台和架構中開啟 bitcode
- 當前支持的平台主要是 iPhoneOS(armv7/arm64),watchOS 等;
- 注意不包括 iPhoneSimulator(i386/x86_64) 和 macos,也就是說模擬器架構下不會編出 bitcode。這個限制只是 Xcode 自身的限制,並非編譯器的限制,我們使用編譯器提供的命令行工具自行操作仍然可以編譯出這些架構下的bitcode,本文中的示例就是基於 macos 平台/x86_64 架構。
進行 Archive 時,bitcode 會被嵌入到鏈接后的二進制文件中,用於提交給 App Store
- Enable Bitcode 設置為 YES 時,從編譯日志中可以看出,Archive 時多了一個編譯參數 -fembed-bitcode
進行其他類型的 Build(非 Archive)時,編譯器只會檢查是否滿足開啟 bitcode 的條件,但並不會真正生成 bitcode
- 非 Archive 編譯時,Enable Bitcode 將會增加編譯參數 -fembed-bitcode-marker, 只是在 object 文件中做了標記,表明可以有 bitcode,但是現在暫時沒有帶上它。因為本地編譯調試時並不需要 bitcode,只有 AppStore 需要這玩意兒,去掉這個不必要的步驟,會加快編譯速度。
- 這就是為什么有的同學在開發 SDK 時,明明開啟了 Enable Bitcode,交給客戶后客戶卻說:你的 sdk 里沒有bitcode,因為你沒有使用 Archive 方式打包。
- 當然,你可以將 Enable Bitcode 設置為 NO, 然后在 Other Compiler Flags 和 Other Linker Flags 中手動為真機架構添加 -fembed-bitcode 參數,這樣任何類型的 Build 都會帶上 bitcode。
接下來看一下 Enable Bitcode 之后,編譯出的文件發生了什么變化, 直接在 clang 的參數中添加 -fembed-bitcode 即可。
$ clang -fembed-bitcode -c test.c -o test_bitcode.o
編譯之后可以通過 otool 工具查看 object 文件的結構,此時你需要對 Mach-O 文件有一些基本的了解。
otool -l test_bitcode.o
test_bitcode.o:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x00 1 4 672 0x00002000
Load command 0
cmd LC\_SEGMENT\_64
cmdsize 552
segname
vmaddr 0x0000000000000000
vmsize 0x0000000000000a88
fileoff 704
filesize 2696
maxprot 0x00000007
initprot 0x00000007
nsects 6
flags 0x0
Section
sectname __bitcode
segname __LLVM
addr 0x0000000000000040
size 0x00000000000009a0
offset 768
align 2^4 (16)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Section
sectname __cmdline
segname __LLVM
addr 0x00000000000009e0
size 0x0000000000000042
offset 3232
align 2^4 (16)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Load command 1
cmd LC\_VERSION\_MIN_MACOSX
cmdsize 16
version 10.13
sdk n/a
Load command 2
cmd LC_SYMTAB
cmdsize 24
symoff 3424
nsyms 4
stroff 3488
strsize 56
Load command 3
cmd LC_DYSYMTAB
cmdsize 80
ilocalsym 0
nlocalsym 2
iextdefsym 2
nextdefsym 1
iundefsym 3
nundefsym 1
tocoff 0
ntoc 0
modtaboff 0
nmodtab 0
extrefsymoff 0
nextrefsyms 0
indirectsymoff 0
nindirectsyms 0
extreloff 0
nextrel 0
locreloff 0
nlocrel 0
或者使用 MachOView 軟件。
可以發現生成的 object 文件中多了兩個 Section,分別是 __LLVM,__bitcode 和 __LLVM,__cmdline,並且 otool 的輸出中給出了這兩個 section 在 object 文件中的偏移和大小,通過 dd 命令可以很方便地將這兩個 Section 提取出來(待驗證)
$ dd bs=1 skip=768 count=0x00000000000009a0 if=test_bitcode.o of=test_bitcode.o.bc
$ dd bs=1 skip=3608 count=0x0000000000000042 if=test_bitcode.o of=test_bitcode.o.cmdline
還有一種更便捷的方式,Xcode 提供的 segedit 命令可以直接將指定的 Section 導出,只需要給定 Section 的名字,和上面的命令效果是一樣的,並且更為方便。
$ segedit -extract __LLVM __bitcode test_bitcode.o.bc -extract __LLVM __cmdline test_bitcode.o.cmdline test_bitcode.o
$ segedit -extract __LLVM __bitcode test_bitcode.o.bc
> -extract __LLVM __cmdline test_bitcode.o.cmdline
> test_bitcode.o
觀察導出的文件:
$ file test_bitcode.o.bc
test_bitcode.o.bc: LLVM bitcode, wrapper x86_64
$ cat test_bitcode.o.cmdline | tr '0' ' '
-triple x86_64-apple-macosx10.13.0 -emit-obj -disable-llvm-passes
$ md5 test.bc test_bitcode.o.bc
MD5 (test.bc) = 718d88a109ba9e1a75119b04eac566f8
MD5 (test_bitcode.o.bc) = 1b3bd72eb7f380cfd6c6528674d90828
不難得出結論:
- object 文件中嵌入的 __LLVM,__bitcode 正是完整的,未經任何加密或者壓縮的 bitcode 文件,通過 -fembed-bitcode 參數,clang 把對應的 bitcode 文件整個嵌入到了 object 文件中。
- __LLVM,__cmdline 是編譯這個文件所用到的參數,如果要通過導出的 bitcode 重新編譯這個 object 文件,必須帶上這些參數
- 導出的參數是 cc1 也就是 clang 中真正"前端"部分的參數(clang 命令其實是整合了各個環節,所以 clang 一個命令可以從源代碼編出可執行文件),所以編譯時要帶上 -cc1
- 導出的 bitcode 文件似乎和直接編譯的 bitcode 不一樣,先留個疑問,后面再研究。
首先, 來測試一下導出的 bitcode 文件結合 cmdline 能否編譯出正常的 object:
$ clang -cc1 -triple x86_64-apple-macosx10.14.0 -emit-obj -disable-llvm-passes test_bitcode.o.bc -o test_rebuild.o
warning: overriding the module target triple with x86_64-apple-macosx10.14.0
1 warning generated.
$ file test_rebuild.o
test_rebuild.o: Mach-O 64-bit object x86_64
$ md5 test.o test_rebuild.o
MD5 (test.o) = 9b90026b9c1d3fa0211e106ff921e9bd
MD5 (test_rebuild.o) = d647be2f0a5cd4ff96b815aef8af5943
沒有任何問題,並且通過內嵌的 bitcode 編譯出的 object 文件與直接從源代碼編譯出來的 object 完全一樣!
回到遺留的問題:為什么導出的 bitcode 文件和直接編譯的 bitcode 會不一樣?明明編出的 object 都是一模一樣的!
這是因為二進制的 bitcode 文件中還保存了一些與實際代碼無關的 meta 信息。如果能將 bitcode 轉換為文本格式,將能更直觀地進行對比。前面已經提到,xcode 中並沒有附帶轉換工具,但是我們依然可以通過 clang 來完成這一操作,還記得前面用過的 -emit-llvm -S 嗎?
$ clang -emit-llvm -S test_bitcode.o.bc -o test_bitcode.o.ll
神奇吧?輸入雖然已經是 bitcode 了,並非源代碼,但是 clang 也能"編譯"出 LLVM Assembly。其實 clang 內部是先將輸入的文件轉換成 Module 對象,然后再執行對應的處理:
- 如果輸入是源代碼,會先進行前端編譯,得到一個 Module;
- 如果輸入是 bitcode 或者 LLVM Assembly,那么直接進行 parse 操作,即可得到 Module 對象;
- 如果輸出類型是 LLVM Assembly,將 Module 對象序列化為文本格式;
- 如果輸出類型是 bitcode,則將 Module 對象序列化為二進制格式
所以完全可以通過 clang 進行 bitcode 和 LLVM Assembly 的相互轉換。
現在,可以對比一下前后兩次生成的.ll文件:(待驗證)
$ diff test_bitcode.o.ll test.ll
$ diff /Users/cykj/Desktop/Chart/Chart/test_bitcode.o.ll /Users/cykj/Desktop/Chart/Chart/test.ll
1c1
< ; ModuleID = 'test_bitcode.o.bc'
---
> ; ModuleID = 'test.c'
除了 ModuleID,也就是來源的文件名以外,其余部分完全相同,這也就解決了前面的疑慮。
再來回顧一下,前文提到非 Archive 類型的 build,比如直接 ⌘ + B,即使開啟了 bitcode,也不會編出 bitcode,那么會產生什么樣的文件呢?通過觀察編譯日志可以看出 xcode 在此時使用了 -fembed-bitcode-marker 這樣一個參數,試一下:
$ clang -fembed-bitcode-marker -c test.c -o test_bitcode_marker.o
$ otool -l test_bitcode_marker.o
Section
sectname __bitcode
segname __LLVM
addr 0x0000000000000039
size 0x0000000000000001
offset 761
align 2^0 (1)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Section
sectname __cmdline
segname __LLVM
addr 0x000000000000003a
size 0x0000000000000001
offset 762
align 2^0 (1)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
$ objdump -s -section=__bitcode test_bitcode_marker.o
test_bitcode_marker.o: file format Mach-O 64-bit x86-64
Contents of section __bitcode:
0039 00
這樣的方式編譯出的文件結構與 -fembed-bitcode 的結果是一樣的,唯一的區別就是 __LLVM,__bitcode 和 __LLVM,__cmdline 的內容並沒有將實際的 bitcode 文件和編譯參數嵌入進來,取而代之的一個字節的占位符 0x00。
九、Bitcode Bundle
已經搞清楚了 bitcode 是如何嵌入在 object 文件里的,但是 object 只是編譯過程的中間產物,真正運行的代碼是多個 object文件經過鏈接之后的可執行文件,接下來要分析下 object 中嵌入的 bitcode 是如何被鏈接的:
$ clang test.o -o test # 鏈接原始 object
$ clang -fembed-bitcode test_bitcode.o -o test_bitcode # 鏈接帶 bitcode 的object
$ clang test.o -o test
$ ./test
hello, world.
$ clang -fembed-bitcode test_bitcode.o -o test_bitcode
$ ./test_bitcode
hello, world.
$ otool -l test_bitcode
Section
sectname __bundle
segname __LLVM
addr 0x0000000100002000
size 0x0000000000001264
offset 8192
align 2^0 (1)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
object 中的 __LLVM,__bitcode 和 __LLVM,__cmdline不見了,取而代之的是一個 __LLVM,__bundle 的 Section, 通過名字可以基本推斷出 object 中的 bitcode 被打包在了一起,把它從可執行文件中 dump 出來一探究竟:
$ segedit -extract __LLVM __bundle bundle test_bitcode
$ file bundle
bundle: xar archive version 1, SHA-1 checksum
這個 bundle 文件是一個 xar 格式的壓縮包,xar 格式包含了一個 xml 格式的文件頭(TOC),里面用於存放各種文件的基本屬性以及一些附加附加信息,可以通過 xar 命令查看並解壓:
$ xar -d toc.xml -f bundle # 導出文件頭
$ xar -x -C bundle.extract -f bundle # 解壓文件
$ xar -d toc.xml -f bundle
$ mkdir bundle.extract
$ xar -x -C bundle.extract -f bundle
$ ls bundle.extract
1
$ file bundle.extract/1
bundle.extract/1: LLVM bitcode, wrapper x86_64
$ md5 bundle.extract/1 test_bitcode.o.bc
MD5 (bundle.extract/1) = 1b3bd72eb7f380cfd6c6528674d90828
MD5 (test_bitcode.o.bc) = 1b3bd72eb7f380cfd6c6528674d90828
查看導出的 toc.xml
<?xml version="1.0" encoding="UTF-8"?>
<xar>
<subdoc subdoc_name="Ld">
<version>1.0</version>
<architecture>x86_64</architecture>
<platform>MacOSX</platform>
<sdkversion>10.13.0</sdkversion>
<dylibs>
<lib>{SDKPATH}/usr/lib/libSystem.B.dylib</lib>
</dylibs>
<link-options>
<option>-execute</option>
<option>-macosx_version_min</option>
<option>10.13.0</option>
<option>-e</option>
<option>_main</option>
<option>-executable_path</option>
<option>test_bitcode</option>
</link-options>
</subdoc>
<toc>
<checksum style="sha1">
<size>20</size>
<offset>0</offset>
</checksum>
<creation-time>2019-01-11T10:21:54</creation-time>
<file id="1">
<name>1</name>
<type>file</type>
<data>
<archived-checksum style="sha1">d64be6fc7a9551555ccb4e8a78a87864cbef40b7</archived-checksum>
<extracted-checksum style="sha1">d64be6fc7a9551555ccb4e8a78a87864cbef40b7</extracted-checksum>
<size>2464</size>
<offset>20</offset>
<encoding style="application/octet-stream"/>
<length>2464</length>
</data>
<file-type>Bitcode</file-type>
<clang>
<cmd>-triple</cmd>
<cmd>x86_64-apple-macosx10.13.0</cmd>
<cmd>-emit-obj</cmd>
<cmd>-disable-llvm-passes</cmd>
</clang>
</file>
</toc>
</xar>
header 的結構非常清晰,內容基本包含這些:
- ld 的基本參數,我們鏈接時使用的是 clang,實際上 clang 內部調用了 ld,這里記錄的是 ld的參數
- version: bitcode bundle 的版本號
- architecture: 目標架構
- platform: 目標平台
- sdkversion: sdk版本
- dylibs: 鏈接的動態庫
- link-options: 其他鏈接參數
- 文件目錄
- checksum類型
- 創建時間
- 每個文件的信息
- 文件名,這里並非原始文件名,而是按照鏈接時輸入的順序被重命名為數字序號
- 基本屬性,包括 checksum、偏移、大小等
- 文件類型,一般是 Bitcode,還有兩種特殊類型,Object 以及 Bundle
- 編譯器類型(clang/swift)及編譯參數,這部分就是 object 文件中 __LLVM,__cmdline 的內容
- 下一個文件的信息(如有)
- 重復
從 bundle 中解壓出來的文件,就是 object 中嵌入的 bitcode,通過 MD5 對比可以看出鏈接時對 bitcode 文件自身沒有做任何處理。可以注意到,用於編譯各個 bitcode 文件的參數(cmdline)被放進了 TOC 中文件描述的區域,而 TOC 中多出了一個部分用於存放鏈接時所需要的信息和必要的參數,有了這些信息, 我們不難通過 bitcode 重新編譯,並鏈接出一個新的可執行文件:
# 首先根據文件目錄,將解壓出的每一個bitcode文件編譯為object
$ clang -cc1 -triple x86_64-apple-macosx10.14.0 -emit-obj -disable-llvm-passes bundle.extract/1 -o bundle.extract/1.o -x ir
# 由於解壓出的文件沒有后綴名,clang無法判斷輸入文件的格式,因此使用 -x ir 強制指定輸入文件為ir格式
# 也可以將其重命名為1.bc,這樣就不用指定-x ir
# 根據toc.xml中提供的鏈接參數,將所有object文件鏈接為可執行文件,本例中只有一個文件
$ ld
-arch x86_64 `# architecture`
-syslibroot `xcrun --show-sdk-path --sdk macosx` `# platform`
-sdk_version 10.14.0 `# sdkversion`
-lSystem `# dylibs`
-execute `# link-options`
-macosx_version_min 10.14.0 `# link-options`
-e _main `# link-options`
-executable_path test `# link-options`
-o test_rebuild `# 輸出文件`
bundle.extract/1.o `# 輸入文件`
$ ./test_rebuild
hello, world.
$ md5 test_rebuild test
MD5 (test_rebuild) = f4786288582decf2b8a1accb1aaa4a3c
MD5 (test) = f4786288582decf2b8a1accb1aaa4a3c
看!我們成功利用 bitcode 重新編了一份一模一樣的可執行文件出來。
現在可以理解,為什么蘋果要強推 bitcode 了吧?開發者把 bitcode 提交到 iTunes Connect 之后,如果蘋果發布了使用新芯片的 iPhone,支持更高效的指令,開發者不需要做任何操作,iTunes Connect 自己就可以編譯出針對新產品優化過的 app 並通過 App Store 分發給用戶,不需要開發者自己重新打包上架,這樣一來蘋果的商店生態就不需要依賴開發者的積極性了。
十、使用 Bitcode 導出 ipa
前面已經提到,如果要以 bitcode 方式上傳 app,必須在開啟 bitcode 的狀態下,進行 Archive 打包,才會得到帶有 bitcode 的 app。大部分 app 都會依賴一堆第三方 sdk,如果此時項目里依賴的某一個或者幾個 sdk 沒有開啟 bitcode,那么很遺憾,Xcode 會拒絕編譯並給出類似這樣的提示:
ld: ‘name_of_the_library_or_framework’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target.
ld: bitcode bundle could not be generated because ‘name_of_the_library_or_framework’ was built without full bitcode.
第一種提示表示這個第三方庫完全沒有開啟 bitcode,而第二種提示表示它只有 bitcode-marker,也就是說它的開發者雖然在工程配置中設置了 Enable Bitcode 為 YES,但並沒有以 Archive 方式編譯,可能只是 ⌘ + B,然后順手把 Products 拷貝出來交付了。
遇到這種問題,也需要分兩種情況來看:
- 如果這個庫是在本地編譯的, 比如自己項目里或者子項目里的 target,或者通過 Pods 引入了源代碼,那么這個 target 一定沒有開啟 bitcode,在工程中找到這個 target 的 Build Settings 把 Enable Bitcode 置為 YES 即可;
- 但如果是第三方提供的二進制庫文件,則需要聯系 sdk 的提供方確認是否能提供帶 bitcode 的版本,否則只能關閉自己項目中的 bitcode。這也是 bitcode 時至今日都沒有得到大面積應用的最大障阻礙。
當使用 Archive 方式打包出帶有 bitcode 的包時,你會發現這個包里的二進制文件比沒有開啟 bitcode 時大出了許多,多出來的其實就是 bitcode 的體積,並且 bitcode 的體積,一般要比二進制文件本身還要大出許多。
$ ls -al test.o test_bitcode.o test.bc
-rw-r--r-- 1 xelz staff 2848 12 19 18:42 test.bc
-rw-r--r--@ 1 xelz staff 784 12 19 18:24 test.o
-rw-r--r--@ 1 xelz staff 3920 12 19 18:59 test_bitcode.o
$ ls -al test test_bitcode
-rwxr-xr-x@ 1 xelz staff 8432 12 19 21:38 test
-rwxr-xr-x@ 1 xelz staff 16624 12 19 20:50 test_bitcode
當然,這部分內容並不會導致用戶下載到的 APP 變大,因為用戶下載到的代碼中只會有機器碼,不會包含 bitcode。有的項目開啟 bitcode 之后會發現二進制的體積增大到超出了蘋果對二進制體積的限制,但是完全不用擔心,蘋果的限制只是針對 __TEXT 段,而嵌入的 bitcode 是存儲在單獨的 __LLVM 段,不在蘋果的限制范圍內。
打包出帶有 bitcode 的 xcarchive 之后,可以導出 Development IPA 進行上線前的最終測試,或者上傳到 App Store Connect進行提審上架。進行此類操作時會發現 Xcode Organizer 中多出了 bitcode 相關的選項:
導出 Development 版本時,可以勾選 Rebuild from Bitcode,這時導出會變的很慢,因為 Xcode 在后台通過 bitcode 重新編譯代碼,這樣導出的 ipa 最接近最終用戶從 AppStore 下載的版本,為什么說是接近呢,因為蘋果使用的編譯器版本很可能和本地 Xcode 不一樣,並且蘋果可能在編譯時增加額外的優化步驟,這些都會導致蘋果編譯后的二進制文件跟本地編譯的版本產生差異。而如果不勾選此選項,則會直接使用 Archive 時編譯出的二進制代碼,並把 bitcode 從二進制中去除以減小體積。
導出 Store 版本或者直接進行上傳時,默認會勾選 Include bitcode for iOS content,如果不勾選,則跟前面類似,將會去除內嵌的 bitcode,直接使用本地編譯的二進制代碼。
勾選后生成的 ipa 中將會只包含 bitcode,這個 ipa 是無法重簽后安裝到設備上進行測試的,因為里面沒有任何可執行代碼:
__TEXT 和 __DATA 等跟已編譯好的二進制相關的內容會被全部去除,但是會保留 __LINKEDIT 中的部分信息,其中最重要的就是 LC_UUID,用於在重編之后能跟原始的符號文件對應起來,如果用戶下載經過 AppStore 重編之后的 app 發生了Crash,得到的 backtrace 地址是跟本地編譯的版本對應不起來的,需要結合 UUID 和從 App Store Connect 下載的dSYM 文件才能得到符號化的 crash 信息。
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem\_kernel.dylib 0x23269c84 \_\_pthread_kill + 8
1 libsystem\_pthread.dylib 0x2330bb46 pthread\_kill + 62
2 libsystem_c.dylib 0x232000c4 abort + 108
3 libc++abi.dylib 0x22d7a7dc \_\_cxa\_bad_cast + 0
4 libc++abi.dylib 0x22d936a0 default\_unexpected\_handler() + 0
5 libobjc.A.dylib 0x22d9f098 \_objc\_terminate() + 192
6 libc++abi.dylib 0x22d90e16 std::__terminate(void (*)()) + 78
7 libc++abi.dylib 0x22d905f4 \_\_cxxabiv1::exception\_cleanup\_func(\_Unwind\_Reason\_Code, \_Unwind\_Exception*) + 0
8 libobjc.A.dylib 0x22d9eed2 objc\_exception\_throw + 250
9 CoreFoundation 0x234e831e -\[__NSArrayI objectAtIndex:\] + 186
10 test 0x000791f2 \_\_hidden#5\_ (\_\_hidden#43\_:35)
11 libdispatch.dylib 0x2316fdd6 \_dispatch\_call\_block\_and_release + 10
12 libdispatch.dylib 0x231794e6 \_dispatch\_after\_timer\_callback + 66
13 libdispatch.dylib 0x2316fdc2 \_dispatch\_client_callout + 22
14 libdispatch.dylib 0x231826d2 \_dispatch\_source\_latch\_and_call + 2042
15 libdispatch.dylib 0x23171d16 \_dispatch\_source_invoke + 738
16 libdispatch.dylib 0x231741fe \_dispatch\_main\_queue\_callback_4CF + 394
17 CoreFoundation 0x23594fc4 \_\_CFRUNLOOP\_IS\_SERVICING\_THE\_MAIN\_DISPATCH\_QUEUE\_\_ + 8
18 CoreFoundation 0x235934be __CFRunLoopRun + 1590
19 CoreFoundation 0x234e5bb8 CFRunLoopRunSpecific + 516
20 CoreFoundation 0x234e59ac CFRunLoopRunInMode + 108
21 GraphicsServices 0x2475faf8 GSEventRunModal + 160
22 UIKit 0x277d1fb4 UIApplicationMain + 144
23 test 0x000797de main (\_\_hidden#317\_:14)
24 libdyld.dylib 0x23198872 start + 2
\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
File: /Users/Breeze/Desktop/crash/test.app.dSYM/Contents/Resources/DWARF/test (armv7)
\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
.debug_info contents:
0x00000000: Compile Unit: length = 0x00000073 version = 0x0002 abbr\_offset = 0x00000000 addr\_size = 0x04 (next CU at 0x00000077)
0x0000000b: TAG\_compile\_unit \[1\] *
AT\_producer( "\_\_hidden#30_" )
AT\_language( DW\_LANG_ObjC )
AT\_name( "\_\_hidden#43_" )
AT\_stmt\_list( 0x00000000 )
AT\_comp\_dir( "\_\_hidden#41\_" )
AT\_APPLE\_optimized( 0x01 )
AT\_APPLE\_major\_runtime\_vers( 0x02 )
AT\_low\_pc( 0x0000a0b0 )
AT\_high\_pc( 0x0000a206 )
0x00000028: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a0b0 )
AT\_high\_pc( 0x0000a154 )
AT\_name( "\_\_hidden#45_" )
0x00000035: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a154 )
AT\_high\_pc( 0x0000a166 )
AT\_name( "\_\_hidden#1_" )
0x00000042: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a168 )
AT\_high\_pc( 0x0000a16e )
AT\_name( "\_\_hidden#2_" )
0x0000004f: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a170 )
AT\_high\_pc( 0x0000a176 )
AT\_name( "\_\_hidden#3_" )
0x0000005c: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a178 )
AT\_high\_pc( 0x0000a1a4 )
AT\_name( "\_\_hidden#44_" )
0x00000069: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a1a4 )
AT\_high\_pc( 0x0000a206 )
AT\_name( "\_\_hidden#42_" )
0x00000076: NULL
0x00000077: Compile Unit: length = 0x000000db version = 0x0002 abbr\_offset = 0x00000000 addr\_size = 0x04 (next CU at 0x00000156)
0x00000082: TAG\_compile\_unit \[1\] *
AT\_producer( "\_\_hidden#30_" )
AT\_language( DW\_LANG_ObjC )
AT\_name( "\_\_hidden#301_" )
AT\_stmt\_list( 0x000000bf )
AT\_comp\_dir( "\_\_hidden#41\_" )
AT\_APPLE\_optimized( 0x01 )
AT\_APPLE\_major\_runtime\_vers( 0x02 )
AT\_low\_pc( 0x0000a208 )
AT\_high\_pc( 0x0000a796 )
0x0000009f: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a208 )
AT\_high\_pc( 0x0000a20c )
AT\_name( "\_\_hidden#315_" )
0x000000ac: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a20c )
AT\_high\_pc( 0x0000a20e )
AT\_name( "\_\_hidden#314_" )
0x000000b9: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a210 )
AT\_high\_pc( 0x0000a212 )
AT\_name( "\_\_hidden#313_" )
0x000000c6: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a214 )
AT\_high\_pc( 0x0000a216 )
AT\_name( "\_\_hidden#312_" )
0x000000d3: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a218 )
AT\_high\_pc( 0x0000a21a )
AT\_name( "\_\_hidden#311_" )
0x000000e0: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a21c )
AT\_high\_pc( 0x0000a22c )
AT\_name( "\_\_hidden#310_" )
0x000000ed: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a22c )
AT\_high\_pc( 0x0000a2a2 )
AT\_name( "\_\_hidden#309_" )
0x000000fa: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a2a4 )
AT\_high\_pc( 0x0000a372 )
AT\_name( "\_\_hidden#308_" )
0x00000107: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a374 )
AT\_high\_pc( 0x0000a5b6 )
AT\_name( "\_\_hidden#307_" )
0x00000114: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a5b8 )
AT\_high\_pc( 0x0000a65c )
AT\_name( "\_\_hidden#306_" )
0x00000121: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a65c )
AT\_high\_pc( 0x0000a702 )
AT\_name( "\_\_hidden#305_" )
0x0000012e: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a704 )
AT\_high\_pc( 0x0000a714 )
AT\_name( "\_\_hidden#304_" )
0x0000013b: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a714 )
AT\_high\_pc( 0x0000a73a )
AT\_name( "\_\_hidden#302_" )
0x00000148: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a73c )
AT\_high\_pc( 0x0000a796 )
AT\_name( "\_\_hidden#300_" )
0x00000155: NULL
0x00000156: Compile Unit: length = 0x00000032 version = 0x0002 abbr\_offset = 0x00000000 addr\_size = 0x04 (next CU at 0x0000018c)
0x00000161: TAG\_compile\_unit \[1\] *
AT\_producer( "\_\_hidden#30_" )
AT\_language( DW\_LANG_ObjC )
AT\_name( "\_\_hidden#317_" )
AT\_stmt\_list( 0x00000320 )
AT\_comp\_dir( "\_\_hidden#41\_" )
AT\_APPLE\_optimized( 0x01 )
AT\_APPLE\_major\_runtime\_vers( 0x02 )
AT\_low\_pc( 0x0000a798 )
AT\_high\_pc( 0x0000a7f4 )
0x0000017e: TAG_subprogram \[2\]
AT\_low\_pc( 0x0000a798 )
AT\_high\_pc( 0x0000a7f4 )
AT\_name( "\_\_hidden#316_" )
0x0000018b: NULL
十一、bitcode 不是 bytecode
bitcode 不能翻譯為字節碼(bytecode),顯然從字面上看這兩個詞代表的含義並不等同:字節碼是按照字節存取的,一般其控制代碼的最小寬度是一個字節(也即 8 個bits),而 bitcode 是按位(bit)存取,最大化利用空間。比如用 bitcode 中使用 6-bit characters來編碼只包含字母/數字的字符串。
'a' .. 'z' --- 0 .. 25 ---> 00 0000 .. 01 1001
'A' .. 'Z' --- 26 .. 51 ---> 01 1010 .. 11 0011
'0' .. '9' --- 52 .. 61 ---> 11 0100 .. 11 1101
'.' \-\-\- 62 ---> 11 1110
'_' --- 63 ---> 11 1111
在這種編碼模式下,4 字節的字符串 abcd只用 3 個字節就可以表示
char: a | b | c | d
binary: 00 00 00|00|00 01|00 00|10|00 00 11
hex: 00 | 10 | 83
完整的編碼格式可以參考官方文檔LLVM Bitcode File Format
十二、bitcode 的兼容性
bitcode 的格式目前是一直在變化的,且無法向前兼容,舉例來說 Xcode8 的編譯器無法讀取並解析 xcode9 產生的 bitcode。
另外蘋果的 bitcode 格式與社區版 LLVM 的 bitcode 有一定差異,但蘋果並不會及時開源 Xcode 最新版編譯器的代碼,所以如果你使用第三方基於社區版 LLVM 制作的編譯器進行開發,不要嘗試開啟並提交 bitcode 到 App Store Connect,否則會因為App Store Connect 解析不了你的 bitcode 而被拒。
十三、bitcode 不是架構無關代碼
如果一個 app 同時要支持 armv7 和 arm64 兩種架構,那么同一個源代碼文件將會被編譯出兩份 bitcode,也就是說,在一開始介紹 LLVM 的那張圖中,並不是代表同一份 bitcode 代碼可以直接被編譯為不同目標機器的機器碼。
LLVM 只是統一了中間語言的結構和語法格式,但不能像 Java 那樣,Compile Once & Run Everywhere。
十四、如何判斷是否開啟 bitcode
可以通過 otool 檢查二進制文件,網上有很多類似這樣的方法:
otool -arch armv7 -l xxxx.a | grep __LLVM | wc -l
通過判斷是否包含 __LLVM 或者關鍵字來判斷是否支持 bitcode,其實這種方式是完全錯誤的,通過前面的測試可以知道,這種方式區分不了 bitcode 和 bitcode-marker,確定是否包含 bitcode,還需要檢查 otool 輸出中 __LLVM Segment 的長度,如果長度只有 1 個字節,則並不能代表真正開啟了 bitcode:
$ otool -l test\_bitcode.o | grep -A 2 \_\_LLVM | grep size
size 0x0000000000000b10
size 0x0000000000000042
$ otool -l test\_bitcode\_marker.o | grep -A 2 __LLVM | grep size
size 0x0000000000000001
size 0x0000000000000001
十五、bitcode 是否能反編譯出源代碼
從科學嚴謹的角度來說,無法給出確定的答案,但是這個問題跟"二進制文件是否能反編譯出源代碼"是一樣的道理。編譯是一個將源代碼一層一層不斷低級化的過程,每一層都可能會丟失一些特性,產生不可逆的轉換,把源代碼編譯為 bitcode 或是二進制機器碼是五十步之於百步的關系。在通常情況下,反編譯 bitcode 跟反編譯二進制文件比要相對容易一些,但通過 bitcode 反編譯出和源代碼語義完全相同的代碼,也是幾乎不可能的。
另外,從安全的角度考慮,Xcode 引入了 Symbol Hiding 和 Debug info Striping 機制,在鏈接時,bitcode 中所有非導出符號均被隱藏,取而代之的是 __hidden#0_ 或者 __ir_hidden#1_ 這樣的形式,debug 信息也只保留了 line-table,所有跟文件路徑、標識符、導出符號等相關的信息全部都從 bitcode 中移除,相當於做了一層混淆,防止源代碼級別的信息泄露,可謂是煞費苦心。
十六、參考文章
lansekuangtu & 《理解Bitcode:一種中間代碼》
戴維營教育 & 《深入理解iOS開發中的BitCode功能》
LLVM 官方文檔介紹的 bitcode 文件的格式:LLVM Bitcode File Format
Understanding and Analyzing Application Crash Reports
Troubleshooting App Thinning and Bitcode Build Failures
http://xelz.info/blog/2018/11/24/all-you-need-to-know-about-bitcode/
iOS 打包上線 bitcode問題
判斷一個庫是否包含 bitcode