“手把手教你構(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é)得愉快。