關(guān)鍵詞:
LLVM,Clang,Swiftc,IR,preprocessor,Mach-O,dyld
編譯器
把一種編程語(yǔ)言(原始語(yǔ)言)轉(zhuǎn)換為另一種編程語(yǔ)言(目標(biāo)語(yǔ)言)的程序叫做編譯器.
大多數(shù)編譯器由兩部分組成: 前端和后端.
前端負(fù)責(zé)詞法分析,語(yǔ)法分析,生成中間代碼;
后端以中間代碼作為輸入,進(jìn)行行架構(gòu)無關(guān)的代碼優(yōu)化,接著針對(duì)不同架構(gòu)生成不同的機(jī)器碼。
前后端依賴統(tǒng)一格式的中間代碼(IR), 使得前后端可以獨(dú)立的變化. 新增一門語(yǔ)言只需要修改前端, 而新增一個(gè)CPU架構(gòu)只需要修改后端即可. Objective C/C/C++使用的編譯器前端是clang, swift是swift, 后端都是LLVM.
一、LLVM
LLVM的核心庫(kù)提供了現(xiàn)代化的source-target-independent優(yōu)化器和支持諸多流行CPU架構(gòu)的代碼生成器. Clang 和 LLDB都是基于LLVM衍生的子項(xiàng)目.
二、Clang
Clang是C語(yǔ)言家族的編譯器前端,誕生之初是為了替代GCC,提供更快的編譯速度。一張圖了解clang編譯的大致流程:
大致看來, Clang可以分為一下幾個(gè)步驟:
預(yù)處理 -> 詞法分析 -> 語(yǔ)法分析 -> 靜態(tài)分析 -> 生成中間代碼和優(yōu)化 -> 匯編 -> 鏈接
1、預(yù)處理(preprocessor)
預(yù)處理會(huì)進(jìn)行如下操作:
1)頭文件引入, 遞歸將頭文件引用替換為頭文件中的實(shí)際內(nèi)容, 所以盡量減少頭文件中的#import, 使用@class替代, 把#import放到.m文件中.
2)宏替換, 在源碼中使用的宏定義會(huì)被替換為對(duì)應(yīng)#define的內(nèi)容, 不要在需要預(yù)處理的代碼中加入太多的內(nèi)聯(lián)代碼邏輯.
3)注釋處理, 在預(yù)處理的時(shí)候, 注釋被刪除
4)條件編譯, (#if, #else, #endif)
2、詞法分析(lexical anaysis)
這一步把源文件中的代碼轉(zhuǎn)化為特殊的標(biāo)記流. 詞法分析器讀入源文件的字符流, 將他們組織稱有意義的詞素(lexeme)序列,對(duì)于每個(gè)詞素,此法分析器產(chǎn)生詞法單元(token)作為輸出.
源碼被分割成一個(gè)一個(gè)的字符和單詞, 在行尾Loc中都標(biāo)記出了源碼所在的對(duì)應(yīng)源文件和具體行數(shù), 方便在報(bào)錯(cuò)時(shí)定位問題. 類似于下面:
int 'int' [StartOfLine] Loc=<main.m:14:1>
identifier 'main' [LeadingSpace] Loc=<main.m:14:5>
l_paren '(' Loc=<main.m:14:9>
int 'int' Loc=<main.m:14:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:14:14>
comma ',' Loc=<main.m:14:18>
char 'char' [LeadingSpace] Loc=<main.m:14:20>
star '*' [LeadingSpace] Loc=<main.m:14:25>
3、語(yǔ)法分析(semantic analysis)
詞法分析的Token流會(huì)被解析成一顆抽象語(yǔ)法樹(abstract syntax tree - AST). 在這里面每一節(jié)點(diǎn)也都標(biāo)記了其在源碼中的位置.
有了抽象語(yǔ)法樹,clang就可以對(duì)這個(gè)樹進(jìn)行分析,找出代碼中的錯(cuò)誤。比如類型不匹配,亦或Objective C中向target發(fā)送了一個(gè)未實(shí)現(xiàn)的消息.
AST是開發(fā)者編寫clang插件主要交互的數(shù)據(jù)結(jié)構(gòu),clang也提供很多API去讀取AST.
4、靜態(tài)分析(CodeGen)
把源碼轉(zhuǎn)化為抽象語(yǔ)法樹之后,編譯器就可以對(duì)這個(gè)樹進(jìn)行分析處理。靜態(tài)分析會(huì)對(duì)代碼進(jìn)行錯(cuò)誤檢查,如出現(xiàn)方法被調(diào)用但是未定義、定義但是未使用的變量等,以此提高代碼質(zhì)量. 也可以使用 Xcode 自帶的靜態(tài)分析工具(Product -> Analyze).
常見的操作有:
1)當(dāng)在代碼中使用 ARC 時(shí),編譯器在編譯期間,會(huì)做許多的類型檢查. 最常見的是檢查程序是否發(fā)送正確的消息給正確的對(duì)象,是否在正確的值上調(diào)用了正常函數(shù)。如果你給一個(gè)單純的 NSObject* 對(duì)象發(fā)送了一個(gè) hello 消息,那么 clang 就會(huì)報(bào)錯(cuò),同樣,給屬性設(shè)置一個(gè)與其自身類型不相符的對(duì)象,編譯器會(huì)給出一個(gè)可能使用不正確的警告.
一般會(huì)把類型分為兩類:動(dòng)態(tài)的和靜態(tài)的。動(dòng)態(tài)的在運(yùn)行時(shí)做檢查,靜態(tài)的在編譯時(shí)做檢查。以往,編寫代碼時(shí)可以向任意對(duì)象發(fā)送任何消息,在運(yùn)行時(shí),才會(huì)檢查對(duì)象是否能夠響應(yīng)這些消息。由于只是在運(yùn)行時(shí)做此類檢查,所以叫做動(dòng)態(tài)類型。
至于靜態(tài)類型,是在編譯時(shí)做檢查。當(dāng)在代碼中使用 ARC 時(shí),編譯器在編譯期間,會(huì)做許多的類型檢查:因?yàn)榫幾g器需要知道哪個(gè)對(duì)象該如何使用。
2)檢查是否有定義了,但是從未使用過的變量.
3)檢查在 你的初始化方法中中調(diào)用 self 之前, 是否已經(jīng)調(diào)用 [self initWith…] 或 [super init] 了.
此處遍歷語(yǔ)法樹,最終生成LLVM IR代碼。LLVM IR是前端的輸出,后端的輸入. Objective C代碼在這一步會(huì)進(jìn)行runtime的橋接:property合成,ARC處理等
- LLVM 會(huì)去做些優(yōu)化工作, 在 Xcode 的編譯設(shè)置里也可以設(shè)置優(yōu)化級(jí)別-01,-03,-0s,還可以寫些自己的 Pass.
- 如果開啟了 Bitcode 蘋果會(huì)做進(jìn)一步的優(yōu)化. 雖然Bitcode僅僅只是一個(gè)中間碼不能在任何平臺(tái)上運(yùn)行, 但是它可以轉(zhuǎn)化為任何被支持的CPU架構(gòu), 包括現(xiàn)在還沒被發(fā)明的CPU架構(gòu). iOS Apps中Enable Bitcode 為可選項(xiàng), WatchOS和tvOS, Bitcode必須開啟. 如果你的App支持Bitcode, App Bundle(項(xiàng)目中所有的target)中的所有的 Apps 和 frameworks 都需要支持Bitcode.
5、生成匯編指令
LLVM對(duì)IR進(jìn)行優(yōu)化后,會(huì)對(duì)代碼進(jìn)行編譯優(yōu)化例如針對(duì)全局變量?jī)?yōu)化、循環(huán)優(yōu)化、尾遞歸優(yōu)化等, 然后會(huì)針對(duì)不同架構(gòu)生成不同的目標(biāo)代碼,最后以匯編代碼的格式輸出.
6、匯編
在這一階段,匯編器將上一步生成的可讀的匯編代碼轉(zhuǎn)化為機(jī)器代碼。最終產(chǎn)物就是 以 .o 結(jié)尾的目標(biāo)文件。使用Xcode構(gòu)建的程序會(huì)在DerivedData目錄中找到這個(gè)文件.
Tips:什么是符號(hào)(Symbols)? 符號(hào)就是指向一段代碼或者數(shù)據(jù)的名稱。還有一種叫做WeakSymols,也就是并不一定會(huì)存在的符號(hào),需要在運(yùn)行時(shí)決定。比如iOS 12特有的API,在iOS11上就沒有.
7、鏈接
目標(biāo)文件(.o)和引用的庫(kù)(dylib,a,tbd)鏈接起來, 最終生成可執(zhí)行文件(mach-o), 鏈接器解決了目標(biāo)文件和庫(kù)之間的鏈接.
這時(shí)可執(zhí)行文件的符號(hào)表信息已經(jīng)有了, 會(huì)在運(yùn)行時(shí)動(dòng)態(tài)綁定.
8、Mach-O文件
Mach-O是OS X中二進(jìn)制文件的原生可執(zhí)行格式,是傳送代碼的首選格式??蓤?zhí)行格式?jīng)Q定了二進(jìn)制文件中的代碼和數(shù)據(jù)讀入內(nèi)存的順序。代碼和數(shù)據(jù)的順序會(huì)影響內(nèi)存使用和分頁(yè)活動(dòng),從而直接影響程序的性能.
Mach-O是記錄編譯后的可執(zhí)行文件,對(duì)象代碼,共享庫(kù),動(dòng)態(tài)加載代碼和內(nèi)存轉(zhuǎn)儲(chǔ)的文件格式。不同于 xml 這樣的文件,它只是二進(jìn)制字節(jié)流,里面有不同的包含元信息的數(shù)據(jù)塊,比如字節(jié)順序,cpu 類型,塊大小等。文件內(nèi)容是不可以修改的,因?yàn)樵?.app 目錄中有個(gè) _CodeSignature 的目錄,里面包含了程序代碼的簽名,這個(gè)簽名的作用就是保證簽名后 .app 里的文件,包括資源文件,Mach-O 文件都不能夠更改.
Mach-O結(jié)構(gòu)
Mach-O 文件包含三個(gè)區(qū)域:
Mach-O Header: 包含字節(jié)順序,magic,cpu 類型,加載指令的數(shù)量等.
Load Commands: 包含很多內(nèi)容的表,包括區(qū)域的位置,符號(hào)表,動(dòng)態(tài)符號(hào)表等。每個(gè)加載指令包含一個(gè)元信息,比如指令類型,名稱,在二進(jìn)制中的位置等.
Data: 最大的部分,包含了代碼,數(shù)據(jù),比如符號(hào)表,動(dòng)態(tài)符號(hào)表等.
Mach-O文件的結(jié)構(gòu)如下:
Header
保存了Mach-O的一些基本信息,包括了平臺(tái)、文件類型、LoadCommands的個(gè)數(shù)等等.
使用otool -v -h a.out查看其內(nèi)容:
Load commands
這一段緊跟Header,加載Mach-O文件時(shí)會(huì)使用這里的數(shù)據(jù)來確定內(nèi)存的分布
Data
包含 Load commands 中需要的各個(gè) segment,每個(gè) segment 中又包含多個(gè) section。當(dāng)運(yùn)行一個(gè)可執(zhí)行文件時(shí),虛擬內(nèi)存 (virtual memory) 系統(tǒng)將 segment 映射到進(jìn)程的地址空間上.
使用xcrun size -x -l -m a.out查看segment中的內(nèi)容:
Segment __PAGEZERO。
大小為 4GB,規(guī)定進(jìn)程地址空間的前 4GB 被映射為不可讀不可寫不可執(zhí)行。Segment __TEXT。
包含可執(zhí)行的代碼,以只讀和可執(zhí)行方式映射。Segment __DATA。
包含了將會(huì)被更改的數(shù)據(jù),以可讀寫和不可執(zhí)行方式映射。Segment __LINKEDIT。
包含了方法和變量的元數(shù)據(jù),代碼簽名等信息。
9、dyld動(dòng)態(tài)鏈接
生成可執(zhí)行文件后就是在啟動(dòng)時(shí)進(jìn)行動(dòng)態(tài)鏈接了, 進(jìn)行符號(hào)和地址的綁定. 首先會(huì)加載所依賴的 dylibs,修正地址偏移,因?yàn)?iOS 會(huì)用 ASLR 來做地址偏移避免攻擊,確定 Non-Lazy Pointer 地址進(jìn)行符號(hào)地址綁定,加載所有類,最后執(zhí)行 load 方法和 clang attribute 的 constructor 修飾函數(shù).
10、dSYM
在每次編譯后都會(huì)生成一個(gè) dSYM 文件,程序在執(zhí)行中通過地址來調(diào)用方法函數(shù),而 dSYM 文件里存儲(chǔ)了函數(shù)地址映射,這樣調(diào)用棧里的地址可以通過 dSYM 這個(gè)映射表能夠獲得具體函數(shù)的位置。一般都會(huì)用來處理 crash 時(shí)獲取到的調(diào)用棧 .crash 文件將其符號(hào)化.
當(dāng)release的版本 crash的時(shí)候,會(huì)有一個(gè)日志文件,包含出錯(cuò)的內(nèi)存地址, 使用symbolicatecrash工具能夠把日志和dSYM文件轉(zhuǎn)換成可以閱讀的log信息,也就是將內(nèi)存地址,轉(zhuǎn)換成程序里的函數(shù)或變量和所屬于的 文件名.
相關(guān)參考