手把手教你做一個(gè) C 語(yǔ)言編譯器設(shè)計(jì)

“手把手教你構(gòu)建 C 語(yǔ)言編譯器” 這一系列教程將帶你從頭編寫一個(gè) C 語(yǔ)言的編譯器奏瞬。希望通過(guò)這個(gè)系列吸祟,我們能對(duì)編譯器的構(gòu)建有一定的了解,同時(shí),我們也將構(gòu)建出一個(gè)能用的 C 語(yǔ)言編譯器恩沽,盡管有許多語(yǔ)法并不支持。

在開始進(jìn)入正題之前切心,本篇是一些閑聊飒筑,談?wù)勥@個(gè)系列的初衷。如果你急切地想進(jìn)入正篇绽昏,請(qǐng)?zhí)^(guò)本章协屡。

前言

為什么要學(xué)編譯原理

如果要我說(shuō)計(jì)算機(jī)專業(yè)最重要的三門課,我會(huì)說(shuō)是《數(shù)據(jù)結(jié)構(gòu)》全谤、《算法》和《編譯原理》肤晓。在我看來(lái),能不能理解“遞歸”像是程序員的第一道門檻认然,而會(huì)不會(huì)寫編譯器則是第二道补憾。

(當(dāng)然,并不是說(shuō)是沒(méi)寫過(guò)編譯器就不是好程序員卷员,只能說(shuō)它是一個(gè)相當(dāng)大的挑戰(zhàn)吧)

以前人們會(huì)說(shuō)盈匾,學(xué)習(xí)了編譯原理,你就能寫出更加高效的代碼毕骡,但隨著計(jì)算機(jī)性能的提升削饵,代碼是否高效顯得就不那么重要了岩瘦。那么為什么要學(xué)習(xí)編譯原理呢?

原因只有一個(gè):裝B窿撬。

好吧启昧,也許現(xiàn)在還想學(xué)習(xí)編譯原理的人只可能是因?yàn)榕d趣了。一方面想了解它的工作原理劈伴;另一方面希望挑戰(zhàn)一下自己密末,看看自己能走多遠(yuǎn)。

理論很復(fù)雜跛璧,實(shí)現(xiàn)也很復(fù)雜严里?

我對(duì)編譯器一直心存敬佩。所以當(dāng)學(xué)校開《編譯原理》的課程后赡模,我是抱著滿腔熱情去上課的田炭,但是兩節(jié)課后我就放棄了师抄。原因是太復(fù)雜了漓柑,聽不懂。

一般編譯原理的課程會(huì)說(shuō)一些:

1叨吮、如何表示語(yǔ)法(BNF什么的)

2辆布、詞法分析,用什么有窮自動(dòng)機(jī)和無(wú)窮自動(dòng)機(jī)

3茶鉴、語(yǔ)法分析锋玲,遞歸下降法,什么 LL(k)涵叮,LALR 分析惭蹂。

4、中間代碼的表示

5割粮、代碼的生成

6盾碗、代碼優(yōu)化

我相信絕大多數(shù)(98%)的學(xué)生頂多學(xué)到語(yǔ)法分析就結(jié)束了。并且最重要的是舀瓢,學(xué)了這么多也沒(méi)用廷雅!依舊幫助不了我們學(xué)習(xí)編譯器!這其中最主要的原因是《編譯原理》試圖教會(huì)我們的是如何構(gòu)造“編譯器生成器”京髓,即構(gòu)造一個(gè)工具航缀,根據(jù)文法來(lái)生成編譯器(如 lex/yacc)等等。

這些理論試圖教會(huì)我們?nèi)绾斡猛ㄓ玫姆椒▉?lái)自動(dòng)解決問(wèn)題堰怨,它們有很強(qiáng)的實(shí)際意義芥玉,只是對(duì)于一般的學(xué)生或程序員來(lái)說(shuō),它們過(guò)于強(qiáng)大备图,內(nèi)容過(guò)于復(fù)雜灿巧。如果你嘗試閱讀 lex/yacc (或 flex/bison)的代碼皇型,就會(huì)發(fā)現(xiàn)太可怕了。

然而如果你能跟我一樣砸烦,真正來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的編譯器弃鸦,那么你會(huì)發(fā)現(xiàn),比起可怕的《編譯原理》幢痘,這點(diǎn)復(fù)雜度還是不算什么的(因?yàn)楹枚嗬碚摳居貌簧希?/p>

項(xiàng)目的初衷

有一次在 Github 上看到了一個(gè)項(xiàng)目(當(dāng)時(shí)很火的)唬格,名叫 c4,號(hào)稱用 4 個(gè)函數(shù)來(lái)實(shí)現(xiàn)了一個(gè)小的 C 語(yǔ)言編譯器颜说。它最讓我震驚的是能夠自舉购岗,即能自己編譯自己。并且它用很少的代碼就完成了一個(gè)功能相當(dāng)完善的 C 語(yǔ)言編譯器门粪。

