首頁 / 少年科技人雜誌 / 2015年6月號

Nand2Tetris 第 6 週 -- 組譯器

假如 HackComputer 只有硬體的話,那麼寫起程式來將會是非常痛苦的,就算你可以用另一台電腦寫程式,然後再上傳到 HackComputer 當中,那麼你所寫的程式很可能會長得像這樣。

0000000000010000 1110111111001000 0000000000010001 1110101010001000
0000000000010000 1111110000010000 0000000000001010 1110010011010000
0000000000010010 1110001100000001 0000000000010000 1111110000010000
0000000000010001 1111000010001000 0000000000010000 1111110111001000
0000000000000100 1110101010000111 0000000000010010 1110101010000111

事實上,上述『程式』真的可以在 HackComputer 上執行,這個程式可以計算 1+...+10 的結果,其組合語言如下所示。

    // Adds 1+...+10.
    @i // i refers to some RAM location
    M=1 // i=1
    @sum // sum refers to some RAM location
    M=0 // sum=0
(LOOP)
    @i
    D=M // D = i
    @10
    D=D-A // D = i - 10
    @END
    D;JGT // If (i-100) > 0 goto END
    @i
    D=M // D = i
    @sum
    M=D+M // sum += i
    @i
    M=M+1 // i++
    @LOOP
    0;JMP // Got LOOP
(END)
    @END
    0;JMP // Infinite loop

雖然這種組合語言還是不容易閱讀和撰寫,但是總比寫那些 0 與 1 要好得多了。

本週的 nand2tetris 課程作業,就是要你寫出一個組譯器,可以將上述的組合語言轉換成由 0 與 1 組成的二進位機器碼後輸出。

這次的作業,並沒有要求要用哪一種程式語言撰寫,您可以自行選定一種自己熟悉的就行了。

當然,如果能夠用接下來的課程所要發展的 Jack 程式語言的話,那未來就有可能在 HackComputer 上撰寫這種語言了。

不過我沒有這樣做,我選擇了使用 JavaScript 來開發組譯器,寫了一個稱為 ash.js 的組譯器,並且用 io.js 當作執行工具。

以下是用 ash.js 來組譯 sum.asm 的執行過程,您可以清楚地看到組譯器的兩個階段分別做了什麼事情。

nqu-192-168-61-142:n2t mac020$ iojs ash sum
============== pass1 ================
002:0000     @i // i refers to some RAM location
003:0001     M=1 // i=1
004:0002     @sum // sum refers to some RAM location
005:0003     M=0 // sum=0
 symbol: LOOP 0004
007:0004     @i
008:0005     D=M // D = i
009:0006     @10
010:0007     D=D-A // D = i - 10
011:0008     @END
012:0009     D;JGT // If (i-100) > 0 goto END
013:0010     @i
014:0011     D=M // D = i
015:0012     @sum
016:0013     M=D+M // sum += i
017:0014     @i
018:0015     M=M+1 // i++
019:0016     @LOOP
020:0017     0;JMP // Got LOOP
 symbol: END 0018
022:0018     @END
023:0019     0;JMP // Infinite loop
============== pass2 ================
002:0000000000010000     @i // i refers to some RAM location
003:1110111111001000     M=1 // i=1
004:0000000000010001     @sum // sum refers to some RAM location
005:1110101010001000     M=0 // sum=0
007:0000000000010000     @i
008:1111110000010000     D=M // D = i
009:0000000000001010     @10
010:1110010011010000     D=D-A // D = i - 10
011:0000000000010010     @END
012:1110001100000001     D;JGT // If (i-100) > 0 goto END
013:0000000000010000     @i
014:1111110000010000     D=M // D = i
015:0000000000010001     @sum
016:1111000010001000     M=D+M // sum += i
017:0000000000010000     @i
018:1111110111001000     M=M+1 // i++
019:0000000000000100     @LOOP
020:1110101010000111     0;JMP // Got LOOP
022:0000000000010010     @END
023:1110101010000111     0;JMP // Infinite loop

組譯器的第一階段 (pass1) 是要記錄所有符號的記憶體位址,像是上述程式中的 LOOP 與 END 位址分別為 0004 和 0018,這些內容會儲存在一個稱為符號表的結構當中。

為了配合 nand2tetris 不公佈答案的政策,筆者將自己的程式碼內容抽掉,僅留下框架

function parse(line, i) {
// put your code here
}

function addSymbol(symbol) {
// put your code here
}

function pass1(lines) {
// put your code here
}

接著就可以在第二階段 (pass2) 當中把每個指令都翻譯成二進位的機器碼,然後輸出到目的黨 *.hack 當中,其方法主要是利用查表的方式查出指令各欄位的對應二進位值,然後填入到最後的二進位碼中就行了。

function pass2(lines, objFile) {
// put your code here
}

function toCode(p) {
// put your code here
}

最後,我們在主程式中呼叫上述的兩階段組譯函數,就完成了整個程式的建構。

function assemble(asmFile, objFile) {
  var asmText = fs.readFileSync(asmFile, "utf8"); // 讀取檔案到 text 字串中
  var lines   = asmText.split(/\r?\n/); // 將組合語言分割成一行一行
  pass1(lines);
  pass2(lines, objFile);
} 

接著您可以用上述程式將作業裏給的 Add.asm, Max.asm, Rect.asm, Pong.asm 等程式,組譯成機器碼之後,上傳到課程網站的作業繳交點就可以了。

以下是第六週習題的題目網址,請務必盡可能靠自己的能力完成,這樣才能深度理解這些設計的實作方法。

雖然兩位老師請同學不要將解答上傳到公開網路上,但還是有人上傳了,所以如果您真的做不出來或卡住了,還是可以參考一下答案,以下是 havivha 所提供的參考答案! (他的組譯器程式是用 Python 寫的)

一旦完成了組譯器之後,我們就可以進入第二階段的課程了。

在第二階段的課程當中,我們將學會『虛擬機,編譯器,作業系統』的設計方式,且讓我們拭目以待!


本文部份內容與大部份圖片修改自 維基百科 , 使用時請遵守 姓名標示、相同方式分享 授權。
編輯: 陳鍾誠 email: ccckmit@gmail.com