前言
一般可以將編程語言分為兩種蔬捷,編譯語言和直譯式語言榔袋。
像C++,Objective C都是編譯語言周拐。編譯語言在執(zhí)行的時(shí)候,必須先通過編譯器生成機(jī)器碼,機(jī)器碼可以直接在CPU上執(zhí)行锦秒,所以執(zhí)行效率較高生真。
像JavaScript,Python都是直譯式語言蚜厉。直譯式語言不需要經(jīng)過編譯的過程,而是在執(zhí)行的時(shí)候通過一個中間的解釋器將代碼解釋為CPU可以執(zhí)行的代碼恬汁。所以甘苍,較編譯語言來說廊佩,直譯式語言效率低一些茁计,但是編寫的更靈活,也就是為啥JS大法好。
iOS開發(fā)目前的常用語言是:Objective和Swift。二者都是編譯語言,換句話說都是需要編譯才能執(zhí)行的。二者的編譯都是依賴于Clang + LLVM. 篇幅限制春缕,本文只關(guān)注Objective C宅荤,因?yàn)樵砩洗笸‘悺?/p>
Clang和LLVM
不管是OC還是Swift惫确,都是采用Clang作為編譯器前端枉昏,LLVM(Low level vritual machine)作為編譯器后端晰奖。所以簡單的編譯過程如下:
Clang編譯過程
預(yù)處理: 預(yù)處理器會處理源文件中的宏定義午衰,將代碼中的宏用其對應(yīng)定義的具體內(nèi)容進(jìn)行替換,刪除注釋崖技,展開頭文件,產(chǎn)生 .i 文件。
詞法分析:預(yù)處理完成了以后写烤,開始詞法分析,這里會把代碼切成一個個 Token,比如大小括號,等于號還有字符串等隧膘。
語法分析: 語法分析萨驶,在 Clang 中由 Parser 和 Sema 兩個模塊配合完成核畴,驗(yàn)證語法是否正確冀宴,根據(jù)當(dāng)前語言的語法,生成語意節(jié)點(diǎn),并將所有節(jié)點(diǎn)組合成抽象語法樹 AST。
靜態(tài)分析: 一旦編譯器把源碼生成了抽象語法樹站绪,編譯器可以對這棵樹做分析處理,以找出代碼中的錯誤丽柿,比如類型檢查:即檢查程序中是否有類型錯誤。例如:如果代碼中給某個對象發(fā)送了一個消息馁筐,編譯器會檢查這個對象是否實(shí)現(xiàn)了這個消息(函數(shù)炎码、方法)。此外饲握,clang 對整個程序還做了其它更高級的一些分析蹬刷,以確保程序沒有錯誤搂漠。
類型檢查:一般會把類型檢查分為兩類:動態(tài)的和靜態(tài)的迂卢。動態(tài)的在運(yùn)行時(shí)做檢查,靜態(tài)的在編譯時(shí)做檢查惊科。以往,編寫代碼時(shí)可以向任意對象發(fā)送任何消息亮钦,在運(yùn)行時(shí)馆截,才會檢查對象是否能夠響應(yīng)這些消息。由于只是在運(yùn)行時(shí)做此類檢查蜂莉,所以叫做動態(tài)類型蜡娶。至于靜態(tài)類型,是在編譯時(shí)做檢查映穗。當(dāng)在代碼中使用 ARC 時(shí)窖张,編譯器在編譯期間,會做許多的類型檢查:因?yàn)榫幾g器需要知道哪個對象該如何使用蚁滋。
目標(biāo)代碼的生成與優(yōu)化: CodeGen 負(fù)責(zé)將語法樹 AST 叢頂至下遍歷宿接,翻譯成 LLVM IR 中間碼赘淮,LLVM IR 中間碼編譯過程的前端的輸出后端的輸入。編譯器后端主要包括代碼生成器睦霎、代碼優(yōu)化器梢卸。代碼生成器將中間代碼轉(zhuǎn)換為目標(biāo)代碼,代碼優(yōu)化器主要是進(jìn)行一些優(yōu)化副女,比如刪除多余指令蛤高,選擇合適尋址方式等,如果開啟了 bitcode 蘋果會做進(jìn)一步的優(yōu)化碑幅,有新的后端架構(gòu)還是可以用這份優(yōu)化過的 bitcode 去生成戴陡。優(yōu)化中間代碼生成輸出匯編代碼,把之前的 .i 文件轉(zhuǎn)換為匯編語言沟涨,產(chǎn)生 .s 文件.
LLVM編譯過程
匯編: 目標(biāo)代碼需要經(jīng)過匯編器處理恤批,把匯編語言文件轉(zhuǎn)換為機(jī)器碼文件,產(chǎn)生 .o 文件拷窜。
鏈接: 對 .o 文件中的對于其他的庫的引用的地方進(jìn)行引用开皿,生成最后的可執(zhí)行文件(同時(shí)也包括多個 .o 文件進(jìn)行 link)。鏈接又分為靜態(tài)鏈接和動態(tài)鏈接篮昧。
- 靜態(tài)鏈接:在編譯鏈接期間發(fā)揮作用赋荆,把目標(biāo)文件和靜態(tài)庫一起鏈接形成可執(zhí)行文件.
- 動態(tài)鏈接:鏈接過程推遲到運(yùn)行時(shí)再進(jìn)行.
如果多個程序都用到了一個庫,那么每個程序都要將其鏈接到可執(zhí)行文件中懊昨,非常冗余窄潭,動態(tài)鏈接的話,多個程序可以共享同一段代碼酵颁,不需要在磁盤上存多份拷貝嫉你,但是動態(tài)鏈接發(fā)生在啟動或運(yùn)行時(shí),增加了啟動時(shí)間躏惋,造成一些性能的影響幽污。
靜態(tài)庫不方便升級,必須重新編譯簿姨,動態(tài)庫的升級更加方便距误。
代碼案列
上面總結(jié)了編譯的流程,接下來我們用實(shí)際的代碼來看看具體的轉(zhuǎn)化流程.首先創(chuàng)建一個main.m文件
#import <Foundation/Foundation.h>
//來個注釋
#define DEBUG 1
int main(){
#ifdef DEBUG
NSLog(@"DEBUG模式");
#else
NSLog(@"RELEASE模式");
#endif
return 0;
}
預(yù)處理
預(yù)處理器會處理源文件中的宏定義,將代碼中的宏用其對應(yīng)定義的具體內(nèi)容進(jìn)行替換扁位,刪除注釋准潭,展開頭文件,產(chǎn)生 .i 文件域仇。
'#import <Foundation/Foundation.h>'這一行是告訴預(yù)處理器將這行用Foundation.h中的內(nèi)容替換.這個過程是遞歸的,因?yàn)镕oundation.h中也import了其他文件.使用clang查看預(yù)處理結(jié)果
xcrun clang -E main.m
與處理后的文件會有很多代碼.其中基本上都是引用的其他文件然后被遞歸替換的內(nèi)容.劃到最底部可以看到main函數(shù).
int main(){
NSLog(@"DEBUG模式");
return 0;
}
同時(shí),我們也可以發(fā)現(xiàn),在這個階段,我們所寫的注釋被刪除,條件編譯也被處理了.
詞法分析
詞法分析器讀入源文件的字符流刑然,將他們組織稱有意義的詞素(lexeme)序列,對于每個詞素暇务,此法分析器產(chǎn)生詞法單元(token)作為輸出泼掠。
$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
輸出
annot_module_include '#import <Foundation/Foundation.h>
//' Loc=<main.m:2:1>
int 'int' [StartOfLine] Loc=<main.m:5:1>
identifier 'main' [LeadingSpace] Loc=<main.m:5:5>
l_paren '(' Loc=<main.m:5:9>
r_paren ')' Loc=<main.m:5:10>
l_brace '{' Loc=<main.m:5:11>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<main.m:7:5>
l_paren '(' Loc=<main.m:7:10>
at '@' Loc=<main.m:7:11>
string_literal '"DEBUG模式"' Loc=<main.m:7:12>
r_paren ')' Loc=<main.m:7:25>
semi ';' Loc=<main.m:7:26>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:11:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:11:12>
semi ';' Loc=<main.m:11:13>
r_brace '}' [StartOfLine] Loc=<main.m:12:1>
eof '' Loc=<main.m:12:2>
Loc=<main.m:2:1>標(biāo)示這個token位于源文件main.m的第2行怔软,從第1個字符開始。保存token在源文件中的位置是方便后續(xù)clang分析的時(shí)候能夠找到出錯的原始位置武鲁。
語法分析
語法分析爽雄,在 Clang 中由 Parser 和 Sema 兩個模塊配合完成,驗(yàn)證語法是否正確沐鼠,根據(jù)當(dāng)前語言的語法挚瘟,生成語意節(jié)點(diǎn),并將所有節(jié)點(diǎn)組合成抽象語法樹 AST.簡單點(diǎn)來說,就是將詞法分析的Token流會被解析成一顆抽象語法樹.
$ xcrun clang -fsyntax-only -Xclang -ast-dump main.m | open -f
得到的AST結(jié)構(gòu),部分如下
?[0;34m| |-?[0m?[0;32mBuiltinType?[0m?[0;33m 0x7fa22903ae60?[0m ?[0;32m'void'?[0m
?[0;34m| |-?[0m?[0;32mAttributedType?[0m?[0;33m 0x7fa22a204fc0?[0m ?[0;32m'id _Nullable'?[0m sugar
?[0;34m| | |-?[0m?[0;32mTypedefType?[0m?[0;33m 0x7fa22a204310?[0m ?[0;32m'id'?[0m sugar
?[0;34m| | | |-?[0m?[0;1;32mTypedef?[0m?[0;33m 0x7fa22903b898?[0m?[0;1;36m 'id'?[0m
?[0;34m| | | `-?[0m?[0;32mObjCObjectPointerType?[0m?[0;33m 0x7fa22903b840?[0m ?[0;32m'id'?[0m
?[0;34m| | | `-?[0m?[0;32mObjCObjectType?[0m?[0;33m 0x7fa22903b810?[0m ?[0;32m'id'?[0m
?[0;34m| | `-?[0m?[0;32mTypedefType?[0m?[0;33m 0x7fa22a204310?[0m ?[0;32m'id'?[0m sugar
?[0;34m| | |-?[0m?[0;1;32mTypedef?[0m?[0;33m 0x7fa22903b898?[0m?[0;1;36m 'id'?[0m
?[0;34m| | `-?[0m?[0;32mObjCObjectPointerType?[0m?[0;33m 0x7fa22903b840?[0m ?[0;32m'id'?[0m
?[0;34m| | `-?[0m?[0;32mObjCObjectType?[0m?[0;33m 0x7fa22903b810?[0m ?[0;32m'id'?[0m
?[0;34m| `-?[0m?[0;32mAttributedType?[0m?[0;33m 0x7fa22a3925f0?[0m ?[0;32m'NSError * _Nullable'?[0m sugar
?[0;34m| |-?[0m?[0;32mObjCObjectPointerType?[0m?[0;33m 0x7fa22a3925b0?[0m ?[0;32m'NSError *'?[0m
?[0;34m| | `-?[0m?[0;32mObjCInterfaceType?[0m?[0;33m 0x7fa22a103b30?[0m ?[0;32m'NSError'?[0m
?[0;34m| | `-?[0m?[0;1;32mObjCInterface?[0m?[0;33m 0x7fa22a527de0?[0m?[0;1;36m 'NSError'?[0m
?[0;34m| `-?[0m?[0;32mObjCObjectPointerType?[0m?[0;33m 0x7fa22a3925b0?[0m ?[0;32m'NSError *'?[0m
?[0;34m| `-?[0m?[0;32mObjCInterfaceType?[0m?[0;33m 0x7fa22a103b30?[0m ?[0;32m'NSError'?[0m
?[0;34m| `-?[0m?[0;1;32mObjCInterface?[0m?[0;33m 0x7fa22a527de0?[0m?[0;1;36m 'NSError'?[0m
有了抽象語法樹饲梭,Clang就可以對這個樹進(jìn)行分析乘盖,找出代碼中的錯誤。Clang Static Analyzer是開源編譯器前端clang中內(nèi)置的針對C憔涉,C++和Objective-C源代碼的靜態(tài)分析工具订框,能提供普通warning之外的檢查,涵蓋內(nèi)存操作兜叨,安全等方面穿扳。這部分功能可通過clang --analyze命令或者庫文件等方式調(diào)用.由于需要實(shí)現(xiàn)checker.這一步我們先過掉.有興趣的話可以在做研究.
目標(biāo)代碼的生成與優(yōu)化
CodeGen 會負(fù)責(zé)將語法樹自頂向下遍歷逐步翻譯成 LLVM IR,IR 是編譯過程的前端的輸出国旷,也是后端的輸入矛物。 Objective C代碼也在這一步會進(jìn)行runtime的橋接:property合成,ARC處理等跪但。
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
得到的的main.ll內(nèi)容而下
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [8 x i16] [i16 68, i16 69, i16 66, i16 85, i16 71, i16 27169, i16 24335, i16 0], section "__TEXT,__ustring", align 2
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 2000, i8* bitcast ([8 x i16]* @.str to i8*), i64 7 }, section "__DATA,__cfstring", align 8 #0
; Function Attrs: noinline optnone ssp uwtable
define i32 @main() #1 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
ret i32 0
}
declare void @NSLog(i8*, ...) #2
attributes #0 = { "objc_arc_inert" }
attributes #1 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [3 x i32] [i32 10, i32 15, i32 4]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 11.0.3 (clang-1103.0.32.62)"}
中間代碼生成后,需要將LLVM代碼轉(zhuǎn)化為匯編語言,生成.s文件交給后面的匯編器處理.
clang -S -fobjc-arc main.m -o main.s
使用上面命令行的到匯編文件,部分內(nèi)容如下
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 4
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
匯編
目標(biāo)代碼需要經(jīng)過匯編器處理履羞,把匯編語言文件轉(zhuǎn)換為機(jī)器碼文件,產(chǎn)生 .o 文件(object file)屡久。
clang -fmodules -c main.m -o main.o
使用命令行查看main.o文件
nm -nm main.o
輸出
(undefined) external _NSLog
(undefined) external ___CFConstantStringClassReference
0000000000000000 (__TEXT,__text) external _main
0000000000000028 (__TEXT,__ustring) non-external l_.str
這里可以看到_NSLog是一個是undefined external的忆首。undefined表示在當(dāng)前文件暫時(shí)找不到符號_NSLog,而external表示這個符號是外部可以訪問的被环,對應(yīng)表示文件私有的符號是non-external糙及。
鏈接生成可執(zhí)行文件
拿到.o機(jī)器碼文件后,需要對 .o 文件中的對于其他的庫的引用的地方進(jìn)行引用,生成最后的match-o可執(zhí)行文件.
clang main.o -o main
當(dāng)然,這個命令行是封裝完成的.內(nèi)部是使用
cc main.o -framework Foundation
來鏈接其他庫的.
最終可以拿到我們的執(zhí)行文件.運(yùn)行 ./
得到輸出結(jié)果 "DEBUG模式".
我們查看可執(zhí)行文件的符號表
U _NSLog
U ___CFConstantStringClassReference
0000000100002008 d __dyld_private
0000000100000000 T __mh_execute_header
0000000100000f50 T _main
U dyld_stub_binder
關(guān)于match-o文件里的符號表的解釋,會專門在出一篇文章來做解釋.
從上我們可以大致了解了,iOS代碼帶match-o可執(zhí)行文件的整個過程.
了解這些知識后,在深入研究可以解決很多問題,譬如:
- 自動化打包;
- 在拿到AST后對代碼規(guī)范進(jìn)行review;
- 提高項(xiàng)目編譯速度
...