Gcc 1.31 考古(三) 表達式 ast 和 rtl 快速概覽

在前一篇極其概略地描述了一個簡單聲明 "int x" 的生成 ast 的過程, 此篇打算
一樣概略地描述一個簡單表達式 "x+2" 的生成 ast 的大體過程. 由於在表達式
中會使用聲明的變量, 以及一些基本的 tree_node 樹節點結構, 因此先以一個
簡單聲明開始的. node


實際上表達式 "x+2" 不能在 C 語言頂層(toplev) 上書寫, 以及 x 須要實現聲明,
因此實際最簡單的出現該表達式的 C 程序看起來以下: ide

   int x;  // 或用 extern int. 
   int f() {  // 或者用 int f(int x), 此時 x 是函數的參數. 
      x + 2;    // 咱們關心的表達式實際寫在這裏. 
   } 函數

每次都寫 int x; int f() 比較繁冗, 因此就省略掉這些外圍的部分代碼. 工具


對於表達式的產生式部分, 使用前一篇中的方法論: 
   1. 自頂向下列出 parse 此例子相關的產生式, 並編號, 以方便指代. 
   2. 自底向上歸約, 計算每一個非終結符的語法值. 測試

 

1. 一個 C 語句: (選擇表達式的那個) 
   stmt -> expr:$1 ';' {$3}
2.  產生式 1.$1 :
   expr -> nonnull_exprlist:$1 {$2}
3. 產生式 2.$1 : 
   nonnull_exprlist -> expr_no_commas:$1 {$2}
4. 產生式 3.$1 : (須要選擇2個產生式) 
   4.1 expr_no_commas -> primary:$1; 
   4.2 expr_no_commas -> expr_no_commas:$1 '+' expr_no_commas:$3 {$4};
5. 產生式 4.1.$1:  
   5.1 primary -> IDENTIFIER:$1 {$2}; 
   5.2 primary -> CONSTANT:$1; 優化

 

==== ui

下面按照自底向上地方式歸約表達式 "x + 2 ;".
這個表達式由四個詞法符號構成 標識符 "x", 加法符號 '+', 整數常量 2, 分號 ';'. spa

-- 歸約 5.1
標識符 "x" 被按照產生式 5.1 歸約. 
   5.1 primary -> IDENTIFIER:$1 {$2}; 指針

按照習慣, 終結符被寫做大寫的, 如 IDENTIFIER 表示詞法器識別的標識符. 
詞法器爲標識符返回的詞法值爲 IDENTIFIER, 詞法器爲該標識符創建一個 tree_identifier 節點,
其節點值可寫做 (identifier_node x), 而後在當前詞法域中查找該符號的聲明(decl), 咱們已假設
該符號("x") 被聲明爲一個全局變量 ("int x"), 則查找到的聲明爲 (var_decl int x), 這個聲明被放入
到一個全局變量 lastiddecl 中. 調試

關聯代碼塊 $2: 
  $$ = lastiddecl; // 即 (var_decl int (identifier_node x))
(實際的代碼塊還有檢查各類狀況的代碼, 咱們暫時忽略, 只列出符合此實際例子的代碼)

這樣非終結符 primary 的語法值就是 (var_decl int x).


-- 歸約 4.1 
   4.1 expr_no_commas -> primary:$1;  
按照缺省語義動做 $$=$1, 此時非終結符 expr_no_commas 的語法值爲 $1 即 (var_decl int x).  

(這裏能夠在紙上進行推導, 而後調試程序以進行驗證.)  
順便說一下, expr_no_commas 表示 C 表達式, 不含逗號分隔. 如 "x*2", 用逗號分隔的例子  
是 "x, y-3, 4+z*5" 是一個 expr, 可是含三個 expr_no_commas.   

-- 歸約 5.2
此步驟歸約(reduce) 整數常量 2. 該常量以前的 加法符號 '+' 被移入(shift) 到語法分析棧中,
此時該語法分析棧的內容是: [...前面的略...  expr_no_commas '+' ), 其中 expr_no_commas
是上一個步驟歸約的, '+' 是移入的. 
   5.2 primary -> CONSTANT:$1;  

終結符 CONSTANT 是詞法器識別並返回的, 其語法值是一個 tree_int_cst 類型的 tree_node,  
咱們寫做 (int_const 2), 按照缺省語義動做, 非終結符 primary 的語法值爲 (int_const 2).  

此時再執行一次 4.1 的歸約, 對應 expr_no_commas 的語法值爲 (int_const 2).  
這時語法分析棧的內容: [... expr_no_commas '+' expr_no_commas ), 已經具有了按照  
產生式  4.2 歸約爲更大的一個 expr_no_commas 的條件.  


-- 歸約 4.2    
   4.2 expr_no_commas -> expr_no_commas:$1 '+':$2 expr_no_commas:$3 {$4};  

