開放電腦計畫 -- 系統程式
(使用 JavaScript 實作)

目錄 | 下載 陳鍾誠金門大學

開放電腦計畫

如果您是資工系畢業的學生,必然會上過「計算機結構、編譯器、作業系統、系統程式」等等課程, 這些課程都是設計出一台電腦所必需的基本課程。但是如果有人問您「您是否會設計電腦呢?」,相信大部分人 的回答應該是:「我不會,也沒有設計過」。

光是設計一個作業系統,就得花上十年的工夫,遑論還要自己設計「CPU、匯流排、組譯器、編譯器、作業系統」 等等。因此,我們都曾經有過這樣的夢想,然後在年紀越大,越來越瞭解整個工業結構之後,我們就放棄了這樣 一個夢想,因為我們必須與現實妥協。

但是,身為一個大學教師,我有責任教導學生,告訴他們「電腦是怎麼做出來的」,因此我不自量力的提出了 這樣一個計畫,那就是「開放電腦計畫」,我們將以「千里之行、始於足下」的精神,設計出一台全世界最簡單 且清楚的「電腦」,包含「軟體與硬體」。

從 2007 年我開始寫「系統程式」這本書以來,就有一個想法逐漸在內心發酵, 這個想法就是:「我想從 CPU 設計、組譯器、虛擬機、編譯器到作業系統」,自己打造一台電腦,於是、「開放電腦計畫」就誕生了!

那麼、開放電腦計畫的「產品」會是什麼呢?

應該有些人會認為是一套自行編寫的軟硬體程式,當然、這部份是包含在「開放電腦計畫」當中的。

但是、更重要的事情是,我們希望透過「開放電腦計畫」讓學生能夠學會整個「電腦的軟硬體設計方式」,並且透過這個踏腳石瞭解整個「電腦軟硬體工業」,進而能夠達到「以理論指導實務、以實務驗證理論」的目標。

為了達成這個目標,我們將「開放電腦計畫」分成三個階段,也就是「簡單設計 (程式) => 理論闡述 (書籍) => 開源實作 (工業軟硬體與流程)」,整體的構想說明如下:

  1. 簡單設計(程式): 採用 Verilog + C 設計「CPU、組譯器、編譯器、作業系統」等軟硬體,遵循 KISS (Keep It Simple and Stupid) 原則,不考慮「效能」 與「商業競爭力」等問題,甚至在實用性上進行了不少妥協,一律採用「容易理解」為最高指導原則,目的是清楚的展現整個「軟硬體系統」的架構。
  2. 理論闡述(書籍): 但是、要瞭解像「處理器、系統軟體、編譯器、作業系統」這些領域,只有程式是不夠的。因為程式通常不容易懂,而且對於沒有背景知識的人而言,往往難如天書。所以我們將撰寫一系列書籍,用來說明上述簡單程式的設計原理,然後開始進入「計算機結構、編譯器、作業系統、系統程式」的理論體系中,導引出進一步的設計可能性與工業考量等議題。
  3. 開源實作(工業):一但有了前述的理論與實作基礎之後,我們就會採用「開放原始碼」來進行案例研究。舉例而言、在「計算機結構」上我們會以 ARM 為實務核心、「編譯器」領域則以 gcc, LLVM 為研究標的,「作業系統」上則會對 FreeRTOS、Linux 等進行案例研究,「虛擬機」上則會以 QEMU、V8 等開源案例為研究對象。
圖、開放電腦計畫地圖

圖、開放電腦計畫地圖

根據以上規劃,本書乃為一系列書籍中的一本,完整的書籍架構如下:

開放電腦計畫書籍 簡易程式 工業實作
系統程式 as0, vm0, cc0, os0 gcc/llvm
計算機結構 mcu0, cpu0 ARM/OpenRISC
編譯器 c 0c, j0c g cc/llvm
作業系統 os0, XINU, MINIX FreeRTOS, Linux

這些書籍分別描述不同的面向,其涵蓋範圍如下圖所示:

圖、開放電腦計畫書籍圖

圖、開放電腦計畫書籍圖

硬體:計算機結構

在硬體方面,我們將自行設計兩款處理器,一款是用來展示簡單「微處理器」設計原理的16 位元微控制器 MCU0,而另一款則是用來展示「高階處理器」設計原理的 32 位元處理器 CPU0。

