這篇文章是關(guān)于Sunny大神在MDCC 2016 的 topic 《把玩編譯器,Clang有意思》的學(xué)習(xí)筆記及實踐。
相關(guān)鏈接:視頻 PPT
Apple 編譯器采用的是 Clang-LLVM 架構(gòu),Clang 作為編譯器前端,LLVM 作為編譯器后端,整體的架構(gòu)如圖:

采用這樣的架構(gòu)是因為,如果只有一個整體的編譯過程,面對程序員編寫的 M 種高級語言,面對不同機器所對應(yīng)的 N 種可執(zhí)行文件,我們需要 M*N 種編譯器……
若是分為前后端,我們可以將 M 種高級語言編譯為一個機器無關(guān)的中間代碼,作為前后端的橋接語言,再交給不同編譯器后端生成各種機器所需要的目標(biāo)機器代碼,大大簡化了編譯過程。
現(xiàn)在,我們來看代碼是怎么一步步變成可執(zhí)行文件的。
1.Preprocess - 預(yù)處理
處理‘#’開頭的預(yù)處理指令,包括 import 頭文件(將頭文件內(nèi)容逐字替換 import 語句)、macro(宏) 展開、條件預(yù)處理指令,刪除注釋,添加行號和文件名標(biāo)識。
現(xiàn)在嘗試預(yù)處理一個文件,看看是什么樣子:
$clang -E main.m

...lots of codes

相關(guān)問題:每個頭文件中都 import 基礎(chǔ)庫(Foundation等)或第三方庫頭文件,這些文件重復(fù)編譯,代碼量非常大,如上圖而且不夠整潔優(yōu)雅。
優(yōu)化:
- 可用pch文件將這些庫文件預(yù)編譯,加快編譯速度。
- 或是當(dāng)引入蘋果自己的庫時,可采用 @import 關(guān)鍵字引用這些庫,告訴編譯器去使用 modules 的引用形式。蘋果已經(jīng)將一些基礎(chǔ)庫進(jìn)行了封裝,生成一個已編譯的 modules 文件列表,我們編譯時,會首先從已編譯文件里面尋找,若已存在這個編譯文件,直接使用;若沒有,再添加進(jìn)來進(jìn)行編譯。

2.Lexical Analysis - 詞法分析
將預(yù)處理后的代碼文本拆成 Token 流,并不進(jìn)行語義校驗。
$clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
//執(zhí)行到詞法分析這一步,并將 -dump-tokens 透傳給編譯器前端,將token打出來

3.Semantic Analysis - 語法分析
由 Clang 中 Parser 和 Sema 配合完成
- 驗證語法是否正確
- 提示各種錯誤警告提示
- 根據(jù)設(shè)置語言的語法,形成語義結(jié)點,并將所有節(jié)點組合形成抽象語法樹AST
$clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
//生成抽象語法樹

另外,這步之后,在我們Run一個工程時,如果選擇Analyze,這里會進(jìn)行
Static Analysis - 靜態(tài)分析
找出一些非語法性錯誤、若需要隱式轉(zhuǎn)換,會在語法樹中插入相應(yīng)的轉(zhuǎn)換節(jié)點。


這里,我試圖用 copy 修飾一個可變對象,這樣會造成這個屬性雖然叫 “mutableArray”,但是它存儲著一個不可變的對象。
4.CodeGen - IR 代碼生成
語法樹從頂至下遍歷,翻譯成LLVM 中間代碼,作為前后端的橋接語言,是Clang 編譯器前端的輸出,LLVM 編譯器后端的輸入。
中間代碼一般已經(jīng)非常接近目標(biāo)代碼了,但跟目標(biāo)機器和運行時環(huán)境無關(guān)。
同時,一個重要的作用是與 OC Runtime 進(jìn)行橋接
- 內(nèi)存結(jié)構(gòu)的生成:
- Class/Meta Class/Protocol/Category 生成并存放在指定section中,_DATA 或 _objc_classrefs
- Method/Ivar/Property 生成
- 組成method_list/ivar_list/property_list 并填入Class
- 為每個 Ivar 合成偏移值常量,其地址為對象的基地址 + 偏移量
- 將語法樹中的ObjCMessageExpr翻譯成相應(yīng)objc_msgSeng,對super關(guān)鍵字的調(diào)用翻譯成objc_msgSendSuper
- 根據(jù)修飾符strong/weak/copy/atomic 合成@property,自動實現(xiàn)setter/getter,處理@synthesize
- 生成block_layout數(shù)據(jù)結(jié)構(gòu)
變量的capture _block _weak
生成_block_invoke 函數(shù) - 分析對象引用關(guān)系,插入ARC代碼
自動調(diào)用[super dealloc]
為每個擁有ivar 的 Class 合成 .cxx_destructor 方法來自動釋放類的成員變量
自動釋放池的管理,將ObjcAutoreleasePoolStmt 轉(zhuǎn)譯成 objc_autoreleasePoolPush/Pop
$clang -S -fobjc-arc -emit-llvm main.m -o main.ll
//生成中間代碼

這里我們可以看到一些熟悉的身影,比如 @objc_msgSend...
5.Optimize - 優(yōu)化
$clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll
//可采用不同優(yōu)化級別優(yōu)化中間代碼


LLVM Bitcode - 生成字節(jié)碼
字節(jié)碼是一種包含執(zhí)行程序、由一序列 op 代碼/數(shù)據(jù)對組成的二進(jìn)制文件,但與特定機器碼無關(guān),需要直譯器轉(zhuǎn)譯后才能生成機器碼,可以看作是包含一個執(zhí)行程序的二進(jìn)制文件。
$clang -emit-llvm -c main.m -o main.bc
//形成二進(jìn)制流

6. Assemble - 生成 Target 相關(guān)匯編
$clang -S -fobjc-arc main.m -o main.s
//生成匯編代碼

Assemble - 生成Target相關(guān)Object(Mach-o)
$clang -fmodules -c main.m -o main.o
//Mach-o 是蘋果系統(tǒng)的目標(biāo)文件

可以看到,生成的目標(biāo)文件有 Mach Header 頭部、Load Commands 加載命令、Section 區(qū)域、 Relocations 重定位信息、Symbol 符號表、String字符串表等。
一個 mach_header 標(biāo)記一些元信息,比如架構(gòu)、CPU、大小端等信息
多個 Load Command 表示如何加載每個段的信息
-
多個 Segment 及 Section 包含每個段自身的信息,包括數(shù)據(jù)、代碼等
- Common Segments 段包含
__PAGEZERO : Catch 訪問NULL指針的非法操作段
__TEXT : 只讀數(shù)據(jù),只讀常量,C strings
__DATA : 全局/靜態(tài)變量
__LINKEDIT : 包含需要被動態(tài)連接器使用的信息,包括符號表、字符串表、重定位表項
- Common Segments 段包含
可以用MachoView來打開 .o 文件
MachoView GitHub
7. Link - 鏈接,生成 Executable 可執(zhí)行文件
$clang main.m -o main
$./main
//TODO

經(jīng)過這一步步,我們用各種高級語言編寫的代碼就轉(zhuǎn)換成了機器可以看懂可以執(zhí)行的目標(biāo)代碼了??????