由 $1 指代的那個 expr_no_commas 語法值爲 (var_decl int x), 由 $3 指代的那個 
expr_no_commas 的語法值爲 (int_const 2), 它們都是 tree_node 的實例.   
另外, 加號 '+' 在詞法器中返回的語法值 $2 爲 enum tree_code 類型的枚舉值 PLUS_EXPR,  
從名字看就知道是 '加法表達式'.  

代碼塊 $4:  
   $$ = build_binary_op($2, $1, $3);  
代入 $2, $1, $3 的值, 寫爲:  
   $$ = build_binary_op(PLUS_EXPR, (var_decl int x), (int_const 2))  

函數 build_binary_op() 比較複雜, 咱們先略過其過程(之後討論), 看其返回的結果: 
   $$ = (plus_expr (var_decl int x) (int_const 2))

這裏 plus_expr 表示構造了一個 tree_node 節點, 代碼(code)是 PLUS_EXPR, 它的結構
對應爲 struct tree_exp, 加法操做擁有兩個操做數(operand), 此例分別是 x, 2.

咱們從內向外的簡化這個 $$ 值的寫法, (var_decl int x) 簡寫爲 x, (int_const 2) 簡寫爲 2.
  $$ = (plus_expr x 2), 再把 plus_expr 簡寫爲 +, 則
  $$ = (+ x 2), 則這樣的結果看上去就是 lisp 表達式的樣子了.

一樣, 表達式 "x+y*2" 按照上面相似的方式歸約, 簡化, 最後的 expr_no_commas
的語法值就是 $$ = (+ x (* y 2)), 也是一個 lisp 表達式的樣子.

-- 歸約 3: 
   nonnull_exprlist -> expr_no_commas:$1 {$2}  
   nonnull_exprlist -> nonnull_exprlist ',' expr_no_commas  (另外一個產生式)  

已知 $1 是一個表達式節點: (+ x 2), 代碼塊 $2 爲: 
   $$ = build_tree_list(null, $1)

函數 build_tree_list() 至關於 Lisp 中的函數 cons(), 用於構造一個 list. 咱們能夠簡寫爲: 
  $$ = ($1) = ((+ x 2)). // 外層括號表示列表, 裏層括號表示 '+'表達式.
由於非終結符 nonnull_exprlist 表示 expr_no_commas 的列表, 多個 expr_no_commas
是經過 tree_list 鏈接起來的, 以 "x+2, y-3, 4+z*5" 例子來講, 對應的三個 expr_no_commas
爲: (+ x 2), (- y 3) (+ 4 (* z 5)), 歸約到 nonnull_exprlist 就成了 list: 
  ( (+ x 2)  (- y 3) (+ 4 (* z 5)) )  ... 

因此咱們說, Lisp 中的 list 在程序中常常出現...

因此最後非終結符 nonnull_exprlist 的語法值爲 tree_list:(expr_no_commas+).

-- 歸約 2: 
   expr -> nonnull_exprlist:$1 {$2}  
已知 nonnull_exprlist 是 tree_list:(expr_no_commas+), 代碼塊 $2: 
   $$ = build_compound_expr($1)

這裏函數 build_compound_expr() 咱們先簡化爲僅使用 $1 表達式列表的最後一個
做爲結果表達式返回. (多個逗號分隔的表達式, 按照 C 語言要求, 取值取最後一個表達式的).

這樣非終結符 expr 是一個結構爲 tree_exp 的 tree_node, 例子 "x+2" 中就是 (+ x 2) 這個
表達式節點. 更復雜的 compound_expr 之後咱們有機會再探討.

-- 歸約 1: 
   stmt -> expr:$1 ';' {$3}  

已知 $1 是一個表達式節點 tree_exp:(+ x 2), 代碼塊 $3 爲:  
   expand_expr_stmt($1)    // 僅留下核心部分, 其它無關的略.  

函數 expand_expr_stmt() 的最核心的任務是將 tree_exp, 也就是 AST 轉換爲 RTL.
這個關鍵轉換步驟是咱們下一階段要研究的重點, 但 parse 語法以構造 ast 過程到這裏
是一個階段的結束了. (意思是咱們分清楚各個階段, 就是在這個點劃分和方便理解)

對於更加複雜的表達式, 咱們在之後遇到的時候再研究.

==== 如下快速概覽這種簡單表達式從 ast 轉換到 rtl 的過程.

在產生式 stmt -> expr ';' 的關聯語義代碼塊中, 右側的非終結符 expr 是表達式的 ast,
調用的函數 expand_expr_stmt(expr) 中完成從 ast 都 rtl 的轉換. (可能該函數還進行別的
任務, 但就 ast->rtl 的轉換任務, 該函數是主要完成的部分). 簡而言之, 這個轉換函數的
輸入是 ast, 輸出/產出是 rtl. (其內部使用關鍵性的龐大的 expand_expr() 函數完成任務)

爲了研究 ast->rtl 的轉換, 須要一些相關的理論和知識: 
   1. 知道 ast 的基本結構和一些基本 tree 節點知識 (前面說起過一小部分) 
   2. 知道 rtl 的基本結構, 少許 rtl 節點知識. 
   3. 從 ast->rtl 轉換的基本思想.

