前言
我們會(huì)經(jīng)常聽到編譯器這個(gè)詞語,我們就會(huì)想什么是編譯器,它的功能是什么飞袋,跟我們的開發(fā)又有什么關(guān)系,這篇文章就帶大家走入LLVM編譯器架構(gòu)链患,揭開編譯器的神秘面紗巧鸭。
1 什么是編譯器
我們用Python(解釋型)和C(編譯型)來先對(duì)比下
Python代碼如下
print("hello world\n")
我們通過python py1.py命令執(zhí)行下,看下效果麻捻,如圖
python是python的解釋器纲仍,這個(gè)就是解釋型語言的效果。
我們再來看C贸毕,代碼如下
#include<stdio.h>
int main(int argc,char * argv[]){
printf("hello world\n");
return 0;
}
我們通過命令clang hello.c郑叠,效果如下
我們看到并沒有執(zhí)行,而在我們的文件中多了一個(gè)a.out文件明棍,在unix下乡革,這是個(gè)可執(zhí)行文件,我們再通過./a.out執(zhí)行下摊腋,效果如圖
我們看到了執(zhí)行效果沸版。
從這兩個(gè)小小的案例可以看出,解釋型語言和編譯型語言的區(qū)別兴蒸,
解釋型語言讀取代碼就會(huì)執(zhí)行推穷,而編譯型語言要先翻譯成cpu可以讀的二進(jìn)制代碼。
我們剛才的用的clang命令就是C,C++和Objective-C的編譯器类咧。
python就是python的解釋器馒铃。
我們今天就從clang這個(gè)編譯器開始說起蟹腾。
2 LLVM介紹
LLVM概述
LLVM是構(gòu)架編譯器(compiler)的框架系統(tǒng),以C++編寫而成区宇,用于優(yōu)化以任意程序語言編寫的程序的編譯時(shí)間(compile-time)娃殖、鏈接時(shí)間(link-time)、運(yùn)行時(shí)間(run-time)以及空閑時(shí)間(idle-time)议谷,對(duì)開發(fā)者保持開話炉爆,并兼容已有腳本。
LLVM計(jì)劃啟動(dòng)于2000年卧晓,最初由由美國UIUC大學(xué)的Chris Lattner博士主持開展芬首。2006年Chris Lattner加盟Apple Inc.并致力LLVM在Apple開發(fā)體系中的應(yīng)用。Apple也是LLVM計(jì)劃的主要資助者逼裆。
目前LLVM已經(jīng)被蘋果iOS開發(fā)工具郁稍、Xilinx Vivado、Facebook胜宇、Google等各大公司采用耀怜。
傳統(tǒng)編譯器設(shè)計(jì)
編譯器前端(Frontend)
編譯器前端的任務(wù)是解析源代碼。它會(huì)進(jìn)行:詞法分析桐愉、語法分析财破、檢查源代碼是否存在錯(cuò)誤,然后構(gòu)建抽象語法樹(Abstract Syntax Tree AST),LLVM的前端還會(huì)生成中間代碼(intermediate representation,IR)
優(yōu)化器(Optimizer)
優(yōu)化器負(fù)責(zé)進(jìn)行各種優(yōu)化从诲。改善代碼的運(yùn)行時(shí)間左痢,例始消除冗余計(jì)算ac等。
后端(Backend)/代碼生成器(CodeGenerator)
將代碼映財(cái)?shù)侥繕?biāo)指令集系洛。生成機(jī)器語言俊性,并且進(jìn)行機(jī)器相關(guān)的代碼優(yōu)化。
iOS的編譯器架構(gòu)
Objcective C/C/C++使用的編譯器前端是Clang碎罚,Swift是Swift磅废,后端都是LLVM纳像。
LLVM的設(shè)計(jì)
當(dāng)編譯器決定支持多種源語言或多種硬架構(gòu)時(shí)荆烈,LLVM的最重要的地方就來了。
其它的編對(duì)器如GCC竟趾,它方法非常成功憔购,但由于它是作為整體應(yīng)用程序設(shè)計(jì)的,因此它的用途受到了很大的限制岔帽。
LLVM設(shè)計(jì)的最重要方便是玫鸟,使用通用的代碼表示形式(IR ),它是用來在編譯器中表示代碼的形式。所以LLVM可以為任何編譯語言獨(dú)立編寫前端犀勒,并且可以為任意硬件架構(gòu)獨(dú)立編寫后端屎飘。
Clang是LLVM項(xiàng)目的中的一個(gè)子項(xiàng)目妥曲。它是基于LLVM架構(gòu)的輕量編譯器,誕生之初是為了替代GCC,提供更快的編譯速度钦购。它是負(fù)責(zé)編譯C檐盟、C++、Objective-C語言的編譯器押桃,它屬于整個(gè)LLVM架構(gòu)中的葵萎,編譯器前端。對(duì)于開發(fā)者來說唱凯,研究Clang可以給我們帶來很多好處羡忘。
3 編譯流程分析
我們先看下一段代碼,如下
#import <stdio.h>
int main(int argc, const char * argv[]) {
return 0;
}
我們通過命令clang -ccc-print-phases main.m執(zhí)行
我們看編譯的流程是什么樣的磕昼。
- +- 0: input, "main.m", objective-c 讀取代碼卷雕。
- +- 1: preprocessor, {0}, objective-c-cpp-output 預(yù)處理價(jià)段,把宏替換掰烟,.h的導(dǎo)入進(jìn)去爽蝴。
- +- 2: compiler, {1}, ir 編譯價(jià)段,前端編譯器的任務(wù)纫骑。
- +- 3: backend, {2}, assembler 編譯器后端蝎亚,pass(環(huán)節(jié),節(jié)點(diǎn))優(yōu)化先馆,生成匯編代碼发框。
- +- 4: assembler, {3}, object 生成目標(biāo)文件。
- +- 5: linker, {4}, image 鏈接外部函數(shù)煤墙,靜態(tài)庫梅惯,動(dòng)態(tài)庫,生成鏡像文件即可執(zhí)行文件
- bind-arch, "x86_64", {5}, image 根據(jù)不同的架構(gòu)生成不同的鏡像文件仿野。
編譯流程的分析
1. 讀取代碼
讀取我們編寫的源代碼铣减。
2. 預(yù)處理
我們改下源碼,如
#import <stdio.h>
#define C 30
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
printf("%d",a + b +C);
return 0;
}
接著執(zhí)行clang -E main.m >> main1.m脚作,我們看下main1.m文件葫哗,
# 1 "main.m"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 379 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.m" 2
這里是宏展開,我們看下main函數(shù)
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
printf("%d",a + b +30);
return 0;
}
直接把我們的C這個(gè)宏展開直接替換成30球涛。
我們還用過typedef劣针,我們改下代碼
#import <stdio.h>
typedef int RO_INT_64
int main(int argc, const char * argv[]) {
RO_INT_64 a = 10;
RO_INT_64 b = 20;
printf("%d",a + b);
return 0;
}
執(zhí)行clang -E main.m >> main1.m,如
typedef int RO_INT_64
int main(int argc, const char * argv[]) {
RO_INT_64 a = 10;
RO_INT_64 b = 20;
printf("%d",a + b);
return 0;
}
沒有展開亿扁,typedef只是取別名捺典,增強(qiáng)可讀性,不是預(yù)處理指令从祝。
3.編譯價(jià)段
3.1詞法分析
我們再執(zhí)行命令clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m襟己,詞法分析引谜,會(huì)把代碼切成token,如下所示
annot_module_include '#import <stdio.h>
#d' Loc=<main.m:2:1>
int 'int' [StartOfLine] Loc=<main.m:4:1>
identifier 'main' [LeadingSpace] Loc=<main.m:4:5>
l_paren '(' Loc=<main.m:4:9>
int 'int' Loc=<main.m:4:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:4:14>
comma ',' Loc=<main.m:4:18>
const 'const' [LeadingSpace] Loc=<main.m:4:20>
char 'char' [LeadingSpace] Loc=<main.m:4:26>
star '*' [LeadingSpace] Loc=<main.m:4:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:4:33>
l_square '[' Loc=<main.m:4:37>
r_square ']' Loc=<main.m:4:38>
r_paren ')' Loc=<main.m:4:39>
l_brace '{' [LeadingSpace] Loc=<main.m:4:41>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:5:5>
identifier 'a' [LeadingSpace] Loc=<main.m:5:9>
equal '=' [LeadingSpace] Loc=<main.m:5:11>
numeric_constant '10' [LeadingSpace] Loc=<main.m:5:13>
semi ';' Loc=<main.m:5:15>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:6:5>
identifier 'b' [LeadingSpace] Loc=<main.m:6:9>
equal '=' [LeadingSpace] Loc=<main.m:6:11>
numeric_constant '20' [LeadingSpace] Loc=<main.m:6:13>
semi ';' Loc=<main.m:6:15>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:7:5>
l_paren '(' Loc=<main.m:7:11>
string_literal '"%d"' Loc=<main.m:7:12>
comma ',' Loc=<main.m:7:16>
identifier 'a' Loc=<main.m:7:17>
plus '+' [LeadingSpace] Loc=<main.m:7:19>
identifier 'b' [LeadingSpace] Loc=<main.m:7:21>
plus '+' [LeadingSpace] Loc=<main.m:7:23>
numeric_constant '30' Loc=<main.m:7:24 <Spelling=main.m:3:11>>
r_paren ')' Loc=<main.m:7:25>
semi ';' Loc=<main.m:7:26>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:8:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:8:12>
semi ';' Loc=<main.m:8:13>
r_brace '}' [StartOfLine] Loc=<main.m:9:1>
eof '' Loc=<main.m:9:2>
會(huì)把代碼切成token擎浴,比如大小括號(hào)煌张,等于號(hào)還有字符串等。
3.2語法分析
檢查語法是否正確退客,在詞法分析的基礎(chǔ)上將單詞序列組合成各類語法短語骏融,如“程序”,“語句”萌狂,“表達(dá)式”等等档玻,然后將所有節(jié)點(diǎn)組成抽像語法樹(Abstract Syntax Tree,AST)。語法分析程序判斷源程序在結(jié)構(gòu)上是否正確茫藏。
我們執(zhí)行clang -fmodules -fsyntax-only -Xclang -ast-dump main.m,
我們把代碼改錯(cuò)误趴,看下效果
這里有錯(cuò)誤提示。
我分析下語法樹
-FunctionDecl 0x7f9aed0bee00 <line:5:1, line:10:1> line:5:5 main 'int (int, const char **)'
|-ParmVarDecl 0x7f9aed01e140 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7f9aed01e288 <col:20, col:38> col:33 argv 'const char **':'const char **'
`-CompoundStmt 0x7f9aed0bf7d0 <col:41, line:10:1>
|-DeclStmt 0x7f9aed0bf010 <line:6:5, col:21>
| `-VarDecl 0x7f9aed0bef88 <col:5, col:19> col:15 used a 'RO_INT_64':'int' cinit
| `-IntegerLiteral 0x7f9aed0beff0 <col:19> 'int' 10
|-DeclStmt 0x7f9aed0bf538 <line:7:5, col:21>
| `-VarDecl 0x7f9aed0bf038 <col:5, col:19> col:15 used b 'RO_INT_64':'int' cinit
| `-IntegerLiteral 0x7f9aed0bf0a0 <col:19> 'int' 20
|-CallExpr 0x7f9aed0bf740 <line:8:5, col:25> 'int'
| |-ImplicitCastExpr 0x7f9aed0bf728 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f9aed0bf550 <col:5> 'int (const char *, ...)' Function 0x7f9aed0bf0c8 'printf' 'int (const char *, ...)'
| |-ImplicitCastExpr 0x7f9aed0bf788 <col:12> 'const char *' <NoOp>
| | `-ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d"
| `-BinaryOperator 0x7f9aed0bf6b0 <col:17, line:3:11> 'int' '+'
| |-BinaryOperator 0x7f9aed0bf670 <line:8:17, col:21> 'int' '+'
| | |-ImplicitCastExpr 0x7f9aed0bf640 <col:17> 'RO_INT_64':'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7f9aed0bf5d0 <col:17> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bef88 'a' 'RO_INT_64':'int'
| | `-ImplicitCastExpr 0x7f9aed0bf658 <col:21> 'RO_INT_64':'int' <LValueToRValue>
| | `-DeclRefExpr 0x7f9aed0bf608 <col:21> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bf038 'b' 'RO_INT_64':'int'
| `-IntegerLiteral 0x7f9aed0bf690 <line:3:11> 'int' 30
`-ReturnStmt 0x7f9aed0bf7c0 <line:9:5, col:12>
`-IntegerLiteral 0x7f9aed0bf7a0 <col:12> 'int' 0
- FunctionDecl 0x7f9aed0bee00 <line:5:1, line:10:1> line:5:5 main 'int (int, const char )'
|-ParmVarDecl 0x7f9aed01e140 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7f9aed01e288 <col:20, col:38> col:33 argv 'const char ':'const char ** 這里就是main函數(shù)务傲,返回值int凉当,參數(shù)int和char,參數(shù)名稱arc,int類型,參數(shù)argv const char類型 - |-CallExpr 0x7f9aed0bf740 <line:8:5, col:25> 'int'
| |-ImplicitCastExpr 0x7f9aed0bf728 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f9aed0bf550 <col:5> 'int (const char *, ...)' Function 0x7f9aed0bf0c8 'printf' 'int (const char *, ...)'這里有一個(gè)函數(shù)的調(diào)用printf售葡,返回int類型看杭。 - |-ImplicitCastExpr 0x7f9aed0bf788 <col:12> 'const char *' <NoOp>
| |-ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay> | |
-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d" 這是第一個(gè)參數(shù) - |-DeclStmt 0x7f9aed0bf010 <line:6:5, col:21>
|-VarDecl 0x7f9aed0bef88 <col:5, col:19> col:15 used a 'RO_INT_64':'int' cinit |
-IntegerLiteral 0x7f9aed0beff0 <col:19> 'int' 10
|-DeclStmt 0x7f9aed0bf538 <line:7:5, col:21>
| `-VarDecl 0x7f9aed0bf038 <col:5, col:19> col:15 used b 'RO_INT_64':'int' 這里是a,b - | |
-ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay> | |
-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d"這是第一個(gè)參數(shù) - BinaryOperator 0x7f9aed0bf6b0 <col:17, line:3:11> 'int' '+'是+運(yùn)算結(jié)果,
- BinaryOperator 0x7f9aed0bf670 <line:8:17, col:21> 'int' '+'
| | |-ImplicitCastExpr 0x7f9aed0bf640 <col:17> 'RO_INT_64':'int' <LValueToRValue>
| | |-DeclRefExpr 0x7f9aed0bf5d0 <col:17> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bef88 'a' 'RO_INT_64':'int' | |
-ImplicitCastExpr 0x7f9aed0bf658 <col:21> 'RO_INT_64':'int' <LValueToRValue>
| |-DeclRefExpr 0x7f9aed0bf608 <col:21> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bf038 'b' 'RO_INT_64':'int' |
-IntegerLiteral 0x7f9aed0bf690 <line:3:11> 'int' 30 第一個(gè)加法運(yùn)算的結(jié)果+30 - ReturnStmt 0x7f9aed0bf7c0 <line:9:5, col:12> 這里是返回
- 返回int類型值為0
3.4 生成中間代碼(intermediate representation )
代碼生成器(Code Generation)會(huì)將語法樹自頂向下遍歷逐步翻譯成LLVM IR挟伙。
IR基本語法
@全局標(biāo)識(shí)
%局部標(biāo)識(shí)
alloca 開辟空間
align 內(nèi)存對(duì)齊
i32 32個(gè)bit
store寫入內(nèi)存
load讀取數(shù)據(jù)
call調(diào)用函數(shù)
ret返回
我們改下代碼
#import <stdio.h>
#define C 30
typedef int RO_INT_64;
int test(int a, int b) {
return a+ b +3;
}
int main(int argc, const char * argv[]) {
int a = test(1, 2);
printf("%d", a);
return 0;
}
我們執(zhí)行命令clang -S -fobjc-arc -emit-llvm main.m楼雹,會(huì)生成main.ll文件,我們看下main.ll文件內(nèi)容
define i32 @test(i32 %0, i32 %1) #0 { #test(int a, int b )
%3 = alloca i32, align 4 #開辟空間 4字節(jié)對(duì)齊 int a3;
%4 = alloca i32, align 4 #開辟空間 4字節(jié)對(duì)齊 int a4;
store i32 %0, i32* %3, align 4 # a3=a;
store i32 %1, i32* %4, align 4 # a4=b;
%5 = load i32, i32* %3, align 4 # int a5=a3;
%6 = load i32, i32* %4, align 4 # int a6=a4;
%7 = add nsw i32 %5, %6 # int a7 = a5+a6;
%8 = add nsw i32 %7, 3 # int a8= a7+ 3;
ret i32 %8 # return a8;
}
這就是test函數(shù)IR代碼尖阔,這是沒有經(jīng)過優(yōu)化的贮缅。
IR的優(yōu)化
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
經(jīng)過優(yōu)化會(huì)簡潔很多,這里不再贅述介却。
xcode中的Optimization Level可以設(shè)置谴供。
bitCode
clang -emit-llvm -c main.ll -o main.bc
4 生成匯編代碼
我們通過最終的.bc或者.ll代碼生成匯編代碼
命令
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
生成匯編代碼也可以進(jìn)行優(yōu)化
clang -Os -S -fobjc-arc main.m -o main.s
執(zhí)行命令
clang -S -fobjc-arc main.ll -o main.s
_test: ## @test
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
addl -8(%rbp), %eax
addl $3, %eax
popq %rbp
retq
.cfi_endproc
## -- End function
.globl _main ## -- Begin function main
.p2align 4, 0x90
這是x86的匯編指令集。
我們再執(zhí)行這個(gè)clang -Os -S -fobjc-arc main.m -o main.s優(yōu)化的命令
_test: ## @test
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
## kill: def $esi killed $esi def $rsi
## kill: def $edi killed $edi def $rdi
leal 3(%rdi,%rsi), %eax
popq %rbp
retq
.cfi_endproc
## -- End function
.globl _main ## -- Begin function main
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
leaq L_.str(%rip), %rdi
movl $6, %esi
這是經(jīng)過優(yōu)化過的齿坷,main的函數(shù)調(diào)用的test直接優(yōu)化成了6桂肌。
5 生成目標(biāo)文件(匯編器)
目標(biāo)文件的生成,是匯編器以匯編代碼作為輸入胃夏,將匯編代碼轉(zhuǎn)換為機(jī)器代碼轴或,最后輸了目標(biāo)文件(object file)昌跌。這里屬于后端的作務(wù)仰禀。
執(zhí)行命令clang -fmodules -c main.s -o main.o,生成的main.o就是目標(biāo)文件蚕愤。通過xcrun nm -nm main.o查看符號(hào)答恶,如下所示
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _test
000000000000000a (__TEXT,__text) external _main
_printf是一個(gè)undefined external的符號(hào)饺蚊。
undefined表示當(dāng)前文件暫時(shí)找不到符號(hào)。
external表示這個(gè)符號(hào)是外部可以訪問的悬嗓。
5 生成可執(zhí)行文件(鏈接)
連接器把編譯產(chǎn)生的.o文件和(.dylib.a)文件污呼,生成一個(gè)macho-o文件。
我們執(zhí)行命令clang main.o -o main生成了可執(zhí)行文件main包竹。
我們再通過命令xcrun nm -nm main燕酷,如下
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f6d (__TEXT,__text) external _test
0000000100003f77 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private
這里有兩個(gè)外部符號(hào)_printf(可以找到)和dyld_stub_binder。
當(dāng)我們的程序進(jìn)入內(nèi)存的的時(shí)候周瞎,外部函數(shù)會(huì)立即跟dyld_stub_binder綁定苗缩,這個(gè)dyld是強(qiáng)制執(zhí)行,鏈接是打個(gè)標(biāo)記声诸,符號(hào)在哪個(gè)庫中(編譯期)酱讶,綁定是在執(zhí)行的時(shí)候把外部函數(shù)地址和符號(hào)進(jìn)行綁定(運(yùn)行期),一定會(huì)有dyld_stub_binder這個(gè)符號(hào)彼乌,先綁定這個(gè)符號(hào)泻肯,其它函數(shù)的綁定由dyld_stub_binder執(zhí)行。
總結(jié)編譯器的流程:
- 前端:讀取代碼慰照,詞法分析灶挟,語法分析,語義分析毒租,生成AST(生成IR)
- 優(yōu)化器:根據(jù)一個(gè)個(gè)的pass進(jìn)行優(yōu)化膏萧,
- 后端:生成匯編,根據(jù)不同的架構(gòu)生成可執(zhí)行文件
LLVM最大的好處:前后端分離蝌衔。
pass的解釋:就是“遍歷一遍IR榛泛,可以同時(shí)對(duì)它做一些操作”的意思。翻譯成中文應(yīng)該叫“趟”噩斟。 在實(shí)現(xiàn)上曹锨,LLVM的核心庫中會(huì)給你一些 Pass類 去繼承。你需要實(shí)現(xiàn)它的一些方法剃允。 最后使用LLVM的編譯器會(huì)把它翻譯得到的IR傳入Pass里沛简,給你遍歷和修改
總結(jié)
這篇文章帶大家初步了解了編譯器的原理,LLVM的架構(gòu)斥废。分析了編譯的流程椒楣,希望這篇文章可以讓大家學(xué)習(xí)到新的知識(shí)。