編譯器概論--游逸平 === [**←** 返回主頁面 :house:](https://hackmd.io/1OBQAda9R7C-ufE0ski4Xg) 2022/1 edit: 近期應該會開始把剩下的補完 # Lecture 0 - Why learn Compiler? * Reverse engineering * Data processing * Write better code * Security * Compiler 是驅動眾多技術的關鍵 e.g. [ref1](https://www.facebook.com/groups/system.software2020/permalink/489917168609154/) > If you don’t know how compilers work, then you don’t know how computers work. If you’re not 100% sure whether you know how compilers work, then you don’t know how they work. > [name= Steve Yegge] 注意本課程所提及的內容其實已經稍嫌過時,比較屬於上個世紀的技術,現今編譯器技術發展迅速,此課程內容只佔了非常小的一部分。建議各大學課程可以改用這本〈[Engineering a compiler](https://www.elsevier.com/books/engineering-a-compiler/cooper/978-0-12-815412-0)〉作為編譯器課程教材。有興趣更深入了解的建議再額外閱讀 gcc 或是 clang 的文件,或是 LLVM 框架的文件等**第一手資料**,以及 [你所不知道的 C 語言:編譯器原理和案例分析](/I5Nr3S-4QCaHHhZ1qsQ1Yg)。透過廣泛閱讀才有機會掌握現在迅速變革的技術。 # Lecture 1 - Overview 先備課程:[正規語言--陳穎平](/7XN2PKySSxuepfcm_8Jb1Q) (計算理論) 此課程著重於圖中 compiler 的部份 > ![](https://i.imgur.com/c4oS1wx.png) > [name= [游逸平](https://people.cs.nctu.edu.tw/~ypyou)] ## Compiler ![](https://i.imgur.com/fzxcQaY.png) + Front End (Analysis) + Lexical Analyzer + Syntax Analyzer + Semantic Analyzer + IR - Intermediate Representation + Back End (Synthesis) + Code Optimizer + Code Generator + 本課程不詳細介紹 + Error Handler + 處理各個階段產生錯誤 (Programmer會很感謝貼心的error reporter的) + 本課程不詳細介紹 + Symbol Table + 把各項處理的結果儲存起來的一種資料結構,包括函式名,變數名等,以供各階段需要時調用 ### Why FrontEnd/IR/BackEnd? **Ans: Reuse of Components** 假設今天已經做好一個 c/c++ 的compiler,想改寫給Java用,那就只需要更改負責分析程式碼的 Front End 就好了,Back End可以繼續沿用。 同理,假設今天想從 x86 指令集執行換到 arm 指令集上執行,也只需要更改 Back End 就好 ![](https://i.imgur.com/Uwl73kf.png) ## Assembler 把組語組譯成一個或多個 object program (不可執行) ## Linker 把多個不可執行的 object file merge (例如.o) 起來變成可執行的 object file( 例如 .out/.exe),被引用的函式庫也是在這階段被 link 起來 + Symbol resolution + Relocation ## Loader/Dynamic Linker * Loader: 負責把程式 load 到 memory 中執行 * Dynamic Linker: 是一種 loader,在程式執行中有需要的時候把 .so (Shared Object) 或是 .dll (Dynamic linking library) 載入記憶體,並進行連結 (linking) 與重新定位 (relocation) 的動作 :construction: 可能需要補一小段程式碼轉成 instruction 的過程 :construction: 再豐富一點的 overview (第一部影片的內容) ## Others notes 越容易撰寫越有彈性的語言 compiler 要負起的責任就越大,例如需要考慮到各式的語法組合出來的各種 corner cases。 :construction: ## Interpreter vs Compiler vs Just-in-time compiler Interpreter 像是一台 VM 一般我們用的 compiler 是 ahead-of-time compiler ## Course project overview # Lecture 2 - Lexical 1 ## Lexical Analysis(語彙分析) 將輸入的字串分析,切割成數個 token,例如: ` if ( b == 0 ) a = b;` ,變成 `KW: if` `(` `ID` `NUM` `)` `ID` `=` `ID` `SEMI` ## Token 描述原始碼中一些字串邏輯上的意義的最小單位,為一對 token name 和 attribute 的組合,以下為範例,詳情見課本或作業 <!--編輯這個表格建議不要雙開預覽與編輯,單開編輯就好--> | 所對應的語彙 (lexemes) | Token name | attribute | |:---------------------------:|:----------:|:----------------------------:| | `,` `;` `:` `[` `]` `(` `)` | Delimiters | _no attribute_ | | `array` `begin` `boolean` | keyword | _no attribute_ | | `var1` `var2` | identifier | Pointer to table entry | | `1` `123` `348579` | integer | integer value 1, 123, ... | | `"sdkfjh"` `"gfgjkhgjhg"` | string | string content "sdkfjh" .... | ### Pattern 一段表達式描述 lexeme 如何對應到 token,可以使用正規語言 ([regular expression](https://en.wikipedia.org/wiki/Regular_expression)) 表達式,例如:`[A−Za−z_][A−Za−z0−9_]*` ## Error Recovery (非課程重點) 碰到無法分析的錯誤該怎麼處理? * panic mode: 忽略接下來的所有字元直到看到一個正確的 * Aggressive: 修改 input ## Regular Expression 規則有點多,自己看講義吧,要注意優先度。之前考試有出過給一個特定情境,請寫出能篩選出特定格式的 RE。 推薦一個線上測試 RE 的工具:https://regex101.com/ 他還會逐步分析你寫的 RE,展示 test cases 是如何 match 到的 RE 是有能力限制的,並非所有語言都能描述,例如一個 `(` 就要與一個 `)` 配對或是其他巢狀的敘述是無法使用 RE 描述的,會變成 infinite state machine <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Chomsky-hierarchy.svg/1200px-Chomsky-hierarchy.svg.png" height=200 width=250></img> ## Interaction between Scanner and Parser > ![](https://i.imgur.com/j9RhaIX.png) > > [name=[游逸平](https://people.cs.nctu.edu.tw/~ypyou)] * 讀取 Source Program * Parser 呼叫 scanner 給 token :repeat: * Parser 建構出 parse tree (之後做語意分析) * 過程中 Parser 和 Scanner 會更新 symbol 資訊到 symbol table ## Implementation of Lexical Analysis 其中一種方法是使用 Transition Diagram 來描述並使用 `while` 配`switch` 實做 :bulb: **example 1** ![](https://i.imgur.com/FccuQgn.png) :bulb: **example 2** ![](https://i.imgur.com/5rO2h51.png) :::info **小問題:** 像 `if` `then` `else` 這類的保留字會不會被當成一個 identifier? 會! 方法一:先放到 symbol table 當成保留字 (reserved word) 存起來 方法二:特別為那些保留字寫 rule (e.g. `if -> KW: if`) 來辨識保留字,不過這樣會跟原本的 rule 有衝突,造成 ambiguity,所以要訂出 rule 之間的優先順序。之後在作業中碰到的方法會是用方法二 ::: # Lecture 3 - Lexical 2 ## Finite Automata 輸入一個字串,從 start state 開始照著 transition rules 走,會停在 Final state 的就是 accept 其餘皆 reject。 > Special rule: Epsilon moves,前一個 state 可以直接走到下一個 state ### NFA vs DFA * NFA * 一個輸入可以從一個 state 可以有多個 transition 到不只一個 next state (包含自己) * 可以有 epsilon move * DFA * 一個輸入只會有一個 transition 從一個 state 走到下一個 state(包含自己) * 沒有 epsilon move 兩者實質上是等價的,RE 一定可以轉成同等辨識能力 NFA 和 DFA :::spoiler <summary>實做上會採用 NFA 和 DFA 呢? (點擊展開)</summary> DFA,因為一次只有一個 transition 比較好做,但是圖的大小會比 NFA 大指數倍 (理論上)(實際上可能差不多),之後會有詳細分析 ::: <br> 其中一種儲存 transition 的方法是用 state 對 state 的 transition table,使用查表的方式使用,如此就不用寫一堆 `switch` cases。不過有可能會有太多沒使用到的欄位的缺點,之後會介紹另一個比較有效率的儲存方式。 :::info 不過實際上 regular expression matching engine 實做方法有千百種,依據不同使用場合有不同實做方式,例如 [cregex](https://github.com/jserv/cregex) 就是一個值得參考的短小精悍實做 ::: 實做 NFA 的麻煩點 * Backtrack/Backup * Parallelism * Look-ahead 但是 NFA 對人類來說比較好建構,比較直覺,所以會先 Tompson's construction 建構出 NFA ,之後再轉成 DFA ## Thompson's construction ~~Trivial~~ :construction: ### Analysis * 最多會有 `(n(operators) + n(operands)) * 2` 個 state * 因為一個 operator 和 operand 最多只會產生兩個 state (有些只有一個,例如 concatenate) * 一個 state state,一個 accepting state * 除了 accepting state 的 state 會有 * 一個使用合法字元或 epsilon transit 出去的 transition * 或是兩個 epsilon transition 出去 ## NFA to DFA 核心概念:合併 epsilon transitions 操作方法: * epsilon-closure(state s) * 把一坨 epsilon 連接的 NFA 的 state 合併成一個 * epsilon-closure(set T) * 對 T 裡面的 s 都做一次 epsilon-closure * move(T, a) * 對 T 裡面的 s 都輸入 symbol a ### Subset construction * 產生出 start state * 對 NFA 的 state state 做 epsilon closure 並視作已經處理完的 state * 只要有還沒有處理過的 state * 把還沒有處理過的 state 做 move(T, `a`) * 把 move 完得到的 set 視為處理完的 state * 如果 move 完是 Final state 就標示為 Final state * 新增 transition `a` 在 move 前後的 set 之間 pseudo algorithm: ```c add e_closure(s0) as an unmarked state to Dstates; while (there is an unmarked state T in Dstates) { mark T; for (each input symbol a) { U = e_closure(move(T, a)); if (U is not in Dstates) { add U as an unmarked state to Dstates; mark as final if U contains the original final state; } Dtran[T, a] = U; } } ``` ### 範例 :construction: 有空再用 graphiz 畫 ### Analysis * 每一個 DFA state 其實是一坨 NFA state * 理論上 DFA state 個數量會是 NFA 的指數倍,但實際上他們的數量大致相同 * 可以有不只一個 DFA 認得相同的 RE,即兩者之間等價,所以盡可能合併可合併的 state 取得較小的 DFA,降低實做的複雜度 ### DFA minimization :construction: 範例 我懶得畫,有空再補 或是看誰要幫我補 #### Distinguishability between states: 如何辨識兩個 state 可不可以合併: * Final 和 non-Final 不可合併 * 對所有輸入,不管從 state `s` 還是 `t` 出發都會做一樣的決定,那他們就是 indistinguishable (equivalent) 講義上的定義: - String `x` **distinguishes** state `s` from state `t` if exactly one of the states reached from `s` and `t` by following the path with label `x` is an accepting state - State `s` is **distinguishable** from state `t` if there is some string that distinguishes them - E.g., empty string distinguishes any accepting state from any non-accepting state - E.g., string `bb` distinguishes state `A` from state `B` #### Method * Initialization: 把 state 分成 Final 和 non-final * Division within: :::danger 完了才寫一天就又想偷懶了 需要有人斗內食物QQ ::: # Lecture 4 - lex 作業內容暫時先跳過 # Lecture 5 - CFG # Lecture 6 - Parsing 1 <!--先寫出top down 和 buttom up parser 的overview再詳細解釋--> # Lecture 7 - Parsing 2 # Lecture 8 - yacc 作業內容暫時先跳過 # Lecture 9 - SDT (Semantic Analysis) ## SDD/SDT * **SDD:** 比較 high level - guide line * **SDT:** 比較 low leve - 細節。e.g. 在Body的部份寫action, 什麼時候該做什麼 * **Attrbute:** 存放分析時所需要的資訊,甚至是IR ### 舉例:算式in-fix轉post-fix **SDD:** 看到`expr` `oprator` `term`就把`expr`的 attibute concatenate `term` 的 attibute 再 concatenate `oprator` **SDT:** semantic actions, E.g. ``rest -> + term { print('+') } rest 1`` 把SDD定義的動作實做出來 ## Synthesis/Inherit Attribute * **Synthesis:** 來自child的attribute。大多時機是把小孩的attribute合成成自己的attribute,例如加法。 * **inherit:** 來自父母或兄弟姊妹的attribute。大多時機是對其他人的attribute有attribute有dependency,需要從父母或兄弟姊妹pass attribute來計算自己的。會需要傳遞是因為 parse tree 和 abstract syntax tree 的結構不一樣,所以 parse tree 需要傳遞資訊 ### 舉例:以先乘除後加減的計算算式 請參照講義上 parse tree 的箭頭 \[待補圖\] ## Order of evaluate attribute 正常型況下沒有 cycle 的 DAG (的topology sort的所有可能) 才能執行 SDD,這會需要 two pass,也就是先建構完 parse tree 才建構AST。 如果要邊建構 parse tree 邊執行 SDD,會需要對 SDD 做限制 * S-attribute (S: synthesized) * L-attribute (L: left-to-right), S-attribute 包含於 L-attribute ### S-attribute S-attribute 只要用 buttom up 的順序就能計算,所以 buttom up parsing 就可以邊 parse 邊計算。 ### L-attribute (derivation rule)只能由左至右傳遞 attribute,避免產生cycle,亦即在 parse tree 中只能由左至右,由上至下傳遞 > *Semantic Rule with side effect???* ## 建立Syntax tree * **parse tree:** 透過CFG建立的樹 * **syntax tree:** 透過執行SDD從 parse tree 建立一棵語法樹(小提醒: inherit attribute就在這邊扮演傳遞資訊的角色),syntax tree才是可執行、可分析的tree。第四次作業的分析就是base on syntax tree。 ## Implementation of SDT ### General SDT implementation * 建一顆 parse tree * 執行 action 增加 node * post traversal 產出 Syntax tree ### not that general SDT **post-fix SDT:** 都放在 body 最右手邊的 action。最簡單實做的SDT。使用一個stack 舉例:用一個stack和多個action處理四則運算。 **SDT with action inside productions:** 還沒 match 完整條 grammar rule 就執行 action。不是所有這類的SDT都能正常的邊 parse 邊執行。 有問題的SDT在 parsing 的時候會有conflict,亦即在做LR parsing的時候會有conflict 不確定yacc看到有問題的SDT(講義上的舉例)會怎麼做,需要查一下。保守作法就是action都寫在最後面就好了 有問題的SDT還是可以執行的,但是就是沒辦法邊parse邊執行,需要採正常的兩階段執行 消除left recursion *why?* > *blablablablab...* L-attributed SDD to SDT :construction: 待補.... # Lecture 10 - IR 編譯器的中介語言,需要有IR的原因在 Lecture 1 已經提及了。 IR有high level到low level之分,高的比較接近我們寫的程式碼,低的比較接近machine code **Syntax tree:** 把parse tree精鍊過只剩下所需資料的tree **DAG:** 一樣leaf都是operand,不過operator不會重複出現,還有GVA(Global Value Numbering)最佳化。產生一個node之前會檢查是不是已經存在了,已經存在了就直接指過去,<ValueNumber , Signature> pair存在hash table裡面來查詢 signature: <op, l, r> ## Basic info 之前提到的Three address code (e.g. `t1 = t2 + t3`) 比較接近 machine code表示法,接下來說明一些資料結構來存放three address code 課本裡的IR有8種operation,我們這邊以這8種為例 `ph1` `ph2` `ph3` `ph4` `ph5` `ph6` `ph7` `ph8` > *ph = placeholder,等等補* > 詳情請自行參考講義 不管什麼語言都轉成這8種opration ### 範例:把Do-while loop轉成IR using symbolic label/numbering label兩種後者比較接近machine code 選擇允許的IR的operation是一個很重要的設計 * 比較接近machine code的設計比較容易轉成真的machine code * 但是會不好維持原本的架構 * 所以會分成很多層級的IR,high到low給不同的用途 ## IR的資料結構 ### Quadruple < op - arg1 - arg2 - result > three address直接的轉譯的結果 透過temperary存放result * 直覺 * 比較好做最佳化,像是instruction reordering(不能打破dependency) * 在triple裡面移動指令VN reference也要改,Quad不用,可以直接移動 * 但是比較佔空間 * 所以就有Indirect Triple出現啦 ### Triple 沒有temperary需要value number來reference其他operation的計算結果 行為上和syntax tree一樣(op, arg1, arg2) ### Indirect Numbering Triple的變形,解決需要reference的問題。多maintain一個array,這個array會存放每個指令,對調指令時是對調array裡面存放指令的順序而不是更動指令本身,所以就不用動reference了 不過大多open source的IR還是quad因為比較直覺(編譯什麼的吃多一點記憶體很正常的吧) ### SSA(課本沒有) 一個變數只會被定義一次 所以會需要renaming,如果有重複用到同一個變數的話 不過在condition的時候會有confliction,所以會用phy function來解決(最後轉成machine code的時候會拿掉) 現今open source compiler常用 * 優點 * code optimization比較方便 * 不同命名消除dependency,做什麼調換順序的比較方便,甚至可以發覺更多可以最佳化的點 詳情請見:〈[SSA-based Compiler Design](https://link.springer.com/book/10.1007/978-3-030-80515-9)〉 ## Type Checking 在Symantic Analysis的時候會檢查。現在CPU都是control unit和arithmetic-logic unit是分開的,所以data存在哪個相對位址 (storage layout,*等等補圖*) compiler要先決定好,arithmetic-logic unit 只負責去存取 **Static data:** 存放的位置不改變,e.g. 全域變數, static variable **dynamic data:** e.g. `malloc`, `new`, local variable <!--小提醒:static在RAM裡面最多4GB, 考GPE的時候要小心歐 好吧其實我不太確定 --> Type Expression:表示型別的方式, variable, function都有,在動態語言裡面甚至有type variable在type expression裡面來攜帶型別資訊。儲存上可以用graph來儲存 什麼叫做「一樣」的型別? * Structure eq. : 以下任一者成立即相同 * same basic type * same structure * typedef * Name eq. * 要完完全全一模一樣(待補充) * structure eq. 三條都滿足 ## Storage layout of Names 看到變數宣告就知道要分配多少空間、放在哪,再存放到symbol table,下次要存取變數就查表,直接去那個位置拿資料 > 小疑問,如果一個頻繁呼叫的函式,裡面用local variable是合適的嗎?因為local variable會一直不停被allocate, deallocate,這樣很耗時嗎? * address alignment 當存取資料的位址不能被4(在一次存取4 byte的平台上而言,帶補充)整除時,就會多耗很多operation去拿資料,例如一次拿出來之後再抽絲剝繭拿出你要的 在traverse parse tree的時候用SDT產出相關資料 * 例如建構出syntax tree中陣列的型別資訊 * 然後就可以用型別資訊決定變數要存放在table中哪個記憶體位置 * Symbol table裡面存的是相對位置,table起始位置再加上offest才會存到你要的symbol(或是另一張symbol table) * 起始位置用一個top pointer指著,方便存取 以上在你做完作業4之後會很有感覺 ### Translation of Expressions 用SDD定義看到各express的grammar rule該怎麼做 1. 做運算後存起來到new出來的temp或是覆蓋 2. 產生IR(new temp出來可以對應到儲存資訊到temp register) array的記憶體擺放方法,這個是compiler決定好的,像fortran這種row major的擺法或是C這種column major 存取方法: base + offset * 兩種Type checking * **Type Synthesis:** 可以由以宣告的型別推斷expression的型別,所有的東西都有type expression,每做一個動作就是推斷出目前該有的型別為何 * **Type Inference:** 有type variable, 用他來說明型別 * 有Overloading時該怎麼知道在用哪個? 一樣是作type checking,每個東西都有type expression,checking完一樣的就用他。Template function也是一樣的道理,檢查傳進來的參數的型別 e.g. function的type expression可以長這樣 $$ (for\ all)\ a\ list(a) -> integer $$ input -> output ### Compiler正規化推斷型別的流程 <--我覺得這部份會考歐 用已知的資訊推導未知資訊 (帶補講義圖p.62) * unification * ? * ?? * ??? * 演算法 * 遞迴unify(a,b) 題外話:Just In Time compiler 也是需要先做 type inference ## Control flow Statements ### Boolean expression **short circuit expression** * 在or中有人true就直接回傳true了 * 在and中有人false就直接回傳false了 缺點: * 可能在剩餘的expression中有必須被執行的statement被跳過了 * 不過這應該是programmer的問題 ### SDD for Boolean expression backpatching: 把label換成instruction編號 * 把IR存在array裡面 # Lecture 11 - Runtime # Lecture 12 - Code Generation & Code Optimization Compiler 最有趣的地方都在這邊 ## 參考資料 * [淺談編譯器最佳化技術](https://ncku365-my.sharepoint.com/:b:/g/personal/p76104809_ncku_edu_tw/EW9e2-ghXjFDpgTbuRSoU7wB_YinfvhZAyBTUOXGks1vRA?e=qEBgta) - 把基本的最佳化方法淺顯易懂的介紹了一遍 # Lectuer 13 - Controlflow [**←** 返回主頁面 :house:](https://hackmd.io/1OBQAda9R7C-ufE0ski4Xg) --- ## 廢話 G_G ... 我怎麼寫起來都像廢話,排版也亂七八糟 之後有空慢慢寫 :zzz: 不知道專題和準備推甄會花多少時間 *2022 - March -* > 老師:欸我明年被指派要上編譯系統= = > 老師:你有沒有修過編譯器 > 我:有 > 老師:((:DDD)) > > 呃......好我懂了 那明年來上這個好了 How to compile C/C++ for WASM, pure Clang, no libs, no framework https://github.com/ern0/howto-wasm-minimal 或是參考 [你所不知道的 C 語言:編譯器原理和案例分析](/I5Nr3S-4QCaHHhZ1qsQ1Yg),從最佳化開始講之類的,至少用現代化一點的教材 ## 參考資料 * [淺談編譯器最佳化技術](https://ncku365-my.sharepoint.com/:b:/g/personal/p76104809_ncku_edu_tw/EW9e2-ghXjFDpgTbuRSoU7wB_YinfvhZAyBTUOXGks1vRA?e=qEBgta) - 把基本的最佳化方法淺顯易懂的介紹了一遍 * [Optimizations in C++ Compilers](https://queue.acm.org/detail.cfm?id=3372264) - 比較詳細一點的最佳化 * [一點都不深入的了解 Compiler、 Interpreter 和 VM](https://www.spreered.com/compiler_for_dummies/) - 讓你貼近生活一點1 * [Let’s Build A Simple Interpreter. Part 1. ](https://ruslanspivak.com/lsbasi-part1/) - 讓你貼近生活一點2 * [你所不知道的 C 語言: 執行階段程式庫 (CRT)](https://hackmd.io/@sysprog/c-runtime?type=view) - Run Time 的補充資料 * [2000 行 C 編譯器實做](https://github.com/jserv/shecc) - 教學導向,清楚好懂 * [Compiler explorer](https://gcc.godbolt.org/) 即時預覽一段程式碼及其被編譯出來的組合語言 * Godbolt's talk at cpponsea 2019: [What Everyone Should Know About How Amazing Compilers Are](https://www.youtube.com/watch?v=w0sz5WbS5AM),快速提供提供幾個範例告訴你 compiler 擅長做什麼以及是如何最佳化的 * Godbolt's talk at CppCon2017: [What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid](https://www.youtube.com/watch?v=bSkpMdDe4g4) ## LLVM ***聽過 GCC 就也要聽過 LLVM*** * [COSCUP2016 - LLVM框架、由淺入淺](https://www.slideshare.net/hydai/coscup2016-llvm) - 影片應該可以在Youtube找到,但是我有點懶 * [LLVM 總是打開你的心:從電玩模擬器看編譯器應用實例 ](https://www.slideshare.net/jserv/nes-llvm) - 其實你也可以自己做一台紅白機,Jserv 最近的課就在做一台 [GameBoy 模擬器](https://www.facebook.com/groups/system.software2020/permalink/486446318956239/) * [COSCUP 2014 : open source compiler 戰國時代的軍備競賽 ](https://www.slideshare.net/kitocheng/coscup-2014-open-source-compiler) - GCC vs LLVM! 資料有點久了,不過可以大致當個歷史教材看看