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

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

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

前言

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

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

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

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

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

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

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

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

一般編譯原理的課程會說一些:

1、如何表示語法(BNF什么的)

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

3、語法分析,遞歸下降法,什么 LL(k),LALR 分析。

4、中間代碼的表示

5、代碼的生成

6、代碼優(yōu)化

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

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

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

項(xiàng)目的初衷

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

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

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

2、通過教程來說明這個(gè)過程。

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

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

預(yù)警

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

1、麻煩,會有許多類似的代碼,寫起來很無聊。

2、難以調(diào)試,一方面沒有很好的測試用例,另一方面需要對照生成的代碼來調(diào)試(遇到的時(shí)候就知道了)。

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

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

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

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

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

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

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

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

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

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

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

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

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

3、構(gòu)建語法分析器

編譯器的框架

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

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

2、program() 語法分析的入口,分析整個(gè) C 語言程序。

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

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

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

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

#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è)最簡單的編譯器:什么都不干的編譯器,下一章中,我們將實(shí)現(xiàn)其中的eval函數(shù),即我們自己的虛擬機(jī)。

參考資料

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

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

2、Lemon Parser Generator,一個(gè)語法分析器生成器,對照《編譯原理》觀看效果更佳。

祝你學(xué)得愉快。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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