編譯器的編譯基本過程

崤嶙的部落格發表於2013-12-12

  編譯器最基本的功能就是把高階語言(例如C/Fortran)編寫的程式碼轉化為機器指令(就是01串),從這個角度來說它本質上是個轉換過程。經典的編譯過程主要包括:

  1、詞法分析(Lexical Analysis)

  詞法分析就是從輸入程式碼中識別出各種記號(token),例如對於C語言我們就需要知道if,else等是語言的關鍵字,myvar是個標識,而123myvar不能被識別為一個標識。負責實現詞法分析的模組有時也稱為scanner。

  詞法分析的關鍵當然是語言定義的規則了,比如哪些是關鍵詞,哪些是合法的標識等,這些規則一般是通過正規表示式(RE,Regular Expression)來給出,執行時從輸入緩衝區中讀入一部分,然後看和哪個RE匹配就知道它到底是個什麼token。

  下一個問題就是正規表示式的匹配過程如何實現,經典理論對此都會提到有限狀態機(FSM, Finite State Machine)。關於FSM在可行性計算裡一般都會有不少篇幅分析,在編譯裡談到的FSM和RE主要是如何從輸入的構成出對應的FSM。構造的過程一般分為三個步驟,(1)根據Thompson構造法從RE構造出對應的非確定性有限狀態機(NFA, Non-deterministic Automata),(2)經過不斷計算epison-閉包(也成為可到達集)構造出確定性有限狀態機(DFA, deterministic Automata), (3)將DFA最小化,方法是增量式地合併不可區分(對於同一輸入的下一個狀態都一樣)的兩個狀態。

  2、語法分析

  語法分析的輸入是一連串的token(詞法分析的輸出),根據語言的語法規則不斷解析最後得到一棵抽象語法樹(AST, Abstract Syntax Tree),負責語法分析模組通常也被叫做Parser。在詞法分析中,我們經常使用正規表示式來表示語言所接受的token的規則,類似的,在語法分析中,我們使用文法(Grammar)來表示語言的語法規則,這也早期計算機語言設計中的研究熱點(同樣也是大學裡學習編譯時最容易讓人頭暈的東西)。

  編譯裡常說的文法指的是一種上下文無關文法(Context-Free Grammar),簡單地說文法裡包含終結符(terminal,就是26個字元、數字等等)、非終結符(nonterminal,實際是一種抽象)和產生式(production)。上下文無關文法要求每個產生式的左邊必須恰好是一個非終結符,而右邊是0個或多個終結符與非終結符的組合,最後整個文法還必須有一個起始符(某個終結符)。文法裡還有些很重要的基本概念,例如推導(derivation)、歸約(reduction)、二義性(ambiguity)、最左推導等等。

  文法中最重要的基本概念是FIRST集和FOLLOW集的構造。根據這兩個集合就可以很容易構造出一個預測分析表,每個行的名字是一個非終結符,每個列的名字是一個終結符,如果每個表格內沒有兩個以上的項,那麼說明是一個LL(1)文法(Left-to-right parse, Leftmost-derivation, 1-symbol lookhead),簡單地說就是向右邊看一個符號就能確定下一步動作。當原文法不是LL(1)文法時,可以嘗試通過消除左遞迴(Eliminate Left Recursion)和提取左因子(Left Factoring)對原文法進行變形得到等價的LL(1)文法。

  第二種文法就是LR(k)文法(Left-to-right parse, Rightmost derivation, k-token lookhead)。這種文法的解釋過程一般通過棧輔助實現,中間主要有兩種動作:shift(就是將當前輸入入棧)和reduce(選擇產生式並從棧中彈出符號執行歸約操作)。LR(0)的構成過程就是從起始符所在的產生式開始構造item,然後對每個item針對每個可能的input構造它的出邊(同樣還是一個item),最終所有的item形成一個有限狀態機。接下來構造有限狀態機,對於每個狀態,如果出邊是一個終結符,在對應表格記入shift操作,如果是非終結符則記入goto操作。如果S->x.這種item集,那麼對每個終結符r,都記入(S, r)位置處為shift操作。

  第三種SLR文法與LR(0)非常相似,區別在於生成分析表格時,對於S->x.這種item集,僅僅對於r輸入FOLLOW(S)才在(S, r)位置處記入shift操作。

  最後一種LALR(1)相對於LR(0)而言引入了活字首,構造思路仍與LR(0)類似,但是構造出來的預測分析表更大。

  綜合起來,各文法表述能力:LL(0)<LR(0)<SLR<LALR(1)<LR(1)<LR(k),LL(1)<LR(1)

  3、語義分析(Sematic Analysis)

  語義分析包括一些經典的問題。

  (1)型別檢查(Type Checking),例如在語法樹上a+b看起來是沒問題的,因為a和b都是合法的變數名,並且語法中支援變數間+這種操作。但是可能a是一個字串,而b是一個浮點數,這兩者之間的+操作就不符合語義規範了,這種問題在這個階段都會被找出來。

  (2)符號管理,最經典的問題就是如何管理變數(變數的名字,型別,變數的作用域(scope)等),在分析程式碼時,符號管理肯定是被頻繁的搜尋,因此它通常會使用hash來組織。

  4、中間程式碼(IR, intermediate Representation)生成

  IR是非常非常重要的,它被引入的初衷是提高編譯器開發的效率。IR是編譯過程的一個匯聚點,在IR之前我們通常都認為是編譯的前端,而IR之後是編譯的後端,這樣當編譯器需要多支援一種高階語言時主要工作就是提供一個前端,而當需要移植到一種新的平臺上時主要工作就是提供對應的後端。關於IR的表示典型的有三地址碼。IR生成的過程就是將一棵抽象語法樹(這是編譯器前端對原始碼的理解和抽象)變成一串IR定義的程式碼(IR指令種類簡單,這便於優化)。這個轉換過程都是比較固定的套路,例如if-else,while/for等基本結構如何轉都是一個固定的套路。

  5、編譯優化

  這一部分是現代編譯器最核心所在,主要有兩類,一類是通用的優化手段,比如死程式碼刪除、迴圈不變數外提、強度削弱等,另一類就是體系結構相關的,說白了就是某種體系結構針對某類應用提供了特殊指令,例如intel的MMX,SSE2等等。為支援優化工作的開展,我們首先需要能夠比較方便的描述程式碼。最基本的當然是一條指令,但是這個太細微,於是往上抽象出基本塊(Basic Block),這個基本上是所有優化開展必備的工作,然後多個基本塊還可以構成一個超級塊(region)。此外,經典的方法還包括控制流分析和資料流分析,這裡常用的包括d-u鏈等。最後一個經典的topic就是暫存器分配。

  6、目的碼生成

  這裡直接和具體平臺相關,這裡的平臺同時包括軟體和硬體,例如哪種目標檔案格式(ELF, PE),哪種平臺(指令集)。不過現在編譯器一般生成的是字元形式的彙編檔案,所以前面一個問題基本不大,主要影響在後者。

相關文章