1. 程式人生 > >編譯原理入門篇|一篇文章理解編譯全過程

編譯原理入門篇|一篇文章理解編譯全過程

# 編譯過程 ## 編譯目標 目標:把原始碼變成目的碼 1.如果原始碼在作業系統上執行:目的碼就是“彙編程式碼”。再通過彙編和連結的過程形成可執行檔案,然後通過載入器載入到作業系統執行。 2.如果原始碼在虛擬機器(直譯器)上執行:目的碼就是“直譯器可以理解的中間形式的程式碼”,比如位元組碼(中間程式碼)IR、AST語法樹。 編譯過程可以分為這幾個階段,每個階段做了一定的任務,層級的讓下一個階段進行。 ![](https://img2020.cnblogs.com/blog/1454456/202010/1454456-20201031102132451-1095374786.png) ## 詞法分析 編譯器讀入原始碼,經過詞法分析器識別出Token,把字串轉換成一個個Token Token的型別包括:關鍵字、識別符號、字面量、操作符、界符等 比如下面的C語言程式碼原始檔,經過詞法分析器識別出的token有:int、foo、a、b、=、+、return、(){}等token ``` int foo(int a){ int b = a + 3; return b; } ``` ## 語法分析 每一個程式程式碼,實際上可以通過樹這種結構表現出其語法規則。 語法分析階段把Token串,轉換成一個體現語法規則的、樹狀資料結構,即抽象語法樹AST。 AST樹反映了示例程式的語法結構。 比如下面對應的一段C語言程式碼,對應的AST抽象語法樹如下所示: ``` int foo(int a){ int b = a + 3; return b; } ``` ![](https://img2020.cnblogs.com/blog/1454456/202010/1454456-20201031102227842-989046912.png) AST抽象語法樹 AST樹長成什麼樣,由語法的結構有關。 比如 上面C語言程式碼中對函式的語法定義如下:語法分析器就按照語法定義進行解析,就是從上到下匹配的過程。 也就是先匹配function的規則,匹配函式型別type、函式名name、函式引數parameters、函式體 當匹配函式引數時,就去匹配parameters的規則 當匹配函式體時,函式體由一個個語句組成,就去匹配各個語句stmt的規則。 ``` function := type name parameters functionBody parameters:= parameter* functionBody:= stmt returnStatement ``` 生成 AST 以後,程式的語法結構就很清晰了,但這棵樹到底代表了什麼意思,我們目前仍然不能完全確定,要在語義分析階段確定。 ## 語義分析 語義分析階段的任務:理解語義,語句要做什麼。 比如+號要執行加法、=號要執行賦值、for結構要去實現迴圈、if結構實現判斷。 所以語義階段要做的內容有:上下文分析(包括引用消解、型別分析與檢查等) 引用消解:找到變數所在的作用域,一個變數作用範圍屬於全域性還是區域性。 型別識別:比如執行a+3,需要識別出變數a的型別,因為浮點數和整型執行不一樣,要執行不同的運算方式。 型別檢查:比如int b = a + 3,是否可以進行定義賦值。等號右邊的表示式必須返回一個整型的資料、或則能夠自動轉換成整型的資料,才能夠對型別為整型的變數b進行復制。 比如之前的一段C語言程式碼,經過語義分析後獲得的資訊(引用消解資訊、型別資訊),可以在AST上進行標註,形成下面的“帶有標註的語法樹”,讓編譯器更好的理解程式的語義。 ![](https://img2020.cnblogs.com/blog/1454456/202010/1454456-20201031102308523-1286792681.png) 也會將這些上下文資訊存入“符號表”結構中,便於各階段查詢上下文資訊。 符號表是有層次的結構:我們只需要逐級向上查詢就能找到變數、函式等的資訊(作用域、型別等) ![](https://img2020.cnblogs.com/blog/1454456/202010/1454456-20201031102318387-1392209429.png) 接下來就可以 解釋執行:實現一門解釋型的語言 Tip:編譯型語言需要生成目的碼,而解釋性語言只需要直譯器去執行語義就可以了。 實現AST的直譯器:在語法分析後有了程式的抽象語法樹,在語義分析後有了“帶有標註的AST”和符號表後,就可以深度優先遍歷AST,並且一邊遍歷一邊執行結點的語義規則。整個遍歷的過程就是執行程式碼的過程。 舉一個解釋執行的例子,比如執行下面的語義: - 遇到語法樹中的add “+”節點:把兩個子節點的值進行相加,作為“+”節點的值。 - 遇到語法樹中的變數節點(右值):就取出變數的值。 - 遇到字面量比如數字2:返回這個字面量代表的數值2。 ## 中間程式碼生成 在編譯前端完成後(編譯器已經理解了詞法和語義),編譯器可以直接解釋執行、或則直接生成目的碼。對於不同架構的CPU,還需要生成不同的彙編程式碼,如果對每一種彙編程式碼做優化就很繁瑣了。所以我們需要增加一個環節:生成中間程式碼IR,統一優化後中間程式碼,再去將中間程式碼生成目的碼。 中間程式碼IR的兩個用途:解釋執行 、程式碼優化 解釋執行:解釋型語言,比如Python和Java,生成IR後就能直接執行了,也就是前面舉出的例子。 優化程式碼:比如LLVM等工具;在生成程式碼後需要做大量的優化工作,而很多優化工作沒必要使用匯編程式碼來做(因為不同CPU體系的組合語言不同),而可以基於IR用統一的演算法來完成,降低編譯器適配不同CPU的複雜性。 ## 程式碼優化 一種方案:基於基本塊作程式碼優化 分類:本地優化、全域性優化、過程間優化 本地優化:可用表示式分析、活躍性分析 全域性優化:基於控制流圖CFG作優化。 控制流圖CFG :是一種有向圖,它體現了基本塊之前的指令流轉關係,如果從BLOCK1的最後一條指令是跳轉到 BLOCK2, 就連一條邊,如果通過分析 CFG,發現某個變數在其他地方沒有被使用,就可以把這個變數所在程式碼行刪除。 過程間優化:跨越函式的優化,多個函式間作優化 優化案例: 代數優化: 比如刪除“x:=x+0 ”,乘法優化掉“x:=x*0” 可以簡化成“x:=0”,乘法優化成移位運算:“x:=x*8”可以優化成“x:=x<<3”。 常數摺疊: 對常數的運算可以在編譯時計算,比如 “x:= 20 * 3 ”可以優化成“x:=60” 刪除公共子表示式:作“可用表示式分析” ``` x := a + b y := a + b //y := x ``` 拷貝傳播:作“可用表示式分析” ``` x := a + b y := x z := 2 * y //z:= 2 * x ``` 常數傳播: ``` x := 20 y := 10 z := x + y// z := 30 ``` 死程式碼刪除:作變數的“活躍性分析” 活躍性分析(優化刪除死程式碼,沒用到的變數) 資料流分析:使用“半格理論”解決多路徑的V值計算集合問題,不在程式碼下面集合的變數就是死程式碼。 ## 目的碼生成 目的碼生成,也就是生成虛擬機器執行的位元組碼,或則作業系統執行的彙編程式碼 **程式碼生成的過程,其實很簡單,就是將中間程式碼IR逐個翻譯成想要的彙編的程式碼** 那麼目的碼生成階段的任務就有: - 選擇合適指令,生成效能最高的程式碼。 - 優化暫存器的分配,讓頻繁訪問的變數,比如迴圈語句中的變數放到暫存器中,暫存器比記憶體快 - 在不改變執行結果下,對指令做重排序優化,從而充分運用CPU內部的多個功能部件的並行能力 本文參考: [編譯原理實戰課](https://time.geekbang.org/column/intro/314?code=e9FXzvEdyU3ljJnDE8fpVjHW46dF0vKS0XZYn6%2FXaDk%3D&utm_term=SPoster) [編譯原理之美](https://time.geekbang.org/column/intro/219?code=PXe7Oa54n-e8dnzo5vlWkiNvpR4Q1rEc1i2Fki3XB1Q%3D&utm_term=