目錄
傳統(tǒng)編譯器設(shè)計(jì)
輸入源代碼(
Obj-C
,Swift
, ...) → 編譯器處理 → 輸出機(jī)器碼(010101
)編譯器處理分為以下步驟
前端 (Frontend)
負(fù)責(zé)解析源代碼铭段,進(jìn)行:
詞法分析
語法分析骤宣,語義分析,檢查源代碼是否有錯誤序愚,構(gòu)建 抽象語法樹 (
Abstract Syntax Tree
, AST)
優(yōu)化器 (Optimizer)
負(fù)責(zé)進(jìn)行各種優(yōu)化憔披。例如消除冗余計(jì)算 (甚至直接將方法優(yōu)化成一個固定值,而不去調(diào)用方法)等爸吮。
后端 (Backend)
將代碼映射到目標(biāo)指令集芬膝。生成機(jī)器語言,此過程會再次優(yōu)化 (機(jī)器語言層面)形娇。
LLVM 的設(shè)計(jì)
從圖里看出锰霜,編譯器前端輸入源代碼,后端輸出機(jī)器碼桐早。因?yàn)閭鹘y(tǒng)編譯器是按照整體程序設(shè)計(jì)的锈遥,所以總共需要做 n×m 個編譯器。
-
LLVM
使用通用的代碼表現(xiàn)形式 (IR
勘畔,可以理解為中間碼)所灸,優(yōu)化器的出入口都是IR
,所以LLVM
可以為任何編程語言獨(dú)立編寫前端炫七,為任何硬件架構(gòu)獨(dú)立編寫后端爬立,工作量縮減為 n+m,且能集中力量不斷提升優(yōu)化器性能万哪。
Clang 編譯流程
Clang
是LLVM
的一個子項(xiàng)目侠驯。它屬于整個LLVM
架構(gòu)的編譯器 前端抡秆,負(fù)責(zé)編譯C
、C++
吟策、Objective-C
儒士。
運(yùn)行命令,打印源碼編譯階段
運(yùn)行命令clang -ccc-print-phases main.m
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
- 0:輸入文件:找到源文件
- 1:預(yù)處理:替換宏檩坚,但不會替換別名
typedef
着撩;頭文件導(dǎo)入并展開,包括頭文件的頭文件匾委,代碼行數(shù)激增 - 2:編譯:詞法分析 (切割成一個個詞拖叙,不檢查語法錯誤)、語法分析 (組裝詞赂乐,檢查語法錯誤)薯鳍、最終生成
IR
- 3:后端:
LLVM
通過一個個Pass
(類似節(jié)點(diǎn)) 去優(yōu)化,每個Pass
有自己的優(yōu)化方式挨措,最終生成匯編代碼 - 4:把匯編文件變成
.o
文件 - 5:各個
.o
文件有聯(lián)系挖滤,需要進(jìn)行鏈接,生成Mach-O
文件 - 6:對應(yīng)不同架構(gòu)浅役,生成對應(yīng)的
Mach-O
文件
1: 預(yù)處理
-
main.m
文件#import <stdio.h> #define a 10 typedef int MD_INT_64; int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... MD_INT_64 b = 20; printf("sum = %d", a + b + 50); } return 0; }
-
運(yùn)行命令
clang -E main.m >> main1.cpp
壶辜,如果不輸入>> main1.cpp
,則不會新生成文件担租,而直接在命令行工具打印砸民。以下省略前面549行代碼 ↓typedef int MD_INT_64; int main(int argc, const char * argv[]) { @autoreleasepool { MD_INT_64 b = 20; printf("sum = %d", 10 + b + 50); } return 0; }
2.1: 編譯-詞法分析 (切割詞)
運(yùn)行命令
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
-
第幾行,第幾個字符開始奋救,第幾個字符結(jié)束岭参,一目了然。只截取了一些 ↓
// insert' Loc=<main.m:9:1> typedef 'typedef' [StartOfLine] Loc=<main.m:13:1> int 'int' [LeadingSpace] Loc=<main.m:13:9> identifier 'MD_INT_64' [LeadingSpace] Loc=<main.m:13:13> semi ';' Loc=<main.m:13:22> int 'int' [StartOfLine] Loc=<main.m:15:1> identifier 'main' [LeadingSpace] Loc=<main.m:15:5> l_paren '(' Loc=<main.m:15:9> int 'int' Loc=<main.m:15:10> identifier 'argc' [LeadingSpace] Loc=<main.m:15:14>
2.2: 編譯-語法分析 (重新組合尝艘,生成抽象語法樹)
運(yùn)行命令
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
如果導(dǎo)入了iOS特有的頭文件演侯,需要修改一下指令 (僅供參考,每個人電腦路徑和模擬器版本不一樣)
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk??????SDK?????? -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
經(jīng)過重新組合背亥,語法分析出來的代碼行數(shù)通常會比詞法分析短一些秒际,譬如詞法分析里的
int
、argc
狡汉,在語法分析里變成一行這是一個名叫argc的int類型參數(shù)
娄徊。最好帶著棧思維去讀抽象語法樹。只截取了一些 ↓|-TypedefDecl 0x7fd405845368 <line:13:1, col:13> col:13 referenced MD_INT_64 'int' | `-BuiltinType 0x7fd405036700 'int' `-FunctionDecl 0x7fd405845640 <line:15:1, line:22:1> line:15:5 main 'int (int, const char **)' |-ParmVarDecl 0x7fd4058453d8 <col:10, col:14> col:14 argc 'int' |-ParmVarDecl 0x7fd4058454f0 <col:20, col:38> col:33 argv 'const char **':'const char **' `-CompoundStmt 0x7fd4050f1ad8 <col:41, line:22:1> |-ObjCAutoreleasePoolStmt 0x7fd4050f1a90 <line:16:5, line:20:5> | `-CompoundStmt 0x7fd4050f1a70 <line:16:22, line:20:5> | |-DeclStmt 0x7fd4050f1868 <line:18:9, col:25> | | `-VarDecl 0x7fd4050f1400 <col:9, col:23> col:19 used b 'MD_INT_64':'int' cinit | | `-IntegerLiteral 0x7fd4050f1468 <col:23> 'int' 20 | `-CallExpr 0x7fd4050f1a10 <line:19:9, col:38> 'int' | |-ImplicitCastExpr 0x7fd4050f19f8 <col:9> 'int (*)(const char *, ...)' <FunctionToPointerDecay> | | `-DeclRefExpr 0x7fd4050f1880 <col:9> 'int (const char *, ...)' Function 0x7fd4050f1490 'printf' 'int (const char *, ...)'
2.3 / 3.0: 生成中間碼 IR (Intermediate Representation) / Pass 優(yōu)化
代碼生成器 (
Code Generation
) 會將語法樹自頂向下遍歷盾戴,翻譯成LLVM IR
運(yùn)行命令
clang -S -fobjc-arc -emit-llvm main.m
寄锐,獲得main.ll
文件。和匯編有點(diǎn)像。只截取了main
函數(shù) ↓-
IR
基本語法@ 全局標(biāo)識
% 局部標(biāo)識
alloca 開辟空間
align 內(nèi)存對齊
i32 32個bit橄仆,共4個字節(jié)
store 寫入內(nèi)存
load 讀內(nèi)存的數(shù)據(jù)
call 調(diào)用函數(shù)
ret 返回define i32 @main(i32, i8**) #0 { %3 = alloca i32, align 4 %4 = alloca i32, align 4 %5 = alloca i8**, align 8 %6 = alloca i32, align 4 store i32 0, i32* %3, align 4 store i32 %0, i32* %4, align 4 store i8** %1, i8*** %5, align 8 %7 = call i8* @llvm.objc.autoreleasePoolPush() #1 store i32 20, i32* %6, align 4 %8 = load i32, i32* %6, align 4 %9 = add nsw i32 10, %8 %10 = add nsw i32 %9, 50 %11 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str, i64 0, i64 0), i32 %10) call void @llvm.objc.autoreleasePoolPop(i8* %7) ret i32 0 }
-
剛才是沒有優(yōu)化的剩膘,看看優(yōu)化的,
LLVM
的優(yōu)化級別分別為-O0
-O1
-O2
-03
-Os
盆顾,我們試試-Os
怠褐,運(yùn)行命令clang -Os -S -fobjc-arc -emit-llvm main.m
,獲得main.ll
文件您宪。print
函數(shù)的參數(shù)奈懒,直接用絕對值80
,而不像剛才用局部變量算來算去蚕涤。只截取了main
函數(shù) ↓define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 { %3 = tail call i8* @llvm.objc.autoreleasePoolPush() #1 %4 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str, i64 0, i64 0), i32 80) #3, !clang.arc.no_objc_arc_exceptions !9 tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #1 ret i32 0 }
-
這個優(yōu)化級別在
Xcode
可以調(diào):Build Settings
→Code Generation
。Debug
模式下為了編譯快點(diǎn)一般不優(yōu)化铣猩,選None [-O0]
LLVM
的優(yōu)化使用了叫Pass
的東西揖铜,可以理解為優(yōu)化節(jié)點(diǎn),每個節(jié)點(diǎn)負(fù)責(zé)不同的優(yōu)化事項(xiàng) (跳轉(zhuǎn)达皿、運(yùn)算等)天吓,一個個Pass
搞下來,邏輯處理發(fā)生變化峦椰,就完成了優(yōu)化龄寞。如果想玩LLVM
優(yōu)化可以試試寫Pass
。
Pass
能使FuncA→FuncB→FuncC
變成FuncA→FuncC
甚至FuncA(算好的值)
汤功;也能使FuncA→FuncB
變成FuncA→FuncX→FuncY→FuncB
物邑,變得復(fù)雜,做到混淆效果滔金。不光是邏輯色解,其中的局部標(biāo)識也能增加。直接混淆還能看懂些餐茵,優(yōu)化完以后再混淆就真的難看懂科阎。
2.4: Bitcode
Xcode7
以后,Enable Bitcode
蘋果會在IR
的基礎(chǔ)上做進(jìn)一步的優(yōu)化忿族,生成.bc
代碼锣笨。
iOS端:
Bitcode
可選
watchOS端:Bitcode
必選
macOS端:Bitcode
不可選
- 運(yùn)行命令
clang -emit-llvm -c main.ll -o main.bc
。.bc
文件暫時不知道怎么打開道批,沒有截圖错英。
3.1: 生成匯編代碼 (屬于 后端Backend / 代碼生成器CodeGenerator)
匯編代碼可以由.ll
或.bc
代碼生成。
運(yùn)行命令
clang -S -fobjc-arc main.bc -o main.s
或運(yùn)行命令
clang -S -fobjc-arc main.ll -o main.s
這里也能優(yōu)化 (機(jī)器語言層面)
clang -Os -S -fobjc-arc main.m -o main.s
-
只截取部分代碼 ↓
subq $48, %rsp movl $0, -4(%rbp) movl %edi, -8(%rbp) movq %rsi, -16(%rbp) callq _objc_autoreleasePoolPush movl $20, -20(%rbp)
4: 生成目標(biāo)文件 .o
匯編器將匯編代碼轉(zhuǎn)換為機(jī)器代碼隆豹,這就是.o
文件 (object file
)走趋。
運(yùn)行命令
clang -fmodules -c main.s -o main.o
-
運(yùn)行命令
xcrun nm -nm main.o
,查看main.o
中的符號-
undefined
,當(dāng)前文件暫時找不到 -
external
簿煌,這個符號在外部找 (我們自己內(nèi)部沒有)
(undefined) external _objc_autoreleasePoolPop (undefined) external _objc_autoreleasePoolPush (undefined) external _printf 0000000000000000 (__TEXT,__text) external _main
-
5. 生成可執(zhí)行文件 Mach-O
鏈接器 (Linker
) 把.o
文件和.dylib
.a
文件 生成一個Mach-O
文件氮唯。
現(xiàn)在是編譯階段,這個
Linker
不是dyld
姨伟,dyld
是運(yùn)行時的事情惩琉。
-
運(yùn)行命令
clang main.o -o main
友情提示:如果是上面一路跟下來的,這里會因?yàn)檎也坏?code>@autoreleasepool報(bào)錯夺荒,請去掉源碼里的
@autoreleasepool
再跟一下) 文件變大了瞒渠,
main.s
1KB,main
13KB-
運(yùn)行命令
xcrun nm -nm main
技扼,查看main
中的符號伍玖。(undefined) external _printf (from libSystem) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100000f73 (__TEXT,__text) external _main 0000000100002008 (__DATA,__data) non-external __dyld_private
上面是編譯階段,下面要講的是運(yùn)行階段(
dyld
相關(guān))的事情剿吻。雖然printf
仍然是undefined
窍箍,但這只是一個標(biāo)示,后面寫了(from libSystem)
丽旅,意味著當(dāng)程序跑起來的時候椰棘,自己沒有printf
,它是個external
外部函數(shù)榄笙,找libSystem
邪狞,剛好iOS操作系統(tǒng)有libSystem
,在那里找到printf
的地址以后茅撞,進(jìn)行符號綁定就OK了帆卓。-
運(yùn)行命令
./main
,執(zhí)行程序sum = 80%
-
運(yùn)行命令米丘,
file main
鳞疲,看文件類型和架構(gòu)main: Mach-O 64-bit executable x86_64