動手編寫一個編譯器,學習一下較為底層的編程方式,是一種學習計算機到底是如何工作的非常有效方法。
編譯器通常被看作是十分復雜的工程。事實上,編寫一個產品級的編譯器也確實是一個龐大的任務。但是寫一個小巧可用的編譯器卻不是這么困難。
秘訣就是首先去找到一個最小的可用工程,然后把你想要的特性添加進去。這個方法也是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它的代碼,但是我希望看完我的文章你能夠自己動手寫一個。
不要害怕底層的一些事情。這是一個非常奇妙的世界。