• 沒有找到結果。

第三章 研究架構

3.2 建立網頁

3.2.2 製作編譯器

整個過程可以大致上分成四個步驟,分別為預處理 (pre-processing)、編譯 (compilation)、彙編 (assembly) 與鏈接 (linking)。

圖3- 製作編譯器的流程圖

預處理 (pre-processing)

在這個階段編譯器主要的工作是展開引用的外部檔案、 macro 及 define 。 1. 刪除所有的 #define 並展開所有 macro

2. 處理所有的預編譯條件,例如 #ifdef , #include (展開引用文件) 3. 刪除所有註解

4. 增加 行號以及文件識別名,讓編譯器在編譯失敗時可以顯示錯誤的行數

編譯 (compilation)

此階段編譯器會將展開後的程式碼轉換成組合語言。此階段又可以細分成四個小 步驟,分別為 掃描 (scan)、語法分析 (parsing) 、語意分析 (semantic analysis) 與 源 代碼優化 (source code optimization) 與 代碼生成與優化(code generation and

optimization)。

圖3- 編譯流程圖

利用 $ g++ -S main.cpp -o main.s 可以得到編譯過後的組合語言檔。

掃描 (scan)

目的為將程式碼的序列分割成一連串的 token,並將這些 token 分類,例如:

保留關鍵字、標示符、常量 (數字或字串)、特殊符號 (加減號) 等等⋯

舉例來說, B = 4 + 8;這段程式碼是由六個 token 所組成。B 為標示符,= + ; 為特殊符號,4 8 為常量。

圖3- 由符號組成的文法樹

語法分析 (parsing)

有了這些 token 之後,我們就可以組出對應的文法樹 (syntax tree) ,可以幫助 我們分析複雜的運算式是否正確。

圖3- 對應的文法樹

語意分析 (semantic analysis)

接下來要判斷語意的正確性,比如說我們允許兩個常量相乘,但不允許對兩 個指標做相乘。而語意又可分成靜態語意以及動態語意,靜態語意是編譯器可以 在編譯期分析的,並且回報錯誤,例如:型態的轉換是否正確。而動態語意則是 在 執行期相關的,例如:以零當作除數是一個執行期的語意錯誤。

源代碼優化 (source code optimization)

現代編譯器通常會有著不同層級的優化方式,這邊提到的是源代碼層級的優 化。以此例來說,在編譯過程中我們其實就已經知道 4 + 8 的值了,我們就可以直 接用 12 來取代。此外,為了增進程式碼轉換為組合語言的效率,通常會使用 三 位址碼 (three address code) 來改寫程式碼。

彙編 (assembly)

此步驟只是簡單的將組合語言轉換成對應的機器語言,只要根據對照表翻譯 即可。

$ g++ -c main.cpp -o main.o

經過以上指令後,便可以產生 目標文件檔 (.o/.obj)。

目標文件檔遵循可執行文件格式 (ELF) 來儲存,格式內的細節就不在此多加 描述。但我們可以簡單地將其視為儲存數種不同區段的機器語言格式− 代碼區段 , 數據區段 , 符號表 , 重定位表 … 等等。

有一個值得注意的地方是,程式碼內的代碼與數據被分開到不同的區段中。這 樣有什麼好處呢?

(1) 執行時可以給予區段不同的讀寫權限 (代碼 read-only,數據 read-write) (2) 增加緩存 (cache) 的命中率

以這個例子來說,func.cpp 定義 (define) 函數 foo() ,而 main.cpp 引用 (reference) 了函數 foo()。 (relocation table) 中。

而在此階段,鏈結器 (linker) 便會分配符號所需的地址,並且依照符號表與重定位 表來重新修改機器語言的內容。

預編譯 (pre-processing) 展開文件內的定義與預編譯,並刪除不必要的部份。

編譯 (compilation) 將代碼編譯並優化成組合語言。彙編 (assembly)將組合語言轉成 機器語言,轉成目標文件。鏈結 (linking) 透過目標文件內的資訊來分配空間以及 確定符號的確切位址。

相關文件