一般的編譯器相關(guān)的教程要么就十分簡(jiǎn)單(如實(shí)現(xiàn)四則運(yùn)算)喊积,要么就是借助了自動(dòng)生成的工具(如 flex/bison)。而 c4 的代碼完全是手工實(shí)現(xiàn)的玄妈,不用外部工具乾吻。可惜的是它的代碼初衷是代碼最小化拟蜻,所以寫得很亂绎签,很難懂。所以本項(xiàng)目的主要目的:

1酝锅、實(shí)現(xiàn)一個(gè)功能完善的 C 語(yǔ)言編譯器

2诡必、通過(guò)教程來(lái)說(shuō)明這個(gè)過(guò)程。

c4 大致500+行搔扁。重寫的代碼歷時(shí)一周爸舒,總共代碼加注釋1400行。項(xiàng)目地址: Write a C Interpreter稿蹲。

聲明:本項(xiàng)目中的代碼邏輯絕大多數(shù)取自 c4 扭勉,但確為自己重寫。

預(yù)警

在寫編譯器的時(shí)候會(huì)遇到兩個(gè)主要問(wèn)題:

1场绿、麻煩剖效,會(huì)有許多類似的代碼,寫起來(lái)很無(wú)聊焰盗。

2璧尸、難以調(diào)試,一方面沒(méi)有很好的測(cè)試用例熬拒,另一方面需要對(duì)照生成的代碼來(lái)調(diào)試(遇到的時(shí)候就知道了)爷光。

所以我希望你有足夠的耐心和時(shí)間來(lái)學(xué)習(xí),相信當(dāng)你真正完成的時(shí)候會(huì)像我一樣澎粟,十分有成就感蛀序。

雖然標(biāo)題是編譯器欢瞪,但實(shí)際上我們構(gòu)建的是 C 語(yǔ)言的解釋器,這意味著我們可以像運(yùn)行腳本一樣去運(yùn)行 C 語(yǔ)言的源代碼文件徐裸。這么做的理由有兩點(diǎn):

1遣鼓、解釋器與編譯器僅在代碼生成階段有區(qū)別,而其它方面如詞法分析重贺、語(yǔ)法分析是一樣的骑祟。

2、解釋器需要我們實(shí)現(xiàn)自己的虛擬機(jī)與指令集气笙,而這部分能幫助我們了解計(jì)算機(jī)的工作原理次企。

編譯器的構(gòu)建流程

一般而言,編譯器的編寫分為 3 個(gè)步驟:

1潜圃、詞法分析器缸棵,用于將字符串轉(zhuǎn)化成內(nèi)部的表示結(jié)構(gòu)。

2谭期、語(yǔ)法分析器堵第,將詞法分析得到的標(biāo)記流(token)生成一棵語(yǔ)法樹。

3崇堵、目標(biāo)代碼的生成型诚,將語(yǔ)法樹轉(zhuǎn)化成目標(biāo)代碼。

已經(jīng)有許多工具能幫助我們處理階段1和2鸳劳,如 flex 用于詞法分析,bison 用于語(yǔ)法分析纵搁。只是它們的功能都過(guò)于強(qiáng)大崔赌,屏蔽了許多實(shí)現(xiàn)上的細(xì)節(jié)狠鸳,對(duì)于學(xué)習(xí)構(gòu)建編譯器幫助不大。所以我們要完全手寫這些功能幔摸。

所以我們會(huì)根據(jù)下面的流程:

1、構(gòu)建我們自己的虛擬機(jī)以及指令集颤练。這后生成的目標(biāo)代碼便是我們的指令集既忆。

2、構(gòu)建我們的詞法分析器

3嗦玖、構(gòu)建語(yǔ)法分析器

編譯器的框架

我們的編譯器主要包括 4 個(gè)函數(shù):

1患雇、next() 用于詞法分析,獲取下一個(gè)標(biāo)記宇挫,它將自動(dòng)忽略空白字符苛吱。

2、program() 語(yǔ)法分析的入口器瘪,分析整個(gè) C 語(yǔ)言程序翠储。

3绘雁、expression(level) 用于解析一個(gè)表達(dá)式。

4援所、eval() 虛擬機(jī)的入口庐舟,用于解釋目標(biāo)代碼。

這里有一個(gè)單獨(dú)用于解析“表達(dá)式”的函數(shù) expression 是因?yàn)楸磉_(dá)式在語(yǔ)法分析中相對(duì)獨(dú)立并且比較復(fù)雜住拭,所以我們將它單獨(dú)作為一個(gè)模塊(函數(shù))继阻。

因?yàn)槲覀兊脑创a看起來(lái)就像是:

#include

#include

#include

#include

int token; ? ? ? ? ? ?// current token

char *src, *old_src; ?// pointer to source code string;

int poolsize; ? ? ? ? // default size of text/data/stack

int line; ? ? ? ? ? ? // line number

void next() {

? ? token = *src++;

? ? return;

}

void expression(int level) {

? ? // do nothing

}

void program() {

? ? next(); ? ? ? ? ? ? ? ? ?// get next token

? ? while (token > 0) {

? ? ? ? printf("token is: %c\n", token);

? ? ? ? next();

? ? }

}