透過 MCU0,我們希望展示一顆「最簡易微處理器」的設計方法,我們將採用「流程式」與「區塊式」的方法分別實作一遍,讓讀者可以分別從「硬體人」與「軟體人」的角度去體會處理器的設計方式。由於「流程式」的方法比較簡單,因此我們會先用此法進行設計,當讀者理解何謂「微處理器」之後,在將同樣的功能改用「區塊式的方法」實作一遍,這樣應該就能逐漸「由易至難、由淺入深」了。

在 MCU0 當中,我們採用「CPU 與記憶體」合一的設計方式,這種方式比較像「系統單晶片」(SOC) 的設計方法,其記憶體容量較小,因此可以直接用 Verilog 陣列宣告放入 FPGA 當中使用,不需考慮外部 DRAM 存取速度較慢的問題,也不用考慮「記憶階層」的速度問題,因此設計起來會相對容易許多。

接著,我們將再度設計一個 32 位元的處理器 -- CPU0。並透過 CPU0 來討論「當 CPU 速度比 DRAM 記憶體快上許多」的時候,如何能透過快取 (cache) 與記憶體管理單元 (MMU) 達到「又快又大」的目的,並且討論如何透過「流水線」架構 (Pipeline) 達到加速的目的,這些都屬於「高階處理器」所需要討論的問題。

軟體:系統程式

有了 MCU0 與 CPU0 等硬體之後,我們就可以建構運作於這些硬體之上的軟體了,這些軟體包含「組譯器、虛擬機、編譯器、作業系統」等等。

我們已經分別用 C 與 JavaSript 建構出簡易的「組譯器、虛擬機、編譯器」工具了,讓我們先說明一下在 CPU0 上這些程式的使用方法,以下示範是採用 node.js+Javascript 實作的工具版本,因此必須安裝 node.js 才能執行。

組合語言 (Assembly Language)

接著、讓我們從組合語言的角度,來看看 CPU0 處理器的設計,以下是一個可以計算 1+2+...+10 的程式, 計算完成之後會透過呼叫軟體中斷 SWI 程序 (類似 DOS 時代的 INT 中斷),在螢幕上印出下列訊息。

1+...+10=55

以下的檔案 sum.as0 正是完成這樣功能的一個 CPU0 組合語言程式。

檔案:sum.as0

        LD     R1, sum      ; R1 = sum = 0
        LD     R2, i        ; R2 = i = 1
        LDI    R3, 10       ; R3 = 10
FOR:    CMP    R2, R3       ; if (R2 > R3)
        JGT    EXIT         ;   goto EXIT
        ADD    R1, R1, R2   ; R1 = R1 + R2 (sum = sum + i)
        ADDI   R2, R2, 1    ; R2 = R2 + 1  ( i  = i + 1)
        JMP    FOR          ; goto FOR
EXIT:   ST     R1, sum      ; sum = R1
        ST     R2, i        ; i = R2
        LD     R9, msgptr   ; R9= pointer(msg) = &msg
        SWI    3            ; SWI 3 : 印出 R9 (=&msg) 中的字串
        MOV    R9, R1       ; R9 = R1 = sum
        SWI    4            ; SWI 4 : 印出 R9 (=R1=sum) 中的整數
        RET                 ; return 返回上一層呼叫函數
i:      RESW   1            ; int i
sum:    WORD   0            ; int sum=0
msg:    BYTE   "1+...+10=", 0   ; char *msg = "sum="
msgptr: WORD   msg          ; char &msgptr = &msg

組譯器 (Assembler)

我們可以用以下指令呼叫「組譯器 AS0」對上述檔案進行組譯:

node as0 sum.as0 sum.ob0

上述的程式經過組譯之後,會輸出組譯報表,如下所示。

sum.as0 的組譯報表

0000          LD       R1,sum           L 00 001F003C
0004          LD       R2,i             L 00 002F0034
0008          LDI      R3,10            L 08 0830000A
000C FOR      CMP      R2,R3            A 10 10230000
0010          JGT      EXIT             J 23 2300000C
0014          ADD      R1,R1,R2         A 13 13112000
0018          ADDI     R2,R2,1          A 1B 1B220001
001C          JMP      FOR              J 26 26FFFFEC
0020 EXIT     ST       R1,sum           L 01 011F001C
0024          ST       R2,i             L 01 012F0014
0028          LD       R9,msgptr        L 00 009F0022
002C          SWI      3                J 2A 2A000003
0030          MOV      R9,R1            A 12 12910000
0034          SWI      2                J 2A 2A000002
0038          RET                       J 2C 2C000000
003C i        RESW     1                D F0 00000000
0040 sum      WORD     0                D F2 00000000
0044 msg      BYTE     "1+...+10=",0    D F3 312B2E2E2E2B31303D00
004E msgptr   WORD     msg              D F2 00000044

