• 沒有找到結果。

第三章 系統設計與實作

3.2 架構

11

第三章 第三章 第三章

第三章 系統設計與實作 系統設計與實作 系統設計與實作 系統設計與實作

3.1 簡介 簡介 簡介 簡介

如同前一章所言,當目標語言不是機器碼,反而是一般的程式語言或腳本語 言時,LLVM 的後端所需實作的內容會與一般的後端有所不同。有些步驟,如定 義暫存器會省略掉;然而相對的,要撰寫這種後端,也會有一般的後端所不會遇 到的問題,例如 SSA Form 的轉換,以及指標變數的處理等問題。而這些問題,

並不像一般的後端有許多現成的資訊可以參考,必須從 LLVM 所提供繁多的 API 中來想辦法解決。

同時,也不同於一般的後端只需要單純的轉換就可以執行,當目標語言是腳 本語言時,單純轉換出來的程式碼,往往只類似於低階的機器碼,並不能發揮出 高階語言應該要有的高可讀性與效率。這樣一來,便失去了轉譯成腳本語言的意 義。因此我的後端還必須要加上許多的優化功能,讓轉譯出來的程式碼更接近一 般腳本語言應該有的風格。

3.2 架構 架構 架構 架構

12 圖 3-1 系統架構

圖 3-1 是我的 LLVM 後端的基本架構。基本上,系統將中間表示式轉譯成 Lua 時,會分為數個步驟來處理:

3.2.1 Load Module

一開始,我的 LLVM 後端必須從 LLVM 本體載入 Module,Module 中包含 著所輸入的原始碼的所有相關資訊。所以要先對 Module 進行分析,以方便之後 步驟的處理。

3.2.2 Initialization

取得 Module 的相關資訊後,我首先要對 Global variable 以及 System function 進行處理。由於 Lua Script 是直譯式執行的,在設定上,不論是 Local variable 還 是 Global variable 都必須先進行宣告後方能使用。所以一開始我就必須將 Global

13

variable 先宣告好,以方便之後的 statement 使用。

至於 System function 則是來自於 C 的內建函式庫。由於 LLVM 允許中間表 示式直接使用 C 內建函式庫的函式,我所接受的源始碼中往往會有 C 內建函式 庫的函式的宣告,卻沒有相關的定義。然而這些函式在 Lua Script 中大多是沒有,

或者是格式不同。因此我必須要製作這些函式的定義,才能讓轉譯出來的程式正 常運作。

3.2.3 Function Generalize

初步的設定完成之後,接下來就是進行函式(Function)層的處理。在這裡, 餘的。所以 Function Generalize 這步驟就是負責檢查所有的函式,找出對於腳本 語言而言是功能雷同的部分,並建立對照表以方便之後進行轉譯時使用。至於詳 細的內容,會在實作細節中做說明。

3.2.4 Function Initialization

Function Initialization 步驟,基本上又可分為兩部份,其一是對每個函式的

14

引數進行初始設定,包括可變引數(variable number of arguments)的處理,以及為 了之後指標變數的處理,預先檢查各個引數的型別,並分別對指標型別的引數以 及一般引數進行相關處理。

第二部分則是預先對函式底下的指令進行初步的檢查,對函式中所有指令會 用到的變數建立對應的基礎資訊表,以方便之後的 Instruction Process 步驟進行 處理。

3.2.5 Loop Recovery

處理完函式層面的問題之後,我接著要對 Control Flow 的基礎結構,LLVM 中間表示式的 Basic Block 層進行分析以及優化。

由於 LLVM 中間表示式是屬於類似組合語言的低階語言,所以當前端把原 本的程式碼轉譯成中間表示式後,一切的迴圈結構都會被破壞,Control Flow 會 變成由基本的 branch 指令來組成。

然而,這樣子雖然對執行速度上可能會有提昇(但是對於腳本語言而言,這 種提昇微乎其微);相對的,只使用基本 branch 指令的程式碼不僅對 statement 數 會有負面的影響,更重要的是會大幅增加人類閱讀的困難性。這對於程式以後的 維護會造成很大的麻煩。

因此,將已經被摧毀的迴圈結構重新建立起來,對於我的後端而言,具有很 高的必要性。

15

3.3.3.1 Branch Analyze

要重新建立迴圈結構,首先就是要搜尋函式底下的所有 Basic Block。首先先 確定函式的進入點,之後則是分析每個 Basic Block 中的 branch 指令,以了解每 個 Basic Block 之間的關係。

3.3.3.2 CFG Rebuild

確認每個 Basic Block 之間的關係後,則要依據關係建立整個函式內部的 Control Flow Graph,以方便 Reloop 步驟進行處理。

3.3.3.3 Reloop

最後則是對 Control Flow Graph 的結構進行優化,並且依此重新建立迴圈結 構。在這一步驟,需要十分可靠的演算法,以確定重新建立迴圈結構後整體的 Control Flow 依然是一致的。

因此,我在這裡採用 Emscripten 的 Relooper 演算法[6]。Emscripten 是已經 公開發布相當時間的軟體,其演算法也經過許多實際的商業軟體測驗過,在可靠

16

[3] 由於 Lua 腳本語言並沒有 continue 指令,故改為使用 goto 指令取代。

3.2.6 Basic Block Initialization

有時候,LLVM 中間表式示會有一些對於中高階語言而言是不必要的指令,

例如:在使用 malloc 取得記憶體後,LLVM 中間表式會使用數個指令來確保記 憶體的指標的確是指在正確的位址。然而,這些指令對於中高階語言,特別是腳 本語言而言,卻是沒有意義的指令。

Basic Block Initialization 步驟的主要工作,就是在 Basic Block 中檢查這些不 必要的指令,並且事先將之排除。如此一來,不但可以增加轉譯後的程式的效率,

還能減少 Instruction Process 步驟的負擔。

3.2.7 Instruction Process

最後的 Instruction Process 步驟,就是進行指令間實際的轉換。不過,就如 同本章一開始所提的,我的後端並不只是進行單純的轉換。為了發揮出高階語言 應該要有的高可讀性與效率,以及配合 Lua 腳本語言的特性,Instruction Process 步驟還包括了特殊功能,簡述如下:

Instruction optimization

由於 LLVM 中間表示式是類似組合語言的低階語言,會有 load 以及 store 這 種指令負責將數值在記憶體以及暫存器之間存取。然而,這些指令一旦轉換成腳 本語言,就變成是單純的參數之間數值的傳遞。而這些數值的傳遞指令,對腳本 語言而言,有相當部分是可以省略的。

17

因此,我的後端會對這些指令進行一定程度的優化,這樣一來,轉譯出來的 腳本語言不管是執行速度還是總 statement 數都會有明顯的改善。

Pointer variable process

對於大部分的腳本語言而言,一般的變數是只能以 By Value 方式傳遞,無 法以 By Reference 方式傳遞。換句話說,腳本語言基本上是沒有指標變數的種概 念的。所以面臨 LLVM 中間表示式的指標變數時,我也必須對其進行特殊處理。

PHI instruction process

由於 LLVM 中間表示式有使用 SSA From 的指令「phi」,而這種指令在腳本 語言中是沒有的,因此要對 phi 指令做特殊的處理。

Boolean to Integer translation

由於 Lua 腳本語言具有一個特性,即布林值與數值無法通用。這與一般「零 即為 True,非零即為 False」的程式語言不同,因此當 LLVM 中間表示式使用到 布林值與數值之間的轉換時,也要特別加以處理。

相關文件