int eval() { // do nothing yet

? ? return 0;

}

int main(int argc, char **argv)

{

? ? int i, fd;

? ? argc--;

? ? argv++;

? ? poolsize = 256 * 1024; // arbitrary size

? ? line = 1;

? ? if ((fd = open(*argv, 0)) < 0) {

? ? ? ? printf("could not open(%s)\n", *argv);

? ? ? ? return -1;

? ? }

? ? if (!(src = old_src = malloc(poolsize))) {

? ? ? ? printf("could not malloc(%d) for source area\n", poolsize);

? ? ? ? return -1;

? ? }

? ? // read the source file

? ? if ((i = read(fd, src, poolsize-1)) <= 0) {

? ? ? ? printf("read() returned %d\n", i);

? ? ? ? return -1;

? ? }

? ? src[i] = 0; // add EOF character

? ? close(fd);

? ? program();

? ? return eval();

}

上面的代碼看上去挺復(fù)雜,但其實(shí)內(nèi)容不多废酷,就是讀取一個(gè)源代碼文件瘟檩,逐個(gè)讀取每個(gè)字符,并輸出每個(gè)字符澈蟆。這里重要的是注意每個(gè)函數(shù)的作用墨辛,后面的文章中,我們將逐個(gè)填充每個(gè)函數(shù)的功能趴俘,最終構(gòu)建起我們的編譯器睹簇。

本節(jié)的代碼可以在 Github 上下載,也可以直接 clone

git clone -b step-0 https://github.com/lotabout/write-a-C-interpreter

這樣我們就有了一個(gè)最簡(jiǎn)單的編譯器:什么都不干的編譯器寥闪,下一章中太惠,我們將實(shí)現(xiàn)其中的eval函數(shù),即我們自己的虛擬機(jī)疲憋。

參考資料

最后想介紹幾個(gè)資料:

1凿渊、Let’s Build a Compiler 很好的初學(xué)者教程,英文的缚柳。

2埃脏、Lemon Parser Generator,一個(gè)語(yǔ)法分析器生成器秋忙,對(duì)照《編譯原理》觀看效果更佳彩掐。

祝你學(xué)得愉快。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灰追,一起剝皮案震驚了整個(gè)濱河市堵幽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌弹澎,老刑警劉巖朴下,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異裁奇,居然都是意外死亡桐猬,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門刽肠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)溃肪,“玉大人免胃,你說(shuō)我怎么就攤上這事”棺” “怎么了羔沙?”我有些...
    開封第一講書人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)厨钻。 經(jīng)常有香客問(wèn)我扼雏,道長(zhǎng),這世上最難降的妖魔是什么夯膀? 我笑而不...
    開封第一講書人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任诗充,我火速辦了婚禮,結(jié)果婚禮上诱建,老公的妹妹穿的比我還像新娘蝴蜓。我一直安慰自己,他們只是感情好俺猿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開白布茎匠。 她就那樣靜靜地躺著,像睡著了一般押袍。 火紅的嫁衣襯著肌膚如雪诵冒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,842評(píng)論 1 290
  • 那天谊惭,我揣著相機(jī)與錄音汽馋,去河邊找鬼。 笑死午笛,一個(gè)胖子當(dāng)著我的面吹牛惭蟋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播药磺,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼煤伟!你這毒婦竟也來(lái)了癌佩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤便锨,失蹤者是張志新(化名)和其女友劉穎围辙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體放案,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡姚建,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吱殉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掸冤。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厘托,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出稿湿,到底是詐尸還是另有隱情铅匹,我是刑警寧澤,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布饺藤,位于F島的核電站包斑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏涕俗。R本人自食惡果不足惜罗丰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望再姑。 院中可真熱鬧萌抵,春花似錦、人聲如沸询刹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)凹联。三九已至沐兰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蔽挠,已是汗流浹背住闯。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留澳淑,地道東北人比原。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像杠巡,于是被迫代替她去往敵國(guó)和親量窘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

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

  • Kotlin 源代碼編譯過(guò)程分析 我們知道氢拥,Kotlin基于Java虛擬機(jī)(JVM)蚌铜,通過(guò)Kotlin編譯器生成的...
    光劍書架上的書閱讀 2,863評(píng)論 0 13
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)嫩海,斷路器冬殃,智...
    卡卡羅2017閱讀 134,633評(píng)論 18 139
  • 前面章節(jié)中,我們完成了詞法解析器的開發(fā)叁怪。詞法解析的目的是把程序代碼中的各個(gè)字符串進(jìn)行識(shí)別分類审葬,把不同字符串歸納到相...
    望月從良閱讀 1,540評(píng)論 4 4
  • 與你相遇好幸運(yùn)。
    煙澀寒閱讀 97評(píng)論 0 0
  • 故事的開頭很重要,我本想使出所有的力氣涣觉,來(lái)編出一個(gè)能夠讓這件事情聽起來(lái)更有趣味性的開頭痴荐,必竟當(dāng)肉農(nóng)這件事情聽起來(lái)或...
    道道櫻木花道閱讀 331評(píng)論 0 1