最後「組譯器 AS0」會輸出機器碼到目的檔 sum.ob0 當中,其內容如下所示。

sum.as0 的機器碼 (以 16 進位顯示)

001F003C 002F0034 0830000A 10230000
2300000C 13112000 1B220001 26FFFFEC
011F001C 012F0014 009F0022 2A000003
12910000 2A000002 2C000000 00000000
00000000 312B2E2E 2E2B3130 3D000000
0044

虛擬機 (Virtual Machine)

如果我們用「虛擬機 VM0」去執行上述的目的檔 sum.ob0,會看到程式的執行結果,是在 螢幕上列印出 1+...+10=55,以下是我們的操作過程。

1+...+10=55

編譯器 (Compiler)

當然、一個完整的現代電腦應該包含比組譯器更高階的工具,不只支援組合語言,還要支援高階語言。

因此、我們設計了一個稱為 J0 的高階語言,語法有點像 JavaScript,但卻是經過簡化的版本。

然後、我們又設計了一個可以用來編譯 J0 語言的編譯器,稱為 J0C (J0 Compiler),可以用來將 J0 語言編譯成中間碼, 也可以直接將中間碼轉換為 CPU0 的組合語言。

以下是一個 J0 語言的範例,

檔案:sum.j0

s = sum(10);
return s;

function sum(n) {
  s = 0;
  i=1;
  while (i<=10) {
    s = s + i;
    i++;
  }
  return s;
}

當我們使用 j0c 編譯器將上述程式編譯之後,會輸出兩個檔案,一個是 sum.ir,是編譯器中間格式 (Intermediate Representation, 虛擬碼 pcode) 的輸出檔, 其內容如下:

D:\Dropbox\Public\web\oc\code>node j0c sum
         arg      10
         call     T1       sum
         =        s        T1
         return   s
sum      function
         param    n
         =        s        0
         =        i        1
L1
         <=       T2       i        10
         if0      T2       L2
         +        T3       s        i
         =        s        T3
         ++       i
         goto     L1
L2
         return   s
         endf

另一個是將上述中間格式轉換成轉換成 CPU0 組合語言之後的結果,如下所示:

sum
         POP      n
         LDI      R1       0
         ST       R1       s
         LDI      R1       1
         ST       R1       i
L1
         LD       R1       i
         LDI      R2       10
         LDI      R3       0
         CMP      R1       R2
         JLE      else1
         LDI      R3       1
else1
         ST       R3       T1
         LDI      R1       T1
         CMP      R1       0
         JEQ      L2
         LD       R1       s
         LD       R2       i
         ADD      R3       R1       R2
         ST       R3       T2
         LDI      R1       T2
         ST       R1       s
         LD       R1       i
         ADDI     R1       R1       1
         ST       R1       i
         JMP      L1
L2
         LD       R1       s
         RET
         LDI      R1       10
         PUSH     R1
         CALL     sum
         ST       R1       T3
         LDI      R1       T3
         ST       R1       s

s        WORD     0
i        WORD     0
T1       WORD     0
T2       WORD     0
T3       WORD     0

上述由 j0c 所編譯產生的組合語言,感覺相對冗長,是因為這個編譯器是最簡版本,完全沒有做任何優化動作,甚至連暫存器都是 每次重新載入的,所以效率並不會很好。

作業系統 (Operating System)

當然囉!一個完整的電腦還必須要有作業系統,不過如果是嵌入式系統的話,沒有作業系統也沒關係,只要將全部的程式連結在一起, 就可以形成一台電腦了,目前開放電腦計畫的「作業系統」還在研究開發當中,希望很快就能提供大家一個最簡單的作業系統版本。

目前我們已經寫了一個可以進行兩個行程切換 「Task Switching」 範例,接著我們將參考 UNIXv6, L4 等作業系統,以建構更 完整的簡易作業系統。

當然、即使我們從 CPU 硬體一路設計到組譯器、虛擬機、編譯器、作業系統等,未來仍然有更多領域等待我們去探索,例如「網路模組、TCP/IP、 Ethernet、無線 RF 的硬體模組、繪圖卡、OpenGL、.....」等等,希望我們能夠用最簡單的話語,將這些電腦的原理說明清楚,並用簡單的方式 實作得更完整。

參考文獻