動手編寫一個編譯器,學習一下較為底層的程式設計方式,是一種學習計算機到底是如何工作的非常有效方法。
編譯器通常被看作是十分複雜的工程。事實上,編寫一個產品級的編譯器也確實是一個龐大的任務。但是寫一個小巧可用的編譯器卻不是這麼困難。
祕訣就是首先去找到一個最小的可用工程,然後把你想要的特性新增進去。這個方法也是Abdulaziz Ghuloum在他那篇著名的論文“一種構造編譯器的捷徑”裡所提到的辦法。不過這個辦法確實可行。你只需要按照這篇論文中的第一步來操作,就可以得到一個真正可用的編譯器!當然,它只能編譯程式語言中的非常小的子集,但是它確實是一個真實可用的編譯器。你可以隨意的擴充套件這個編譯器,然後從中學到更多更深的知識。
受到這篇文章的鼓舞,我就寫了一個C編譯器。從某種意義上來說這比寫一個scheme的編譯器要困難一些(因為你必須去解析C那複雜的語法),但是在某些方面又很便利(你不需要去處理執行時型別)。要寫這樣一個編譯器,你只需要從你那個可用的最小的編譯器開始。
對於我寫的編譯器來說,我把它叫 babyc,我選了這段程式碼來作為我需要執行的第一個程式:
1 2 3 |
int main() { return 2; } |
沒有變數,沒有函式呼叫,沒有額外的依賴,甚至連if語句,迴圈語句都沒有,一切看起來是那麼簡單。
我們首先需要解析這段程式碼。我們將使用 Flex 和 Bison 來做到這點。這裡有怎麼用的例子可以參考,幸好我們的語法是如此簡單,下面就是詞法分析器:
1 2 3 4 5 6 7 8 9 |
"{" { return '{'; } "}" { return '}'; } "(" { return '('; } ")" { return ')'; } ";" { return ';'; } [0-9]+ { return NUMBER; } "return" { return RETURN; } "int" { return TYPE; } "main" { return IDENTIFIER; } |
這裡是語法分析器:
1 2 3 4 5 6 7 |
function: TYPE IDENTIFIER '(' ')' '{' expression '}' ; expression: RETURN NUMBER ';' ; |
最終,我們需要生成一些彙編程式碼。我們將使用32位的X86彙編,因為它非常的通用而且可以很容易的執行在你的機器上。這裡有X86彙編的相關網站。
下面就是我們需要生成的彙編程式碼:
1 2 3 4 5 6 7 |
.text .global _start # Tell the loader we want to start at _start. _start: movl $2,%ebx # The argument to our system call. movl $1,%eax # The system call number of sys_exit is 1. int $0x80 # Send an interrupt |
然後加上上面的詞法語法分析程式碼,把這段彙編程式碼寫進一個檔案裡。恭喜你!你已經是一個編譯器的編寫者了!
Babyc 就是這樣誕生的,你可以在這裡看到它最開始的樣子。
當然,如果彙編程式碼沒辦法執行也是枉然。讓我們來用編譯器生成我們所希望的真正的彙編程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# Here's the file we want to compile. $ cat return_two.c #include <stdio.h> int main() { return 2; } # Run the compiler with this file. $ ./babyc return_two.c Written out.s. # Check the output looks sensible. $ cat out.s .text .global _start _start: movl $2, %ebx movl $1, %eax int $0x80 |
非常棒!接著讓我們來真正的執行一下編譯之後程式碼來確保它能得到我們所想的結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Assemble the file. We explicitly assemble as 32-bit # to avoid confusion on x86_64 machines. $ as out.s -o out.o --32 # Link the file, again specifying 32-bit. $ ld -m elf_i386 -s -o out out.o # Run it! $ ./out # What was the return code? $ echo $? 2 # Woohoo! |
我們踏出了第一步,接下去怎麼做就全看你了。你可以按照那篇文章所指導的全部做一遍,然後製作一個更加複雜的編譯器。你需要去寫一個更加精巧的語法樹來生成彙編程式碼。接下去的幾步分別是:(1)允許返回任意的值(比如,return 3; 一些可執行程式碼);(2)新增對“非”的支援(比如,return ~1; 一些可執行程式碼)。每一個額外的特性都可以教你關於C語言的更多知識,編譯器到底是怎麼執行的,以及世界上其他編寫編譯器的人是如何想的。
這是構建 babyc 的方法。Babyc 現在已經擁有了if語句,迴圈,變數以及最基礎的資料結構。歡迎你來check out它的程式碼,但是我希望看完我的文章你能夠自己動手寫一個。
不要害怕底層的一些事情。這是一個非常奇妙的世界。