編譯器入門

編譯器(compiler)就是一個翻譯其他程序的程序而已。傳統(tǒng)的編譯器將源代碼翻譯為計算機(jī)能夠理解的可執(zhí)行機(jī)器代碼(有一些編譯器將源代碼翻譯為另一種編程語言薛窥。這些編譯器叫做從源碼到源碼的翻譯器,source-to-source translators or transpilers)。LLVM 是一個廣泛使用的編譯器項(xiàng)目膝晾,它包含了許多模塊化的編譯器工具栓始。傳統(tǒng)編譯器涉及包含了三個部分:

traditional compiler design
  • 前端(frontend)將源代碼翻譯為一個中間表示(intermediate representation, IR)务冕。clang 是 LLVM 中 C 系語言的前端。

  • 優(yōu)化器(optimizer)會對 IR 進(jìn)行分析幻赚,并將其翻譯成一個更高效的形式禀忆。opt 是 LLVM 的優(yōu)化器工具。

  • 后端(backend)通過將 IR 映射為目標(biāo)硬件的指令集生成機(jī)器碼落恼。llc 是 LLVM 的后端工具箩退。

LLVM IR 是一個類似匯編語言的低級語言。但是佳谦,它將針對特定硬件的信息抽象了出去戴涝。

Hello, Compiler

下面是一個簡單的 C 程序,它只是向標(biāo)準(zhǔn)輸出打印出 “Hello, Compiler!”钻蔑。雖然人類可以讀懂 C 語言的語法啥刻,但是機(jī)器并不認(rèn)識它。我將通過三個編譯步驟咪笑,使得機(jī)器能夠執(zhí)行這個程序可帽。

// compile_me.c
// Wave to the compiler. The world can wait.

#include <stdio.h>

int main() {
  printf("Hello, Compiler!\n");
  return 0;
}

The Frontend

正如我上面提到的,clang 是 LLVM C 系語言的前端窗怒。clang 包含了一個 C 預(yù)處理器(preprocessor)映跟,詞法分析器(lexer),語法分析器(parser),semantic analyzer(語義分析器)和中間表示生成器(IR generator)蓄拣。

  • C 預(yù)處理器 在翻譯成 IR 之前對源代碼進(jìn)行修改。預(yù)處理器會將外部文件包含進(jìn)來努隙,比如上面的#include <stdio.h>球恤。它會用 C 標(biāo)準(zhǔn)庫文件 stdio.h 的所有內(nèi)容替換 #include <stdio.h> 這一行,stdio.h 包含了 printf 函數(shù)的聲明荸镊。

通過執(zhí)行下列命令來查看預(yù)處理器步驟的輸出:

clang -E compile_me.c -o preprocessed.i
  • 詞法分析器(lexer, 或者叫 scanner 或 tokenizer) 將一串字符轉(zhuǎn)換成一串詞碎捺。每個詞,或者叫 token (記號)贷洲,被分配到 5 個句法的類別:punctuation(標(biāo)點(diǎn)符號)收厨,keyword(關(guān)鍵詞),identifier(標(biāo)識符)优构,literal(常量) 或 comment(注釋)诵叁。

compile_me.c 的 tokenization:

tokennizaiton
  • 語法分析器決定了由詞法分析器生成的一串詞是否包含了源語言中的有效句。在分析完詞的語法以后钦椭,它輸出了一個抽象語法樹(abstract syntax tree, AST)拧额。一個 Clang AST 中的節(jié)點(diǎn)表示 declaration,statement, type.

compile_me.c 的 AST:

AST
  • 語義分析器遍歷 AST彪腔,判定代碼句的涵義是否有效侥锦。這個階段會檢查類型錯誤。如果 compile_me.c 中的 main 函數(shù)返回了 "zero" 而不是 0, 語義分析器就會拋出一個錯誤德挣,因?yàn)?"zero" 不是 int 類型恭垦。

  • IR 生成器 將 AST 翻譯為 IR。

在 compile_me.c 上運(yùn)行 clang 前端來生成 LLVM IR:

clang -S -emit-llvm -o llvm_ir.ll compile_me.c

在 llvm_ir.ll 中的 main 函數(shù):

