GNU C 編譯器內部/GNU C 編譯器架構 3 4
GCC 架構概述。表示式的編譯。
[編輯原始碼]本節基於一篇 Red Hat 雜誌文章 [1].
GNU 編譯器集合 (GCC) 包含針對不同程式語言的多個編譯器。主要的 GCC 可執行檔案 gcc 處理用 C、C++、Objective-C、Objective-C++、Java、Fortran 或 Ada 編寫的原始檔,併為每個原始檔生成一個彙編檔案。它是一個驅動程式,根據原始檔的語言呼叫相應的編譯程式。對於 C 原始檔,它們是預處理器和編譯器 cc1、彙編器 as 和連結器 collect2。第一個和第三個程式與 GCC 發行版一起提供;彙編器是 GNU binutils 包的一部分。本書描述了預處理器和編譯器 cc1 的內部機制。
每個編譯器包含三個元件:前端、中間端和後端。GCC 一次編譯一個檔案。原始檔依次經過所有三個元件。當它從一個元件到下一個元件時,它的表示形式會發生改變。 圖 1 說明了這些元件以及與每個元件關聯的原始檔表示形式。抽象語法樹 (AST)、暫存器傳輸語言 (RTL) 和目標檔案 是主要表示形式。

主要表示
[編輯原始碼]前端的目的是讀取原始檔、解析它並將其轉換為標準抽象語法樹 (AST) 表示形式。AST 是一種雙型別表示形式:它是一棵樹,其中節點可以有子節點,以及一個語句列表,其中節點一個接一個地連結起來。每種程式語言都有一個前端。
然後使用 AST 生成暫存器傳輸語言 (RTL) 樹。RTL 是一種基於硬體的表示形式,對應於具有無限數量暫存器的抽象目標體系結構。RTL 最佳化階段以 RTL 形式最佳化樹。最後,GCC 後端使用 RTL 表示形式為目標體系結構生成彙編程式碼。後端的示例包括 x86 後端和 MIPS 後端。
在接下來的部分中,我們將描述 C 前端和 x86 後端的內部機制。編譯器從其初始化和命令列選項處理開始。之後,C 前端預處理原始檔、解析它並執行一些最佳化。然後,後端為目標平臺生成彙編程式碼並將其儲存到檔案中。
輔助資料結構
[編輯原始碼]GCC 有許多輔助資料結構,它們簡化了程式碼開發,例如向量和堆。
在 vec.h 中定義的宏實現了一組模板化的向量型別和關聯介面。這些模板是用宏實現的,因為我們不在 C++ 環境中。介面函式是型別安全的,並使用靜態行內函數,有時由內聯的泛型函式支援。這些向量被設計為與 GTY 機制互動。
由於結構物件、標量物件和指標的不同行為,有三種類型,每種對應於這些變體之一。指標和結構物件變體都會傳遞物件的指標——在前一種情況下,指標被儲存到向量中,而在後一種情況下,指標被解引用,物件被複制到向量中。標量物件變體適用於 int 類物件,向量元素按值返回。
有“索引”和“迭代”訪問器。迭代器返回一個布林迭代條件,並按引用更新傳入的迭代變數。由於迭代器將被內聯,因此對地址的最佳化可以被消除。
這些向量使用尾部陣列慣例實現,因此它們不能在不更改向量物件本身地址的情況下調整大小。這意味著你不能使用向量型別的變數或欄位——始終使用指向向量的指標。唯一例外是結構的最後一個欄位,它可以是向量型別。你將不得不使用 embedded_size & embedded_init 呼叫來建立此類物件,並且它們可能無法調整大小(因此不要使用“安全”分配變體)。使用尾部陣列慣例(而不是指向資料陣列的指標)是因為,如果我們允許 NULL 也表示空向量,則空向量在包含它們的結構中佔用最少的空間。
每個增加活動元素數量的操作都有“快速”和“安全”變體。前者假定有足夠的空間用於操作成功(如果沒有,則會失敗)。後者將在需要時重新分配向量。重新分配會導致向量大小呈指數級增長。如果你知道將新增 N 個元素,則在使用“快速”操作新增元素之前使用 reserve 操作會更有效率。這將確保至少與你要求的數量一樣多的元素,如果剩餘插槽太少,它將呈指數級增長。如果你想預留特定數量的插槽,但又不想指數級增長(例如,你知道這是最後一次分配),則使用負數進行預留。你也可以從一開始就建立一個特定大小的向量。
你應該優先使用 push 和 pop 操作,因為它們在向量末尾追加和刪除。如果你需要一次刪除多個專案,請使用 truncate 操作。insert 和 remove 操作允許你在向量中間更改元素。有兩個 remove 操作,一個是保留元素順序的“ordered_remove”,另一個是不保留順序的“unordered_remove”。後者函式將結束元素複製到已刪除的插槽中,而不是呼叫 memmove 操作。“lower_bound”函式將確定在使用 insert 保持排序順序的情況下將專案放在陣列中的位置。
定義向量型別時,首先建立一個非記憶體管理版本。然後你可以定義垃圾收集和堆分配版本中的一個或兩個。分配機制是在定義型別時指定的,因此是型別的一部分。如果你需要垃圾收集和堆分配版本,你仍然必須精確地定義一個公共非記憶體管理基本向量。
如果你需要直接操作向量,則“address”訪問器將返回向量開始處的地址。“space”謂詞將告訴你在向量中是否有剩餘容量。你通常不需要使用這兩個函式。
向量型別使用 DEF_VEC_{O,P,I}(TYPEDEF) 宏定義,以獲得非記憶體分配版本,然後使用 DEF_VEC_ALLOC_{O,P,I}(TYPEDEF,ALLOC) 宏以獲得記憶體管理向量。向量型別的變數使用 VEC(TYPEDEF,ALLOC) 宏宣告。ALLOC 引數指定分配策略,可以是“gc”或“heap”,分別代表垃圾收集和堆分配。它可以是“none”,以獲得必須顯式分配的向量(例如,作為另一個結構的尾部陣列)。字元 O、P 和 I 表示 TYPEDEF 是指標 (P)、物件 (O) 還是整數 (I) 型別。注意選擇正確的型別,因為如果你使用錯誤的型別,你將得到一個笨拙且低效的 API。對於 P 和 I 版本,有一個檢查,會導致編譯時警告,但對於 O 版本沒有檢查,因為在純 C 中這是不可能的。由於 GTY 的工作方式,你必須用 GTY(()) 標籤註釋你希望從向量中插入或引用的任何結構。即使你從未宣告 GC 分配變體,你也需要這樣做。
一個使用它們的示例將是,
DEF_VEC_P(tree); // non-managed tree vector.
DEF_VEC_ALLOC_P(tree,gc); // gc'd vector of tree pointers. This must
// appear at file scope.
struct my_struct {
VEC(tree,gc) *v; // A (pointer to) a vector of tree pointers.
};
struct my_struct *s;
if (VEC_length(tree,s->v)) { we have some contents }
VEC_safe_push(tree,gc,s->v,decl); // append some decl onto the end
for (ix = 0; VEC_iterate(tree,s->v,ix,elt); ix++)
{ do something with elt }
附加表示
[編輯原始碼]GCC 3.4 有附加表示嗎?
| 帶回家: | GCC 是一個編譯器集合,它包含每個程式語言的前端、中間端和每個體系結構的後端。每個原始檔經過的主要表示形式是前端的 AST、中間端的 RTL 以及後端的彙編表示。GCC 一次編譯一個檔案。 |
GCC 初始化
[編輯原始碼]
C 前端包括 C/C++ 預處理器和 C 編譯器。程式 cc1 包含預處理器和 C 編譯器。它編譯 C 原始檔並生成彙編 (.S) 檔案。
編譯器前端和後端透過稱為語言鉤子的回撥函式相互互動。所有鉤子都包含在一個全域性變數結構 lang_hooks lang_hooks 中,該結構在檔案 langhooks.h 中定義。有以下型別的鉤子:樹內聯鉤子、呼叫圖鉤子、函式鉤子、樹轉儲鉤子、型別鉤子、宣告鉤子和特定於語言的鉤子。鉤子的預設值在檔案 langhooks-def.h 中定義。
GCC 初始化包括命令列選項解析、初始化後端、建立全域性範圍以及初始化內建資料型別和函式。
每個宣告都與一個範圍相關聯。例如,區域性變數與其函式的範圍相關聯。全域性宣告與全域性範圍相關聯。
檔案 toplev.c 包含主 cc1 函式 toplev_main() 和定義編譯器狀態的全域性變數。變數 current_function_decl 是正在編譯的函式的宣告,或者在函式之間時為 NULL。
函式 toplev_main() 是處理命令列選項、初始化編譯器、編譯檔案並釋放分配的資源的函式。函式 decode_options() 處理命令列選項並在編譯器中設定相應的變數。
在命令列選項解析函式 do_compile() 之後被呼叫。它透過呼叫函式 backend_init() 執行後端初始化。
後端初始化包括許多步驟。函式 init_emit_once() 為一些暫存器生成 RTL 表示式:pc_rtx 用於程式計數器、cc0 用於條件、stack_pointer_rtx、frame_pointer_rtx 等。它們儲存在陣列 global_rtl 中。
之後,函式 lang_dependent_init() 執行特定於語言的初始化,包括前端和後端的初始化。C 初始化函式 c_objc_common_init() 建立內建資料型別、初始化全域性範圍並執行其他初始化任務。函式 c_common_nodes_and_builtins() 建立檔案 builtin-types.def 中描述的預定義型別。
標準 C 型別在初始化時建立。下表列出了一些型別
| 變數名 | C 型別 |
| char_type_node | char |
| integer_type_node | int |
| unsigned_type_node | unsigned int |
| void_type_node | void |
| ptr_type_node | void* |
GCC 內建函式是在編譯時計算的函式。例如,如果 strcpy() 函式的大小引數是常量,則 GCC 會用所需數量的賦值替換函式呼叫。編譯器將標準庫呼叫替換為內建函式,並在建構函式的 AST 後計算它們。在 strcpy() 的情況下,編譯器會檢查大小引數,如果引數是常量,則使用 strcpy() 的最佳化內建版本。builtin_constant_p() 允許確定其引數的值在編譯時是否已知。GCC 內建函式的用途超出了 GCC。例如,Linux 核心的字串處理庫使用 builtin_constant_p() 來呼叫字串處理函式的最佳化版本,如果字串大小在編譯時已知。
GCC 使用相應的 expand_builtin() 函式計算每個內建函式。例如,builtin_strcmp() 使用 expand_builtin_strcmp() 計算。下表列出了一些 GCC 內建函式的示例
| 內建函式名 | 解釋 |
| builtin_constant_p | 如果引數是常量,則返回 true |
| builtin_memcpy | 等效於 memcpy() |
| builtin_strlen | 等效於 strlen() |
| 帶回家: | GCC 初始化包括命令列選項解析、初始化後端、建立全域性範圍以及初始化內建資料型別和函式。 |
解析器和預處理器
[編輯原始碼]在初始化之後,函式 do_compile() 呼叫函式 compile_file()。此函式呼叫 parse_file() 前端語言鉤子,該鉤子對於 C 語言設定為函式 c_common_parse_file()。後一個函式呼叫函式 finish_options(),該函式初始化預處理器並處理 -D、-U 和 -A 命令列選項(分別等效於 #define、#undef 和 #assert)。C 預處理器處理原始碼中的預處理器指令,例如 #define、#include。
解析器
[編輯原始碼]在預處理器初始化之後,呼叫 {{{4}}} 函式。此函式使用標準 lex/bison 工具解析檔案。
預處理器
[編輯原始碼]預處理器是作為庫實現的。C 語言詞法分析器函式 c_lex() 呼叫 libcpp 函式 cpp_get_token(),該函式處理預處理器關鍵字。預處理器的狀態由變數 cpp_reader *parse_in 定義。型別 struct cpp_reader 最重要的是包含正在處理的文字緩衝區的列表。每個緩衝區對應於一個原始檔 (.c 或 .h)。函式 cpp_get_token() 為合法的預處理器關鍵字呼叫相應的函式。例如,當遇到 #include 時,會呼叫函式 do_include_common()。它會分配一個新的緩衝區並將其放置在緩衝區堆疊的頂部,使其成為當前緩衝區。當編譯一個新檔案時,緩衝區將從堆疊中彈出,並且舊檔案的編譯將繼續。
當使用 #define 關鍵字定義新宏時,會呼叫函式 do_define()。
| 帶回家: | 預處理器處理預處理器指令,例如 #include 和 #ifdef。 |
從原始碼到 AST
[編輯原始碼]執行預處理器後,GCC 為原始檔的每個函式構建抽象語法樹 (AST)。AST 是多個連線的 struct tree 型別的節點。每個節點都有一個 樹程式碼,用於定義樹的型別。宏 TREE_CODE() 用於引用程式碼。樹程式碼在檔案 tree.def 和 c-common.def 中定義。具有不同樹程式碼的樹被分組到 樹類 中。以下樹類在 GCC 中定義,包括:
| 樹類 | 解釋 |
| 'd' | 宣告 |
| '<' | 比較 |
| '2' | 二元算術 |
有兩種型別的樹:語句 和 表示式。語句對應於 C 結構,例如表示式後跟一個 ';'、條件語句、迴圈語句等。表示式是語句的構建基礎。表示式的示例包括賦值表示式、算術表示式、函式呼叫等。樹程式碼的示例在下表中給出
| 樹程式碼 | 樹類 | 解釋 | 運算元 |
| MODIFY_EXPR | 'e' | 賦值表示式 | TREE_OPERAND(t,0) - 左側;TREE_OPERAND(t,1) - 右側; |
| CALL_EXPR | 'e' | 函式呼叫 | TREE_OPERAND(t,0) - 函式定義;TREE_OPERAND(t,1) - 函式引數; |
| FUNCTION_DECL | 'd' | 變數宣告 | DECL_SOURCE_FILE(t) - 原始檔;DECL_NAME(t) - 變數名; |
| ARRAY_TYPE | 't' | 陣列型別 | TREE_TYPE(t) - 陣列元素型別;TYPE_DOMAIN(t) - 索引型別; |
| DECL_STMT | 'e' | 變數宣告 | TREE_OPERAND(t,0) - 變數;DECL_INITIAL(TREE_OPERAND(t,1)) - 初始值; |
除了定義樹型別的樹程式碼外,還有許多針對每種樹型別不同的運算元可用。例如,賦值表示式有兩個運算元,對應於表示式的左側和右側。宏 TREE_OPERAND 用於引用運算元。宏 IDENTIFIER_POINTER 用於查詢 IDENTIFIER_NODE 樹表示的識別符號的名稱。下表列出了一些樹節點、它們的用途及其運算元。
每棵樹都有一個型別,對應於它表示的 C 表示式的型別。例如,MODIFY_EXPR 節點的型別是左側運算元的型別。NOP_EXPR 和 CONVERT_EXPR 樹用於型別轉換。
樹 NULL_TREE 等效於 NULL。函式 debug_tree() 將樹列印到 stderr。
當一個新的識別符號被詞法分析時,它會被新增到 GCC 維持的字串池中。識別符號的樹程式碼是 IDENTIFIER_NODE。當再次詞法分析相同的識別符號時,會返回相同的樹節點。函式 get_identifier() 返回識別符號的樹節點。

新的變數宣告將在多個函式呼叫中處理。首先,呼叫函式 start_decl(),並傳入宣告的名稱、詞法分析器返回的型別以及它的屬性。該函式呼叫 grokdeclarator(),該函式會檢查型別和引數節點,並返回具有適合宣告的程式碼的樹:對於變數,為 VAR_DECL,對於型別,為 TYPE_DECL 等。然後將宣告新增到 scope 中。一個範圍包含在函式內建立的所有宣告,但不包含全域性宣告。還有一個全域性範圍包含全域性宣告。當解析函式時,其宣告作為 BLOCK 節點附加到其主體。當建立宣告時,使用 IDENTIFIER_SYMBOL_VALUE 將識別符號節點與宣告節點相關聯。函式 lookup_name() 返回給定識別符號的宣告。當宣告離開範圍時,會斷言樹屬性 C_DECL_INVISIBLE。
GCC 中沒有維護符號表。相反,編譯器使用識別符號池和C_DECL_INVISIBLE 屬性。語言鉤子lang_hooks.decls.getdecls() 返回範圍內的變數,這些變數連結在一起。
對於已初始化的宣告,會呼叫函式start_init() 和finish_init()。函式finish_decl() 完成宣告。對於陣列宣告,它計算已初始化陣列的大小。然後呼叫函式layout_decl()。它計算宣告的大小和對齊方式。
解析函式取決於它是否存在主體。函式宣告使用與用於變數宣告相同的函式進行解析。對於函式定義,會呼叫函式start_function()。然後編譯器解析函式的主體。當函式結束時,會呼叫函式finish_function()。
函式start_decl() 和start_function() 將宣告的attributes 引數作為其引數之一。屬性在 GCC 手冊中有所描述。屬性是 GNU C 實現的擴充套件。下表介紹了其中一些屬性並解釋了其用途。
| 屬性 | 解釋 |
| constructor | 在 main() 之前自動呼叫函式 |
| destructor | 在 main() 之後自動呼叫函式 |
| alias | 另一個函式的別名 |
對於每種型別的 C 語句,都有一個函式構建相應型別的樹節點。例如,函式build_function_call() 為函式呼叫構建CALL_EXPR 節點。它以函式名的識別符號和引數作為引數。該函式使用lookup_name() 查詢函式宣告,並使用default_conversion() 對引數進行型別轉換。
解析函式後,使用宏DECL_SAVED_TREE 訪問其主體。它使用BIND_EXPR 樹表示,該樹將區域性變數繫結到語句。BIND_EXPR_VARS 給出了已宣告變數的鏈。BIND_EXPR_BODY 返回STATEMENT_LIST 型別的樹。
以下 API 允許遍歷語句列表並對其進行操作。
| 函式 | 用途 |
| tsi_start(stmt_list) | 獲取指向列表頭的迭代器 |
| tsi_last(stmt_list) | 獲取指向列表尾部的迭代器 |
| tsi_end_p(iter) | 是列表的末尾嗎? |
| tsi_stmt(iter) | 獲取當前元素 |
| tsi_split_statement_list_before(&iter) | 在 iter 處拆分元素 |
| tsi_link_after(&iter, stmt, mode) | 在 iter 之後連結鏈 |
| tsi_next(&iter) | 列表的下一個元素 |
| append_to_statement_list(tree, &stmt_list) | 將樹追加到 stmt_list |
可以在此級別上對函式序言/結語進行檢測,如gimplify_function_tree() 中所示。要向函式結語新增語句,請使用TRY_FINALLY_EXPR 樹。它的第一個運算元是舊語句,第二個引數是結語語句。這種型別的樹指示後續傳遞在建立函式的通用退出基本塊時執行語句。
要檢測函式序言,請將樹與所需的語句預先連線。因此,BIND_EXPR_BODY 將具有序言和TRY_FINALLY_EXPR 樹。
然後將 AST 轉換為 SSA,最終轉換為 RTL 表示。轉換是在解析每個函式後還是在解析整個檔案後發生,由編譯器選項-funit-at-a-time 控制。預設情況下它為假。
| 帶回家: | GCC 解析器構建原始檔的 AST 表示。AST 由樹節點組成。每個節點都有一個程式碼。樹節點對應於 C 的語句和表示式。函式 debug_tree() 打印出樹。 |
從 AST 到 GIMPLE
[edit source]最終從 finish_function() 呼叫 gimplify_function_tree() 時,AST 會被簡化。
GIMPLE 表示基於 SIMPLE,如 [2] 中所述。
根據本文,目標是將樹表示為基本語句。
| x=a binop b | x=a | x=cast b | f(args) |
| *p=a binop b | *p=a | *p=cast b | - |
| x=unop a | x=*q | x=&y | x=f(args) |
| *p=unop a | *p=*q | *p=&y | *p=f(args) |
臨時變數在必要時在函式 create_tmp_var() 和 declare_tmp_vars() 中建立。
在此階段,GCC 對複雜條件表示式進行最佳化,即
if (a || b) stmt;
被轉換為
if (a) goto L1; if (b) goto L1; else goto L2; L1: stmt; L2:
此外,條件表示式的每個分支都包裝在 STATEMENT_LIST 樹中。
從 GIMPLE 到 RTL
[edit source]暫存器傳輸語言 表示一臺具有無限數量暫存器的抽象機器。型別rtx 描述一條指令。每個 RTL 表示式都有一個程式碼和機器模式。
與 AST 類似,程式碼被分組到多個類中。它們在 mode-classes.def 中定義。
| 類別 | 解釋 |
| RTX_CONST_OBJ | 表示一個常量物件(例如,CONST_INT) |
| RTX_OBJ | 表示一個物件(例如,REG、MEM) |
| RTX_COMPARE | 比較(例如,LT、GT) |
| RTX_COMM_COMPARE | 可交換比較(例如,EQ、NE、ORDERED) |
| RTX_UNARY | 一元算術表示式(例如,NEG、NOT) |
| RTX_COMM_ARITH | 可交換二元運算(例如,PLUS、MULT) |
| RTX_TERNARY | 非位域三輸入運算(IF_THEN_ELSE) |
| RTX_BIN_ARITH | 非可交換二元運算(例如,MINUS、DIV) |
| RTX_BITFIELD_OPS | 位域運算(ZERO_EXTRACT、SIGN_EXTRACT) |
| RTX_INSN | 機器指令(INSN、JUMP_INSN、CALL_INSN) |
| RTX_MATCH | 指令中匹配的東西(例如,MATCH_DUP) |
| RTX_AUTOINC | 自動遞增定址模式(例如,POST_DEC) |
| RTX_EXTRA | 所有其他 |
檔案中列出的機器模式 machmode.def 指定機器級別資料的尺寸和格式。在語法樹級別,每個..._TYPE 和每個..._DECL 節點都有一個機器模式,該模式描述該型別的資料或宣告的變數的資料。
編譯函式時會構建一個指令列表。函式 emit_insn() 將指令新增到列表中。變數宣告 AST 已經生成了其 RTL。使用 DECL_RTL 訪問它。函式 emit_cmp_and_jump_insns() 輸出條件語句。emit_label() 打印出標籤。這些函式將指令一個接一個地連結起來。宏 PREV_INSN 和 NEXT_INSN 用於遍歷列表。
可以使用 first_insn 和 last_insn 訪問第一個和最後一個指令。get_insns() 提供當前序列或當前函式的第一個指令。
使用 debug_rtx() 在螢幕上列印 RTL 指令,使用函式 print_rtl() 列印 RTL 表示式列表。
許多函式建立節點。例如,gen_label_rtx() 構建標籤。最通用的函式位於特定於目標的目錄中。例如,x86 架構 rtl 生成檔案 genrtl.c 和 genrtl.h 位於 ./host-i686-pc-linux-gnu 中。
從 RTL 到物件
[edit source]每個目標架構都有其描述,表示為結構體 gcc_target targetm。檔案 targhooks.c 中提供了預設的初始值設定項。
後端為指定的目標平臺生成彙編程式碼。函式output_asm_insn() 用於寫入彙編檔案的每條指令。函式final_start_function() 在函式儲存到彙編檔案之前生成函式的序言。
降低傳遞
[edit source]函式的處理包括其降低,此時會對它應用許多最佳化傳遞,如函式 tree_lowering_passes() 中所示。降低函式的結果是生成其控制流圖。隨後對函式 cgraph_create_edges() 的呼叫使用基本塊資訊來擴充套件呼叫圖的邊,這些邊包含當前函式執行的呼叫。對尚未定義的函式的引用儲存在函式 record_references() 中。
| 名稱 | 含義 |
| remove_useless_stmts | N/A |
| mudflap_1 | 透過樹重寫進行窄指標邊界檢查 |
| lower_cf | 將 GIMPLE 降低為非結構化形式 |
| pass_lower_eh | 樹的異常處理語義和分解 |
| pass_build_cfg | 建立基本塊 |
| pass_lower_complex_O0 | 在不進行最佳化的情況下降低複雜運算 |
| pass_lower_vector | 將向量運算降低為標量運算 |
| pass_warn_function_return | 發出返回警告 |
| pass_early_tree_profile | 為基於樹的分析設定鉤子 |
switch 語句
[edit source]讓我們考慮一下 switch 語句如何從原始碼轉換為 GIMPLE 再轉換為 RTL。
在原始碼中遇到語句時,會呼叫函式 c_parser_switch_statement()。典型的 switch 語句包含多個 case 語句,這些語句可能包含 break。因此,解析器具有 c_break_label 樹,用於標記 switch 結束的位置。該函式解析語句的主體,如果發現至少一個 break,則為 break 標籤生成 LABEL_EXPR 樹。函式 c_finish_case() 將主體作為運算元之一附加到 SWITCH_EXPR 樹。此外,該樹還有另外兩個運算元:switch 條件和 switch 標籤。可以使用宏 SWITCH_COND()、SWITCH_BODY() 和 SWITCH_LABELS() 訪問運算元。標籤在解析時不會被填充。
switch 語句在函式 gimplify_switch_expr() 中被簡化。其思想是將主體與決策部分分離,並生成 switch 標籤,以便在驗證條件後可以將執行重定向到相應的 case。我們將考慮存在預設標籤的情況。此函式有兩個指向語句列表的指標:pre_p(這是副作用列表)和 expr_p(這是語句本身)。
switch 語句的主體在 `gimplify_to_stmt_list()` 函式中被簡化。case 標籤被儲存在變數 `gimplify_ctx` 結構體 `gimplify_ctxp` 的 `case_labels` 欄位中。然後,函式建立一個 `TREE_VEC` 來儲存標籤,並使用相應的 case 標籤初始化它們。該 `TREE_VEC` 被分配給 switch 語句的 `SWITCH_LABELS` 運算元,然後被追加到 `pre_p` 列表中。然後,使用 `expr_p` 指標將原始語句覆蓋為 `SWITCH_BODY`。最後,刪除 `switch` 語句在副作用列表中的 `SWITCH_BODY` 運算元,使其僅包含標籤。
從這裡開始,很明顯編譯器試圖使用一個跳轉表來表示原始語句,該跳轉表將每個可能的索引值對映到相應 case 的地址。函式 `expand_case()` 實現這個想法。它生成一個 `table_label`,用於生成每個可能索引值的跳轉指令。然後呼叫函式 `try_tablejump()`,它將索引樹展開為索引 rtl 並呼叫 `do_tablejump()`。該函式生成一個絕對索引 rtl,它組合了基地址 `table_label` 和索引偏移量。它隨後發出跳轉指令到跳轉表的適當條目。執行在函式 `expand_case()` 中繼續。跳轉表使用 `SWITCH_LABELS` 生成。
labelvec[i] = gen_rtx_LABEL_REF (Pmode, label_rtx (n->code_label));
最後發出了一些跳轉指令。
if (CASE_VECTOR_PC_RELATIVE || flag_pic)
emit_jump_insn (gen_rtx_ADDR_DIFF_VEC (CASE_VECTOR_MODE,
gen_rtx_LABEL_REF (Pmode, table_label),
gen_rtvec_v (ncases, labelvec),
const0_rtx, const0_rtx));
| 帶回家: | 後端為指定的目標平臺生成彙編程式碼。 |
- ^ https://web.archive.org/web/20160410185222/https://#/magazine/002dec04/features/gcc/
- ^ L. Hendren、C. Donawa、M. Emami、G. Gao、Justiani 和 B. Sridharan。基於結構化中間表示家族的 McCAT 編譯器設計。在第五屆平行計算語言和編譯器研討會論文集中,1992 年。