2022/06/07更新
bitcode is now deprecated, builds for iOS, tvOS, and watchOS no longer include bitcode by default
開發(fā)組件時(shí)因?yàn)閎itcode兼容問題折騰了好久睛琳,bitcode怎么支持、如何驗(yàn)證bitcode是否生效折騰了好久。分享一篇文章榛瓮。
關(guān)于bitcode, 知道這些就夠了
0x00 前言
蘋果在WWDC 2015大會(huì)上引入了bitcode,隨后在Xcode7中添加了在二進(jìn)制中嵌入bitcode(Enable Bitcode)的功能膏斤,并且默認(rèn)設(shè)置為開啟狀態(tài)朋鞍。很多開發(fā)者在集成第三方SDK的時(shí)候都被bitcode坑過一把,然后google百度一番發(fā)現(xiàn)只要關(guān)閉bitcode就可以了拦坠,但是大部分開發(fā)者都不清楚bitcode到底是什么東西。這篇文檔將給大家詳細(xì)地介紹與bitcode有關(guān)的內(nèi)容贫橙。
0x01 什么是bitcode
研究bitcode之前需要先了解一下LLVM贪婉,因?yàn)?strong>bitcode是由LLVM引入的一種中間代碼(Intermediate Representation,簡稱IR)卢肃,它是源代碼被編譯為二進(jìn)制機(jī)器碼過程中的中間表示形態(tài)疲迂,它既不是源代碼,也不是機(jī)器碼莫湘。從代碼組織結(jié)構(gòu)上看它比較接近機(jī)器碼尤蒿,但是在函數(shù)和指令層面使用了很多高級(jí)語言的特性。
LLVM是一套優(yōu)秀的編譯器框架幅垮,目前NDK/Xcode均采用LLVM作為默認(rèn)的編譯器腰池。LLVM的編譯過程可以簡單分為3個(gè)部分:
- 前端(Frontend),負(fù)責(zé)把各種類型的源代碼編譯為中間表示忙芒,也就是bitcode示弓,在LLVM體系內(nèi),不同的語言有不同的編譯器前端呵萨,最常見的如clang負(fù)責(zé)c/c++/oc的編譯奏属,flang負(fù)責(zé)fortran的編譯,swiftc負(fù)責(zé)swift的編譯等等
- 優(yōu)化(Optimizer)潮峦,負(fù)責(zé)對bitcode進(jìn)行各種類型的優(yōu)化囱皿,將bitcode代碼進(jìn)行一些邏輯等價(jià)的轉(zhuǎn)換勇婴,使得代碼的執(zhí)行效率更高,體積更小嘱腥,比如DeadStrip/SimplifyCFG
- 后端(Backend)耕渴,也叫CodeGenerator,負(fù)責(zé)把優(yōu)化后的bitcode編譯為指定目標(biāo)架構(gòu)的機(jī)器碼齿兔,比如X86Backend負(fù)責(zé)把bitcode編譯為x86指令集的機(jī)器碼
在這個(gè)體系中橱脸,不同語言的源代碼將會(huì)被轉(zhuǎn)化為統(tǒng)一的bitcode格式,三個(gè)模塊可以充分復(fù)用愧驱,防止重復(fù)造輪子慰技。如果要開發(fā)一門新的x語言
,只需要造一個(gè)x語言的前端组砚,將x語言的源代碼編譯為bitcode吻商,優(yōu)化和后端的事情完全不用管。同理糟红,如果新的芯片架構(gòu)問世艾帐,則只需要基于LLVM重新寫一套目標(biāo)平臺(tái)的后端,非常方便盆偿。
0x02 bitcode初探
既然bitcode是代碼的一種表示形式柒爸,因此它也會(huì)有自己的一套獨(dú)立的語法,可以通過一個(gè)簡單的例子來一探究竟事扭,這里以clang為例捎稚,swift的操作和結(jié)果可能稍有不同。
本文所涉及的內(nèi)容可以自行操作求橄,也可以直接下載我寫這篇文章時(shí)保存的副本
先編寫一段helloworld代碼(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
其實(shí)今野,這個(gè)命令同時(shí)完成了前端、優(yōu)化罐农、后端三個(gè)部分条霜,可以通過 -emit-llvm -c
將前端這一步單獨(dú)拆出來,這樣就可以看到bitcode了:
$ clang -emit-llvm -c test.c -o test.bc # 將源代碼編譯為bitcode
$ file test.bc
test.bc: LLVM bitcode, wrapper x86_64
$ clang -c test.bc -o test.bc.o # 將bitcode編譯為object
$ file test.bc.o
test.bc.o: Mach-O 64-bit object x86_64
$ md5 test.bc.o test.o
MD5 (test.bc.o) = 70ea3a520c26df84d1f7ca552e8e6620
MD5 (test.o) = 70ea3a520c26df84d1f7ca552e8e6620
bitcode文件使用后綴名.bc
表示涵亏,可以看到宰睡,將bitcode文件作為clang的輸入,編出的object文件跟直接編源代碼是相同的气筋。然后在來看一下bitcode文件:
$ hexdump -C test.bc | head
00000000 de c0 17 0b 00 00 00 00 14 00 00 00 08 0b 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 96 96 a6 a5 f7 d7 7f 4d d3 b4 5f d7 |b.0$.......M.._.|
00000030 3e 9e fb f9 4f 0b 51 80 4c 01 00 00 21 0c 00 00 |>...O.Q.L...!...|
00000040 74 02 00 00 0b 02 21 00 02 00 00 00 13 00 00 00 |t.....!.........|
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 04 49 0e 90 11 22 c4 50 |CF....2B.I...".P|
00000090 41 51 81 8c e1 83 e5 8a 04 21 46 06 51 18 00 00 |AQ.......!F.Q...|
通過hexdump可以看出這個(gè)文件并非文本文件拆内,全是亂碼,這樣的文件是很難分析的宠默。其實(shí)LLVM提供了llvm-dis
/ llvm-as
兩個(gè)工具矛纹,用于將bitcode在二進(jìn)制格式和可讀的文本格式之間進(jìn)行相互的轉(zhuǎn)化,但遺憾的是Xcode的編譯器工具鏈中并沒有附帶這個(gè)命令光稼,因此只能另尋他法或南。
我們知道通過編譯器的-S
參數(shù)可以將源代碼編譯為文本的assembly代碼,不進(jìn)行最后一步assembly到機(jī)器碼的翻譯工作艾君,而assembly和機(jī)器碼是等價(jià)的兩種表示形式采够,bitcode同樣也是有文本和二進(jìn)制(bitcode)兩種等價(jià)表示形式,clang也為bitcode保留了這一特性冰垄,可以通過-emit-llvm -S
將源代碼編譯為文本格式的bitcode蹬癌, 也叫做LLVM Assembly Language,一般后綴名使用.ll
:
$ clang -emit-llvm -S test.c -o test.ll # 將源代碼編譯為LLVM Assembly
test.ll的全部內(nèi)容如下
; 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.14.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 {
%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 optnone 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,+sahf,+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,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{!"Apple LLVM version 10.0.0 (clang-1000.11.45.5)"}
這樣看上去就很清晰明了了虹茶,我們重點(diǎn)關(guān)注下函數(shù)定義這部分逝薪,我加了一些注釋方便理解
; 定義全局常量 @.str, 內(nèi)容初始化為 '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 { ; 定義函數(shù) @main,返回值為i32類型
%1 = alloca i32, align 4 ; 聲明變量 %1 = 分配i32的內(nèi)存空間
store i32 0, i32* %1, align 4 ; 將 0 存入 %1 的內(nèi)存空間
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str, i32 0, i32 0)) ; 調(diào)用 @printf 函數(shù)蝴罪,并將 @.str 的地址作為參數(shù)
ret i32 0 ; 返回 0
}
declare i32 @printf(i8*, ...) #1 ; 聲明一個(gè)外部函數(shù) @printf
這段代碼不難閱讀董济, 其含義和邏輯與我們所寫的源代碼基本一致,只是用了另外一種語法表示出來要门。因?yàn)闆]有經(jīng)過優(yōu)化虏肾,函數(shù)中的前兩條語句其實(shí)是多余的,這在之后的優(yōu)化階段會(huì)被消除(dead_strip)欢搜。bitcode的具體語法在此不做展開封豪,雖然這個(gè)例子看起來非常簡單易懂,但真實(shí)場景中炒瘟,bitcode的語法遠(yuǎn)比這個(gè)復(fù)雜吹埠,有興趣的同學(xué)可以直接閱讀LLVM Language Reference Manual。
0x03 Enable Bitcode
在對bitcode有了一個(gè)直觀的認(rèn)識(shí)之后疮装,再來看一下Apple圍繞bitcode做了什么缘琅。Xcode中對Enable Bitcode這個(gè)配置的解釋是:
以下摘自Xcode Help
https://help.apple.com/xcode/mac/10.1/index.html?localePath=en.lproj#/itcaec37c2a6
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.
具體展開一下:
- 開啟此設(shè)置將會(huì)在支持的平臺(tái)和架構(gòu)中開啟bitcode
- 當(dāng)前支持的平臺(tái)主要是iPhoneOS(armv7/arm64),watchOS等
- 注意不包括iPhoneSimulator(i386/x86_64)和macos斩个,也就是說模擬器架構(gòu)下不會(huì)編出bitcode胯杭。這個(gè)限制只是Xcode自身的限制,并非編譯器的限制受啥,我們使用編譯器提供的命令行工具自行操作仍然可以編譯出這些架構(gòu)下的bitcode做个,本文中的示例就是基于macos平臺(tái)/x86_64架構(gòu)。
- 進(jìn)行Archive時(shí)滚局,bitcode會(huì)被嵌入到鏈接后的二進(jìn)制文件中居暖,用于提交給App Store
- Enable Bitcode 設(shè)置為 YES 時(shí),從編譯日志中可以看出藤肢,Archive時(shí)多了一個(gè)編譯參數(shù)
-fembed-bitcode
- Enable Bitcode 設(shè)置為 YES 時(shí),從編譯日志中可以看出藤肢,Archive時(shí)多了一個(gè)編譯參數(shù)
- 進(jìn)行其他類型的Build(非Archive)時(shí)太闺,編譯器只會(huì)檢查是否滿足開啟bitcode的條件,但并不會(huì)真正生成bitcode
- 非Archive編譯時(shí)嘁圈,Enable Bitcode 將會(huì)增加編譯參數(shù)
-fembed-bitcode-marker
省骂, 只是在object文件中做了標(biāo)記蟀淮,表明我可以有bitcode,但是現(xiàn)在暫時(shí)沒有帶上它
钞澳。因?yàn)楸镜鼐幾g調(diào)試時(shí)并不需要bitcode怠惶,只有AppStore需要這玩意兒,去掉這個(gè)不必要的步驟轧粟,會(huì)加快編譯速度策治。 - 這就是為什么有的同學(xué)在開發(fā)SDK時(shí),明明開啟了Enable Bitcode兰吟,交給客戶后客戶卻說:你的sdk里沒有bitcode通惫,因?yàn)槟銢]有使用Archive方式打包。
- 當(dāng)然混蔼,你可以將 Enable Bitcode 設(shè)置為NO履腋, 然后在Other Compiler Flags 和 Other Linker Flags 中手動(dòng)為真機(jī)架構(gòu)添加
-fembed-bitcode
參數(shù),這樣任何類型的Build都會(huì)帶上bitcode
- 非Archive編譯時(shí)嘁圈,Enable Bitcode 將會(huì)增加編譯參數(shù)
接下來看一下 Enable Bitcode 之后拄丰,編譯出的文件發(fā)生了什么變化府树, 直接在clang的參數(shù)中添加 -fembed-bitcode
即可
$ clang -fembed-bitcode -c test.c -o test_bitcode.o
編譯之后可以通過tool工具查看object文件的結(jié)構(gòu),此時(shí)你需要對Mach-O文件有一些基本的了解
$ otool -l test_bitcode.o
# 以下為otool輸出節(jié)選
Section
sectname __bitcode
segname __LLVM
addr 0x0000000000000040
size 0x0000000000000b10
offset 776
align 2^4 (16)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Section
sectname __cmdline
segname __LLVM
addr 0x0000000000000b50
size 0x0000000000000042
offset 3608
align 2^4 (16)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
或者使用MachOView
可以發(fā)現(xiàn)生成的 object 文件中多了兩個(gè) Section料按,分別是 __LLVM,__bitcode
和 __LLVM,__cmdline
奄侠,并且otool的輸出中給出了這兩個(gè)section在object文件中的偏移和大小,通過 dd
命令可以很方便地將這兩個(gè)Section提取出來
$ dd bs=1 skip=776 count=0x0000000000000b10 if=test_bitcode.o of=test_bitcode.o.bc
2832+0 records in
2832+0 records out
2832 bytes transferred in 0.017339 secs (163331 bytes/sec)
$ dd bs=1 skip=3608 count=0x0000000000000042 if=test_bitcode.o of=test_bitcode.o.cmdline
66+0 records in
66+0 records out
66 bytes transferred in 0.001312 secs (50304 bytes/sec)
還有一種更便捷的方式载矿,Xcode 提供的 segedit
命令可以直接將指定的Section導(dǎo)出垄潮,只需要給定Section的名字,和上面的命令效果是一樣的闷盔,并且更為方便
$ segedit -extract __LLVM __bitcode test_bitcode.o.bc \
-extract __LLVM __cmdline test_bitcode.o.cmdline \
test_bitcode.o
觀察一下導(dǎ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.14.0 -emit-obj -disable-llvm-passes
$ md5 test.bc test_bitcode.o.bc
MD5 (test.bc) = 1592ed7db86742184a559e86cb9d1355
MD5 (test_bitcode.o.bc) = 9901ac8db63be30dafc19c2f06b0cae8
不難得出結(jié)論:
- object文件中嵌入的
__LLVM,__bitcode
正是完整的弯洗,未經(jīng)任何加密或者壓縮的bitcode文件,通過-fembed-bitcode
參數(shù)逢勾,clang把對應(yīng)的bitcode文件整個(gè)嵌入到了object文件中 -
__LLVM,__cmdline
是編譯這個(gè)文件所用到的參數(shù)牡整,如果要通過導(dǎo)出的bitcode重新編譯這個(gè)object文件,必須帶上這些參數(shù)- 導(dǎo)出的參數(shù)是
cc1
也就是clang中真正”前端”部分的參數(shù)(clang命令其實(shí)是整合了各個(gè)環(huán)節(jié)溺拱,所以clang一個(gè)命令可以從源代碼編出可執(zhí)行文件)逃贝,所以編譯時(shí)要帶上-cc1
- 導(dǎo)出的參數(shù)是
- 導(dǎo)出的bitcode文件似乎和直接編譯的bitcode不一樣,先留個(gè)疑問迫摔,后面再研究
首先沐扳, 來測試一下導(dǎo)出的bitcode文件結(jié)合cmdline能否編譯出正常的object:
$ clang -cc1 -triple x86_64-apple-macosx10.14.0 -emit-obj -disable-llvm-passes test_bitcode.o.bc -o test_rebuild.o
$ file test_rebuild.o
test_rebuild.o: Mach-O 64-bit object x86_64
$ md5 test.o test_rebuild.o
MD5 (test.o) = 70ea3a520c26df84d1f7ca552e8e6620
MD5 (test_rebuild.o) = 70ea3a520c26df84d1f7ca552e8e6620
沒有任何問題,并且通過內(nèi)嵌的bitcode編譯出的object文件與直接從源代碼編譯出來的object完全一樣句占!鵝妹子嚶~沪摄!
回到遺留的問題:為什么導(dǎo)出的bitcode文件和直接編譯的bitcode會(huì)不一樣?明明編出的object都是一模一樣的!這是因?yàn)槎M(jìn)制的bitcode文件中還保存了一些與實(shí)際代碼無關(guān)的meta信息杨拐。如果能將bitcode轉(zhuǎn)換為文本格式祈餐,將能更直觀地進(jìn)行對比。前面已經(jīng)提到戏阅,xcode中并沒有附帶轉(zhuǎn)換工具昼弟,但是我們依然可以通過clang來完成這一操作,還記得前面用過的 -emit-llvm -S
嗎奕筐?
$ clang -emit-llvm -S test_bitcode.o.bc -o test_bitcode.o.ll
神奇吧?輸入雖然已經(jīng)是bitcode了变骡,并非源代碼离赫,但是clang也能”編譯”出LLVM Assembly。其實(shí)clang內(nèi)部是先將輸入的文件轉(zhuǎn)換成Module對象塌碌,然后再執(zhí)行對應(yīng)的處理:
- 如果輸入是源代碼渊胸,會(huì)先進(jìn)行前端編譯,得到一個(gè)Module
- 如果輸入是bitcode或者LLVM Assembly台妆,那么直接進(jìn)行parse操作翎猛,即可得到Module對象
- 如果輸出類型是LLVM Assembly,將Module對象序列化為文本格式
- 如果輸出類型是bitcode接剩,則將Module對象序列化為二進(jìn)制格式
所以完全可以通過clang進(jìn)行bitcode和LLVM Assembly的相互轉(zhuǎn)換切厘。
現(xiàn)在,可以對比一下前后兩次生成的.ll
文件:
$ diff test_bitcode.o.ll test.ll
1c1
< ; ModuleID = 'test_bitcode.o.bc'
---
> ; ModuleID = 'test.c'
除了ModuleID懊缺,也就是來源的文件名以外疫稿,其余部分完全相同,這也就解決了前面的疑慮鹃两。
再來回顧一下遗座,前文提到非Archive類型的build,比如直接? + B
俊扳,即使開啟了bitcode途蒋,也不會(huì)編出bitcode,那么會(huì)產(chǎn)生什么樣的文件呢馋记?通過觀察編譯日志可以看出xcode在此時(shí)使用了-fembed-bitcode-marker
這樣一個(gè)參數(shù)号坡,我們來試一下:
$ clang -fembed-bitcode-marker -c test.c -o test_bitcode_marker.o
$ otool -l test_bitcode_marker.o
# 以下為otool輸出節(jié)選
Section
sectname __bitcode
segname __LLVM
addr 0x0000000000000039
size 0x0000000000000001 # 只有一個(gè)字節(jié)
offset 769
align 2^0 (1)
reloff 0
nreloc 0
flags 0x00000000
$ objdump -s -section=__bitcode test_bitcode_marker.o
Contents of section __bitcode:
0039 00 . # 只有一個(gè)字節(jié) 0x00
這樣的方式編譯出的文件結(jié)構(gòu)與-fembed-bitcode
的結(jié)果是一樣的,唯一的區(qū)別就是 __LLVM,__bitcode
和 __LLVM,__cmdline
的內(nèi)容并沒有將實(shí)際的bitcode文件和編譯參數(shù)嵌入進(jìn)來抗果,取而代之的一個(gè)字節(jié)的占位符 0x00
0x04 Bitcode Bundle
已經(jīng)搞清楚了bitcode是如何嵌入在object文件里的筋帖,但是object只是編譯過程的中間產(chǎn)物,真正運(yùn)行的代碼是多個(gè)object文件經(jīng)過鏈接之后的可執(zhí)行文件冤馏,接下來要分析下object中嵌入的bitcode是如何被鏈接的:
$ clang test.o -o test # 鏈接原始o(jì)bject
$ ./test
hello, world.
$ clang -fembed-bitcode test_bitcode.o -o test_bitcode # 鏈接帶bitcode的object
$ ./test_bitcode
hello, world.
$ otool -l test_bitcode
# 以下為otool輸出節(jié)選
Section
sectname __bundle
segname __LLVM
addr 0x0000000100002000
size 0x0000000000001261
offset 8192
align 2^0 (1)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
object中的 __LLVM,__bitcode
和 __LLVM,__cmdline
不見了日麸,取而代之的是一個(gè) __LLVM,__bundle
的Section, 通過名字可以基本推斷出object中的bitcode被打包在了一起,把它從可執(zhí)行文件中dump出來一探究竟:
$ segedit -extract __LLVM __bundle bundle test_bitcode
$ file bundle
bundle: xar archive version 1, SHA-1 checksum
這個(gè)bundle文件是一個(gè)xar
格式的壓縮包代箭,xar格式包含了一個(gè)xml
格式的文件頭(TOC)墩划,里面用于存放各種文件的基本屬性以及一些附加附加信息,可以通過xar命令查看并解壓
$ xar -d toc.xml -f bundle # 導(dǎo)出文件頭
$ 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) = 9901ac8db63be30dafc19c2f06b0cae8
MD5 (test_bitcode.o.bc) = 9901ac8db63be30dafc19c2f06b0cae8
查看導(dǎo)出的toc.xml
<?xml version="1.0" encoding="UTF-8"?>
<xar>
<subdoc subdoc_name="Ld">
<version>1.0</version>
<architecture>x86_64</architecture>
<platform>macOS</platform>
<sdkversion>10.14.0</sdkversion>
<dylibs>
<lib>{SDKPATH}/usr/lib/libSystem.B.dylib</lib>
</dylibs>
<link-options>
<option>-execute</option>
<option>-macosx_version_min</option>
<option>10.14.0</option>
<option>-e</option>
<option>_main</option>
<option>-executable_path</option>
<option>test</option>
</link-options>
</subdoc>
<toc>
<checksum style="sha1">
<size>20</size>
<offset>0</offset>
</checksum>
<creation-time>2018-12-19T12:07:24</creation-time>
<file id="1">
<name>1</name>
<type>file</type>
<data>
<archived-checksum style="sha1">56346f644ab01200e0ad56eaefb9346a863cb473</archived-checksum>
<extracted-checksum style="sha1">56346f644ab01200e0ad56eaefb9346a863cb473</extracted-checksum>
<size>2832</size>
<offset>20</offset>
<encoding style="application/octet-stream"/>
<length>2832</length>
</data>
<file-type>Bitcode</file-type>
<clang>
<cmd>-triple</cmd>
<cmd>x86_64-apple-macosx10.14.0</cmd>
<cmd>-emit-obj</cmd>
<cmd>-disable-llvm-passes</cmd>
</clang>
</file>
</toc>
</xar>
header的結(jié)構(gòu)非常清晰嗡综,內(nèi)容基本包含這些:
- ld 的基本參數(shù)乙帮,我們鏈接時(shí)使用的是clang,實(shí)際上clang內(nèi)部調(diào)用了ld极景,這里記錄的是ld的參數(shù)
- version: bitcode bundle 的版本號(hào)
- architecture: 目標(biāo)架構(gòu)
- platform: 目標(biāo)平臺(tái)
- sdkversion: sdk版本
- dylibs: 鏈接的動(dòng)態(tài)庫
- link-options: 其他鏈接參數(shù)
- 文件目錄
- checksum類型
- 創(chuàng)建時(shí)間
- 每個(gè)文件的信息
- 文件名察净,這里并非原始文件名,而是按照鏈接時(shí)輸入的順序被重命名為數(shù)字序號(hào)
- 基本屬性盼樟,包括checksum氢卡、偏移、大小等
- 文件類型晨缴,一般是Bitcode译秦,還有兩種特殊類型,Object以及Bundle击碗,這里賣個(gè)關(guān)子筑悴,大家有興趣可已自行研究(想想如果一個(gè)源代碼文件是.s格式,要如何支持bitcode)
- 編譯器類型(clang/swift)及編譯參數(shù)稍途,這部分就是object文件中
__LLVM,__cmdline
的內(nèi)容
- 下一個(gè)文件的信息(如有)
- 重復(fù)
從bundle中解壓出來的文件阁吝,就是object中嵌入的bitcode,通過MD5對比可以看出鏈接時(shí)對bitcode文件自身沒有做任何處理晰房∏笠。可以注意到,用于編譯各個(gè)bitcode文件的參數(shù)(cmdline)被放進(jìn)了TOC中文件描述的區(qū)域殊者,而TOC中多出了一個(gè)部分用于存放鏈接時(shí)所需要的信息和必要的參數(shù)与境,有了這些信息, 我們不難通過bitcode重新編譯猖吴,并鏈接出一個(gè)新的可執(zhí)行文件:
# 首先根據(jù)文件目錄摔刁,將解壓出的每一個(gè)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 強(qiáng)制指定輸入文件為ir格式
# 也可以將其重命名為1.bc共屈,這樣就不用指定-x ir
# 根據(jù)toc.xml中提供的鏈接參數(shù),將所有object文件鏈接為可執(zhí)行文件党窜,本例中只有一個(gè)文件
$ 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重新編了一份一模一樣的可執(zhí)行文件出來。
現(xiàn)在可以理解幌衣,為什么蘋果要強(qiáng)推bitcode了吧矾削?開發(fā)者把bitcode提交到App Store Connect之后壤玫,如果蘋果發(fā)布了使用新芯片的iPhone,支持更高效的指令哼凯,開發(fā)者不需要做任何操作欲间,App Store Connect自己就可以編譯出針對新產(chǎn)品優(yōu)化過的app并通過App Store分發(fā)給用戶,不需要開發(fā)者自己重新打包上架断部,這樣一來蘋果的Store生態(tài)就不需要依賴開發(fā)者的積極性了猎贴。
0x05 使用Bitcode導(dǎo)出ipa
前面已經(jīng)提到,如果要以bitcode方式上傳app蝴光,必須在開啟bitcode的狀態(tài)下她渴,進(jìn)行Archive打包,才會(huì)得到帶有bitcode的app蔑祟。大部分app都會(huì)依賴一堆第三方sdk惹骂,如果此時(shí)項(xiàng)目里依賴的某一個(gè)或者幾個(gè)sdk沒有開啟bitcode,那么很遺憾做瞪,Xcode會(huì)拒絕編譯并給出類似這樣的提示:
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.
第一種提示表示這個(gè)第三方庫完全沒有開啟bitcode,而第二種提示表示它只有bitcode-marker右冻,也就是說它的開發(fā)者雖然在工程配置中設(shè)置了 Enable Bitcode 為 YES装蓬,但并沒有以Archive方式編譯,可能只是? + B纱扭,然后順手把Products拷貝出來交付了牍帚。
遇到這種問題,也需要分兩種情況來看:
- 如果這個(gè)庫是在本地編譯的乳蛾, 比如自己項(xiàng)目里或者子項(xiàng)目里的target暗赶,或者通過Pods引入了源代碼,那么這個(gè)target一定沒有開啟bitcode肃叶,在工程中找到這個(gè)target的Build Settings把Enable Bitcode置為YES即可
- 但如果是第三方提供的二進(jìn)制庫文件蹂随,則需要聯(lián)系sdk的提供方確認(rèn)是否能提供帶bitcode的版本,否則只能關(guān)閉自己項(xiàng)目中的bitcode因惭。這也是bitcode時(shí)至今日都沒有得到大面積應(yīng)用的最大障阻礙岳锁。
當(dāng)使用Archive方式打包出帶有bitcode的包時(shí),你會(huì)發(fā)現(xiàn)這個(gè)包里的二進(jìn)制文件比沒有開啟bitcode時(shí)大出了許多蹦魔,多出來的其實(shí)就是bitcode的體積激率,并且bitcode的體積,一般要比二進(jìn)制文件本身還要大出許多
$ 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
當(dāng)然勿决,這部分內(nèi)容并不會(huì)導(dǎo)致用戶下載到的APP變大乒躺,因?yàn)橛脩粝螺d到的代碼中只會(huì)有機(jī)器碼,不會(huì)包含bitcode低缩。有的項(xiàng)目開啟bitcode之后會(huì)發(fā)現(xiàn)二進(jìn)制的體積增大到超出了蘋果對二進(jìn)制體積的限制嘉冒,但是完全不用擔(dān)心,蘋果的限制只是針對__TEXT
段,而嵌入的bitcode是存儲(chǔ)在單獨(dú)的__LLVM
段健爬,不在蘋果的限制范圍內(nèi)控乾。
打包出帶有bitcode的xcarchive之后,可以導(dǎo)出Development IPA進(jìn)行上線前的最終測試娜遵,或者上傳到App Store Connect進(jìn)行提審上架蜕衡。進(jìn)行此類操作時(shí)會(huì)發(fā)現(xiàn)Xcode Organizer中多出了bitcode相關(guān)的選項(xiàng):
-
導(dǎo)出Development版本時(shí),可以勾選
Rebuild from Bitcode
设拟,這時(shí)導(dǎo)出會(huì)變的很慢慨仿,因?yàn)閄code在后臺(tái)通過bitcode重新編譯代碼,這樣導(dǎo)出的ipa最接近最終用戶從AppStore下載的版本纳胧,為什么說是接近呢镰吆,因?yàn)樘O果使用的編譯器版本很可能和本地Xcode不一樣,并且蘋果可能在編譯時(shí)增加額外的優(yōu)化步驟跑慕,這些都會(huì)導(dǎo)致蘋果編譯后的二進(jìn)制文件跟本地編譯的版本產(chǎn)生差異万皿。而如果不勾選此選項(xiàng),則會(huì)直接使用Archive時(shí)編譯出的二進(jìn)制代碼核行,并把bitcode從二進(jìn)制中去除以減小體積牢硅。 -
導(dǎo)出Store版本或者直接進(jìn)行上傳時(shí),默認(rèn)會(huì)勾選
Include bitcode for iOS content
芝雪,如果不勾選减余,則跟前面類似,將會(huì)去除內(nèi)嵌的bitcode惩系,直接使用本地編譯的二進(jìn)制代碼勾選后生成的ipa中將會(huì)
只包含bitcode
位岔,這個(gè)ipa是無法重簽后安裝到設(shè)備上進(jìn)行測試的,因?yàn)槔锩鏇]有任何可執(zhí)行代碼:__TEXT
和__DATA
等跟已編譯好的二進(jìn)制相關(guān)的內(nèi)容會(huì)被全部去除堡牡,但是會(huì)保留__LINKEDIT
中的部分信息抒抬,其中最重要的就是LC_UUID
,用于在重編之后能跟原始的符號(hào)文件對應(yīng)起來悴侵,如果用戶下載經(jīng)過AppStore重編之后的app發(fā)生了Crash瞧剖,得到的backtrace地址是跟本地編譯的版本對應(yīng)不起來的,需要結(jié)合UUID和從App Store Connect下載的dSYM文件才能得到符號(hào)化的crash信息可免。
0x06 拓展閱讀
bitcode不是bytecode
bitcode不能翻譯為字節(jié)碼(bytecode)抓于,顯然從字面上看這兩個(gè)詞代表的含義并不等同:字節(jié)碼是按照字節(jié)存取的,一般其控制代碼的最小寬度是一個(gè)字節(jié)(也即8個(gè)bits)浇借,而bitcode是按位(bit)存取捉撮,最大化利用空間。比如用bitcode中使用6-bit characters
來編碼只包含字母/數(shù)字的字符串
'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字節(jié)的字符串abcd
只用3個(gè)字節(jié)就可以表示
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產(chǎn)生的bitcode。
另外蘋果的bitcode格式與社區(qū)版LLVM的bitcode有一定差異灼舍,但蘋果并不會(huì)及時(shí)開源Xcode最新版編譯器的代碼吼和,所以如果你使用第三方基于社區(qū)版LLVM制作的編譯器進(jìn)行開發(fā),不要嘗試開啟并提交bitcode到App Store Connect骑素,否則會(huì)因?yàn)锳pp Store Connect解析不了你的bitcode而被拒炫乓。
bitcode不是架構(gòu)無關(guān)代碼
如果一個(gè)app同時(shí)要支持armv7和arm64兩種架構(gòu),那么同一個(gè)源代碼文件將會(huì)被編譯出兩份bitcode献丑,也就是說末捣,在一開始介紹LLVM的那張圖中,并不是代表同一份bitcode代碼可以直接被編譯為不同目標(biāo)機(jī)器的機(jī)器碼创橄。
LLVM只是統(tǒng)一了中間語言的結(jié)構(gòu)和語法格式箩做,但不能像Java那樣,Compile Once & Run Everywhere.
如何判斷是否開啟bitcode
可以通過otool檢查二進(jìn)制文件妥畏,網(wǎng)上有很多類似這樣的方法:
otool -arch armv7 -l xxxx.a | grep __LLVM | wc -l
通過判斷是否包含 __LLVM
或者關(guān)鍵字來判斷是否支持bitcode邦邦,其實(shí)這種方式是完全錯(cuò)誤的,通過前面的測試可以知道醉蚁,這種方式區(qū)分不了bitcode和bitcode-marker圃酵,確定是否包含bitcode,還需要檢查otool輸出中__LLVM
Segment 的長度馍管,如果長度只有1個(gè)字節(jié),則并不能代表真正開啟了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是否能反編譯出源代碼
從科學(xué)嚴(yán)謹(jǐn)?shù)慕嵌葋碚f薪韩,無法給出確定的答案确沸,但是這個(gè)問題跟“二進(jìn)制文件是否能反編譯出源代碼”是一樣的道理。編譯是一個(gè)將源代碼一層一層不斷低級(jí)化的過程俘陷,每一層都可能會(huì)丟失一些特性罗捎,產(chǎn)生不可逆的轉(zhuǎn)換,把源代碼編譯為bitcode或是二進(jìn)制機(jī)器碼是五十步之于百步的關(guān)系拉盾。在通常情況下桨菜,反編譯bitcode跟反編譯二進(jìn)制文件比要相對容易一些,但通過bitcode反編譯出和源代碼語義完全相同的代碼捉偏,也是幾乎不可能的倒得。
另外,從安全的角度考慮夭禽,Xcode 引入了 Symbol Hiding
和 Debug info Striping
機(jī)制霞掺,在鏈接時(shí),bitcode中所有非導(dǎo)出符號(hào)均被隱藏讹躯,取而代之的是 __hidden#0_
或者 __ir_hidden#1_
這樣的形式菩彬,debug信息也只保留了line-table缠劝,所有跟文件路徑、標(biāo)識(shí)符骗灶、導(dǎo)出符號(hào)等相關(guān)的信息全部都從bitcode中移除惨恭,相當(dāng)于做了一層混淆锣笨,防止源代碼級(jí)別的信息泄露决采,可謂是煞費(fèi)苦心。