其中 1,2 在 tree.def, rtl.def 文件中有基本的信息, 在所列參考網頁上也有不少, 出於各類
緣由(極可能是懶惰), 不想這裏敘述更多了. 下面簡要敘說 ast->rtl 基本思想.

已知 ast 是一顆樹, rtl 也是樹, 這轉換是樹之間轉換, 基於 rtl 可用的指令操做數存儲類型.
對於 ast 中任一個操做(如 +, *) 的對應的指令(如 add, mul), 這些指令的操做數的
存儲類型(MEM, IMM, REG) 知足指令的要求, 則能直接轉換. 若是不知足, 則需將子樹
先歸約爲一個僞寄存器(pesudo REG) 以知足操做數存儲分類的要求, 而後再轉換. 
轉換的過程是在對 ast 樹的後序遍歷中實現(遞歸調用 expand_expr() 函數), 
即自底向上的一次重寫. (對於 1 個或多個操做數等狀況也相似)

以一個表達式例子說明: "x + y*3". 這個表達式寫做咱們前面所述簡化的 ast 爲: 
   (+ x (* y 3)), 用圖示更清晰以下(畫得很差, 示意便可): 

                                       

                                           


 

如今這顆樹最終要變換爲兩個 rtl 樹 (具體變換多少, 如何變換都與目標機器的指令集密切相關,
我這裏描述的是缺省 i386 體系, 通常的狀況下). x 子節點對應 MEM (內存地址操做數) 模式,
加法 + 接受這種 MEM 類型的做爲其操做數. 但子樹 (* y 7) 不能直接作操做數, 須要先歸約.
該子樹而後歸約爲(reduce)一個僞寄存器 r_10, 而後樹 (+ x r_10) 變換爲另外一個 rtl. 歸約產生
對展開函數 expand_expr() 的遞歸調用.

下面先給出最後發行的 rtl, 共兩個:  

    1. (set (reg:SI 10) 
              (mult:SI (mem:SI (symbol_ref:SI ("y")))
                           (const_int 7))) 
    2. (set (reg:SI 11)  
                  (plus:SI (reg:SI 10)  
                              (mem:SI (symbol_ref:SI ("x")))))

看起來括號不少, 挺繁瑣的不易弄懂, 爲此咱們使用兩個方法來處理這個問題: 
   1. 使用 lisp 打印/排版慣例(方式), 將一個操做的多個操做數對齊排列. 
       上面是我已經排版好了的. (若是有工具, 這一步是能夠自動作的...) 
   2. 從裏到外簡寫, 以幫助理解. 和對 ast 的簡寫相似. 
       (這一步, 若是咱們肯花時間寫工具, 也是能夠自動作的...) 
       (可是本身完成簡寫過程, 主要是爲了理解, 因此本身作仍是有意義的)


下面咱們開始簡寫上面的 rtl 1:
(symbol_ref:SI ("y" 或 "x")) 表示對一個符號 y 或 x 的引用, 咱們直接簡寫爲 y 或 x.
(mem:SI y) 其中的 y 已是被簡寫的符號了, mem 表示取 y 這個符號所在地址
中的數據, SI 表示機器模式 Single Integer (SI), 即從 y 這個地址取一個 SI 格式
的數字. 因爲這裏都是 SI 模式, 簡寫時候徹底能夠略去. 設簡寫爲 ^y, 符號能夠本身選,
只要方便理解便可(我選擇這個像 pascal 指針寫法, 方便理解便可).

(const_int 7) 簡寫爲 7. 
(mult:SI ^y 7) 簡寫爲 (* ^y 7)
(reg:SI 10) 簡寫爲 r_10.  (編號爲第 10 的僞寄存器)
(set r_10 (* ^y 7)) 簡寫爲 (= r_10 (* ^y 7)), 這個樣子寫爲中序表達式就是: 
   r_10 = ^y * 7

寫成彙編形態就是: 
  mul  ^y, 7 => r_10     也即: 取符號 y 處的內存(MEM) SI 類型操做數, 乘上 當即操做數(IMM) 7, 
結果送入僞寄存器 r_10.

上面的 rtl 2 相似的簡寫爲: 
  (= r_11 (+ r_10 ^x))

寫成對應的彙編形態就是: 
  add r_10, ^x => r_11


這個形態距離最終的實際彙編代碼就更近了一步了.
實際我測試時最終該 rtl 生成的彙編代碼爲:  
   imull $7, y, %eax   ; imull: i有符號 long 乘法. $7 當即數 7, 符號y. 結果在 %eax  
   addl x, %eax         ; x + %eax ==> %eax, l 後綴表示 long, 即 SI, Single Integer   

從 rtl 再經歷多個遍(pass) 優化等各類處理(據我看某文檔, 說在 rtl 上有大約 70+ 種的各類 pass)
然而生成 asm 的過程, 屬於第3階段的轉換問題. 咱們本階段研究 ast->rtl 就忽略它了.  

相關文章
相關標籤/搜索