; llvm_ir.ll

@.str = private unnamed_addr constant [18 x i8] c"Hello, Compiler!\0A\00", align 1

define i32 @main() {
  %1 = alloca i32, align 4 ; <- memory allocated on the stack
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}

declare i32 @printf(i8*, ...)

優(yōu)化器

優(yōu)化器的任務(wù)是格嗅,基于對程序運(yùn)行時行為的理解番挺,提升代碼的效率。優(yōu)化器的輸入為 IR屯掖,輸出為優(yōu)化后的 IR玄柏。LLVM 的優(yōu)化器工具,opt贴铜,將會使用 -O2 (大寫字母 o粪摘,2)標(biāo)志優(yōu)化處理器速度,-Os (大寫字母 o绍坝,s)優(yōu)化生成目標(biāo)的大小徘意。

來看一下優(yōu)化器優(yōu)化之前的 LLVM IR 代碼和優(yōu)化后的代碼:

opt -O2 llvm_ir.ll -o optimized.ll

optimized.ll 的 main 函數(shù):

; optimized.ll

@str = private unnamed_addr constant [17 x i8] c"Hello, Compiler!\00"

define i32 @main() {
  %puts = tail call i32 @puts(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @str, i64 0, i64 0))
  ret i32 0
}

declare i32 @puts(i8* nocapture readonly)

在優(yōu)化后的版本中,main 沒有在棧(stack)上分配內(nèi)存陷嘴,因?yàn)樗鼪]有使用任何內(nèi)存映砖。優(yōu)化后的代碼也沒有調(diào)用 printf, 而是調(diào)用了 puts,因?yàn)樗鼪]有用到 printf 的任何格式化功能灾挨。

當(dāng)然了邑退,優(yōu)化器知道的不僅僅是什么時候該用 puts 代替 printf. 優(yōu)化器也會對循環(huán)進(jìn)行展開竹宋,內(nèi)聯(lián)簡單計算的結(jié)果〉丶迹考慮下面的代碼蜈七,它將兩個數(shù)加起來并打印結(jié)果:

// add.c
#include <stdio.h>

int main() {
  int a = 5, b = 10, c = a + b;
  printf("%i + %i = %i\n", a, b, c);
}

這是未優(yōu)化的 LLVM IR:

@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1

define i32 @main() {
  %1 = alloca i32, align 4 ; <- allocate stack space for var a
  %2 = alloca i32, align 4 ; <- allocate stack space for var b
  %3 = alloca i32, align 4 ; <- allocate stack space for var c
  store i32 5, i32* %1, align 4  ; <- store 5 at memory location %1
  store i32 10, i32* %2, align 4 ; <- store 10 at memory location %2
  %4 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %4
  %5 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %5
  %6 = add nsw i32 %4, %5 ; <- add the values in registers %4 and %5. put the result in register %6
  store i32 %6, i32* %3, align 4 ; <- put the value of register %6 into memory address %3
  %7 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %7
  %8 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %8
  %9 = load i32, i32* %3, align 4 ; <- load the value at memory address %3 into register %9
  %10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %7, i32 %8, i32 %9)
  ret i32 0
}

declare i32 @printf(i8*, ...)

這是優(yōu)化后的 LLVM IR:

@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1

define i32 @main() {
  %1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0), i32 5, i32 10, i32 15)
  ret i32 0
}

declare i32 @printf(i8* nocapture readonly, ...)

優(yōu)化后的 main 函數(shù),本質(zhì)上就是未優(yōu)化版本的 17 和 18 行將變量進(jìn)行內(nèi)聯(lián)莫矗。opt 對加法進(jìn)行了計算飒硅,因?yàn)樗械淖兞慷际浅A俊:芸嶙餮瑁前桑?/p>

The Backend

