(轉(zhuǎn))關(guān)于bitcode, 知道這些就夠了

轉(zhuǎn)自 http://xelz.info/blog/2018/11/24/all-you-need-to-know-about-bitcode/,版權(quán)歸原作者所有

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)閎itcode是由LLVM引入的一種中間代碼(Intermediate Representation,簡(jiǎn)稱IR),它是源代碼被編譯為二進(jìn)制機(jī)器碼過程中的中間表示形態(tài),它既不是源代碼,也不是機(jī)器碼。從代碼組織結(jié)構(gòu)上看它比較接近機(jī)器碼,但是在函數(shù)和指令層面使用了很多高級(jí)語言的特性。

LLVM是一套優(yōu)秀的編譯器框架,目前NDK/Xcode均采用LLVM作為默認(rèn)的編譯器。LLVM的編譯過程可以簡(jiǎn)單分為3個(gè)部分:

圖來自 http://www.aosabook.org/en/llvm.html
  1. 前端(Frontend),負(fù)責(zé)把各種類型的源代碼編譯為中間表示,也就是bitcode,在LLVM體系內(nèi),不同的語言有不同的編譯器前端,最常見的如clang負(fù)責(zé)c/c++/oc的編譯,flang負(fù)責(zé)fortran的編譯,swiftc負(fù)責(zé)swift的編譯等等
  2. 優(yōu)化(Optimizer),負(fù)責(zé)對(duì)bitcode進(jìn)行各種類型的優(yōu)化,將bitcode代碼進(jìn)行一些邏輯等價(jià)的轉(zhuǎn)換,使得代碼的執(zhí)行效率更高,體積更小,比如DeadStrip/SimplifyCFG
  3. 后端(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è)簡(jiǎn)單的例子來一探究竟,這里以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 # 將源代碼編譯為L(zhǎng)LVM Assembly

test.ll的全部?jī)?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è)例子看起來非常簡(jiǎn)單易懂,但真實(shí)場(chǎng)景中,bitcode的語法遠(yuǎn)比這個(gè)復(fù)雜,有興趣的同學(xué)可以直接閱讀LLVM Language Reference Manual

0x03 Enable Bitcode

在對(duì)bitcode有了一個(gè)直觀的認(rèn)識(shí)之后,再來看一下Apple圍繞bitcode做了什么。Xcode中對(duì)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
  • 進(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

接下來看一下 Enable Bitcode 之后,編譯出的文件發(fā)生了什么變化, 直接在clang的參數(shù)中添加 -fembed-bitcode 即可

$ clang -fembed-bitcode -c test.c -o test_bitcode.o

編譯之后可以通過tool工具查看object文件的結(jié)構(gòu),此時(shí)你需要對(duì)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把對(duì)應(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)出的bitcode文件似乎和直接編譯的bitcode不一樣,先留個(gè)疑問,后面再研究

首先, 來測(cè)試一下導(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)行對(duì)比。前面已經(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對(duì)象,然后再執(zhí)行對(duì)應(yīng)的處理:

  • 如果輸入是源代碼,會(huì)先進(jìn)行前端編譯,得到一個(gè)Module
  • 如果輸入是bitcode或者LLVM Assembly,那么直接進(jìn)行parse操作,即可得到Module對(duì)象
  • 如果輸出類型是LLVM Assembly,將Module對(duì)象序列化為文本格式
  • 如果輸出類型是bitcode,則將Module對(duì)象序列化為二進(jìn)制格式

所以完全可以通過clang進(jìn)行bitcode和LLVM Assembly的相互轉(zhuǎn)換。

現(xiàn)在,可以對(duì)比一下前后兩次生成的.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對(duì)比可以看出鏈接時(shí)對(duì)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自己就可以編譯出針對(duì)新產(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)制的體積增大到超出了蘋果對(duì)二進(jìn)制體積的限制,但是完全不用擔(dān)心,蘋果的限制只是針對(duì)__TEXT 段,而嵌入的bitcode是存儲(chǔ)在單獨(dú)的__LLVM 段,不在蘋果的限制范圍內(nèi)。

打包出帶有bitcode的xcarchive之后,可以導(dǎo)出Development IPA進(jìn)行上線前的最終測(cè)試,或者上傳到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)制中去除以減小體積。

    rebuild from bitcode
  • 導(dǎo)出Store版本或者直接進(jìn)行上傳時(shí),默認(rèn)會(huì)勾選Include bitcode for iOS content,如果不勾選,則跟前面類似,將會(huì)去除內(nèi)嵌的bitcode,直接使用本地編譯的二進(jìn)制代碼

    upload

    勾選后生成的ipa中將會(huì)只包含bitcode,這個(gè)ipa是無法重簽后安裝到設(shè)備上進(jìn)行測(cè)試的,因?yàn)槔锩鏇]有任何可執(zhí)行代碼:

    __TEXT__DATA 等跟已編譯好的二進(jìn)制相關(guān)的內(nèi)容會(huì)被全部去除,但是會(huì)保留__LINKEDIT中的部分信息,其中最重要的就是 LC_UUID,用于在重編之后能跟原始的符號(hào)文件對(duì)應(yīng)起來,如果用戶下載經(jīng)過AppStore重編之后的app發(fā)生了Crash,得到的backtrace地址是跟本地編譯的版本對(duì)應(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ò)誤的,通過前面的測(cè)試可以知道,這種方式區(qū)分不了bitcode和bitcode-marker,確定是否包含bitcode,還需要檢查otool輸出中__LLVM Segment 的長(zhǎng)度,如果長(zhǎng)度只有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)制文件比要相對(duì)容易一些,但通過bitcode反編譯出和源代碼語義完全相同的代碼,也是幾乎不可能的。

另外,從安全的角度考慮,Xcode 引入了 Symbol HidingDebug 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)苦心。

原文License:
cc-by-nc
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容