LLVM 的后端工具是 llc.從 LLVM IR 輸入生成機(jī)器碼三娩,它經(jīng)歷了三個階段:

  • 指令選取(instruction selection) 是從 IR 指令到目標(biāo)機(jī)器指令集的映射妹懒。這一步使用了虛擬寄存器一個無限的命令空間雀监。

  • 寄存器分配(register allocation) 是從虛擬寄存器到目標(biāo)架構(gòu)上真實(shí)寄存器的映射。我的 CPU 是 x86 架構(gòu)眨唬,也就是說只能使用 16 個寄存器会前。但是,編譯器會選擇盡可能少地使用寄存器匾竿。

  • 指令調(diào)度(instruction scheduling) 是對操作的重新安排瓦宜,它反映了目標(biāo)機(jī)器上的性能限制。

執(zhí)行下面的命令將會產(chǎn)生一些機(jī)器碼岭妖!

llc -o compiled-assembly.s optimized.ll
_main:
    pushq   %rbp
    movq    %rsp, %rbp
    leaq    L_str(%rip), %rdi
    callq   _puts
    xorl    %eax, %eax
    popq    %rbp
    retq
L_str:
    .asciz  "Hello, Compiler!"

這個程序是 x86 匯編語言临庇,它是目標(biāo)機(jī)器能夠讀懂的語言的一個“人類表示”。目標(biāo)機(jī)器只能讀懂 0 和 1区转,匯編語言是將 0 1 代碼用人類能夠讀懂的方式表達(dá)了出來苔巨。相信肯定會有人懂的:).

資源:

  1. Engineering a compiler
  2. Getting Started with LLVM Core Libraries

本文譯自:An Intro to Compilers

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末版扩,一起剝皮案震驚了整個濱河市废离,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌礁芦,老刑警劉巖蜻韭,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異柿扣,居然都是意外死亡肖方,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門未状,熙熙樓的掌柜王于貴愁眉苦臉地迎上來俯画,“玉大人,你說我怎么就攤上這事司草〖璐梗” “怎么了泡仗?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長猜憎。 經(jīng)常有香客問我娩怎,道長,這世上最難降的妖魔是什么胰柑? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任截亦,我火速辦了婚禮,結(jié)果婚禮上柬讨,老公的妹妹穿的比我還像新娘崩瓤。我一直安慰自己,他們只是感情好踩官,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布谷遂。 她就那樣靜靜地躺著,像睡著了一般卖鲤。 火紅的嫁衣襯著肌膚如雪肾扰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天蛋逾,我揣著相機(jī)與錄音集晚,去河邊找鬼。 笑死区匣,一個胖子當(dāng)著我的面吹牛偷拔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播亏钩,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼莲绰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了姑丑?” 一聲冷哼從身側(cè)響起蛤签,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎栅哀,沒想到半個月后震肮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡留拾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年戳晌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痴柔。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡沦偎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情豪嚎,我是刑警寧澤鸿捧,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站疙渣,受9級特大地震影響匙奴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜妄荔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一泼菌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧啦租,春花似錦哗伯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恳蹲,卻和暖如春虐块,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嘉蕾。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工贺奠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人错忱。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓儡率,卻偏偏與公主長得像,于是被迫代替她去往敵國和親以清。 傳聞我的和親對象是個殘疾皇子儿普,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 2000年,伊利諾伊大學(xué)厄巴納-香檳分校(University of Illinois at Urbana-...
    星光社的戴銘閱讀 15,898評論 8 180
  • 編譯器做些什么掷倔? 本文主要探討一下編譯器主要做些什么眉孩,以及如何有效的利用編譯器。 簡單的說今魔,編譯器有兩個職責(zé):把 ...
    評評分分閱讀 1,133評論 1 5
  • —— 在 Siri 出現(xiàn)以前如何與計算機(jī)對話 譯者:penghuster作者:Nicole Orchard原文:...
    守拙圓閱讀 764評論 0 1
  • 夜出奇的黑勺像,厚厚的烏云藏了星星,壓住了月亮错森。 鳳英不敢點(diǎn)燈,一個人躲西廂房里篮洁,小心臟撲通撲通地跳涩维,用力攥著的兩只手...
    川巒之旅閱讀 252評論 0 2
  • 故事的開頭總是這樣,適逢其會,猝不及防瓦阐∥铣蓿或許我想了想一晃半年過去了,我還是忘不了她睡蟋。但想了在多踏幻,也逃脫不了我為她哭...
    梁叢閱讀 207評論 0 1