跳轉到內容

GNU C 編譯器內部/GNU C 編譯器架構 4 1

來自 Wikibooks,開放世界中的開放書籍

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)目標 是主要表示形式。

GCC front end, middle end, and back end with source file representations.
GCC 前端、中間端和後端,以及原始檔表示形式。

主要表示

[編輯原始碼]

前端的目的是讀取原始檔、解析它並將其轉換為標準抽象語法樹 (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 在陣列中放置專案的哪個位置,以保持排序順序。

當定義向量型別時,首先建立一個非記憶體管理版本。然後你可以定義垃圾回收和堆分配版本中的一個或兩個。分配機制在定義型別時指定,因此它是型別的一部分。如果你需要垃圾回收和堆分配版本,你仍然必須精確地定義一個公共非記憶體管理基本向量。

如果你需要直接操作向量,那麼“地址”訪問器將返回向量開頭的地址。“空間”謂詞將告訴你向量中是否有剩餘容量。你通常不需要使用這兩個函式。

向量型別使用 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 }


其他表示

[edit source]
Additional representations of GCC 4.1.
GCC 4.1 的其他表示。

圖 2 顯示了 GCC 4.1 的其他表示。

由於語言的差異,生成的 AST 的格式對於每種語言略有不同。AST 生成後的下一步是統一步驟,其中 AST 樹被轉換為稱為通用形式的統一形式。之後,編譯器的中間端部分接管。首先,樹被轉換為另一種稱為 GIMPLE 的表示形式。在這種形式中,每個表示式最多包含三個運算元,所有控制流結構都被表示為條件語句和 goto 運算子的組合,函式呼叫的引數只能是變數,等等。 圖 2 說明了通用形式的樹和 GIMPLE 形式的樹之間的區別。GIMPLE 是一個方便的表示,用於最佳化原始碼。


在 GIMPLE 之後,原始碼被轉換為 靜態單賦值 (SSA) 表示形式。這種形式的核心思想是每個變數只賦值一次,但可以在表示式的右側被多次使用。每次在 GIMPLE 形式的樹中重新分配同一個變數時,編譯器都會建立一個該變數的新版本,並將新值儲存到其中。當同一個變數在條件表示式的兩個分支中都被賦值時,需要將變數的兩個可能值合併成一個變數。此操作在 SSA 形式中表示為 PHI 函式。


SSA 形式也用於最佳化。GCC 對 SSA 樹執行 20 多種不同的最佳化。在 SSA 最佳化階段之後,樹被轉換回 GIMPLE 形式。

要點: GCC 是一個編譯器集合,它包含每個程式語言的前端、中間端和每個架構的後端。每個原始檔經過的主要表示形式是前端的 AST、中間端的 RTL 和後端的彙編表示形式。GCC 一次編譯一個檔案。

GCC 初始化

[edit source]
GCC initialization.
GCC 初始化。

C 前端包括 C/C++ 預處理器和 C 編譯器。程式 cc1 包括預處理器和 C 編譯器。它編譯 C 原始檔並生成彙編 (.S) 檔案。

編譯器前端和後端使用稱為語言鉤子的回撥函式相互互動。所有鉤子都被包含在一個全域性變數 struct 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 型別在初始化時建立。下表介紹了多種型別

GCC 內建型別
變數名稱 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 內建函式的示例

GCC 內建函式
內建名稱 說明
builtin_constant_p 如果引數是常量,則返回 true
builtin_memcpy 等效於 memcpy()
builtin_strlen 等效於 strlen()


要點: GCC 初始化包括命令列選項解析、初始化後端、建立全域性範圍以及初始化內建資料型別和函式。

解析器和預處理器

[edit source]

在初始化之後,函式 do_compile() 呼叫函式 compile_file()。此函式呼叫 parse_file() 前端語言鉤子,該鉤子對於 C 語言設定為函式 c_common_parse_file()。後一個函式呼叫函式 finish_options(),它初始化預處理器並處理 -D-U-A 命令列選項(分別等效於 #define、#undef#assert)。C 預處理器處理原始碼中的預處理器指令,例如 #define#include

解析器

[edit source]

解析器在檔案 c_parser.c 中手動實現。與 GCC 的早期版本相比,新的解析器生成更低階的 AST。例如,迴圈存在特殊的樹程式碼,FOR_STMT 表示 for() 迴圈。在這個版本中,迴圈被表示為條件語句和 goto,即程式碼 COND_EXPRLABEL_EXPRGOTO_EXPR 的樹。這可能意味著無法將 AST 表示提升回原始原始碼。

預處理器

[edit source]

預處理器實現為庫。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

[edit source]

執行預處理器後,GCC 為原始檔的每個函式構建一個抽象語法樹 (AST)。AST 是多個連線在一起的型別為 struct tree 的節點。每個節點都有一個 樹程式碼,該程式碼定義樹的型別。宏 TREE_CODE() 用於引用程式碼。樹程式碼在檔案 tree.defc-common.def 中定義。具有不同樹程式碼的樹被分組到 樹類 中。GCC 中定義了以下樹類(以及其他樹類):

GCC 樹類
樹類 說明
'd' 宣告
'<' 比較
'2' 二元算術運算


有兩種型別的樹:語句表示式。語句對應於 C 結構,例如表示式後跟 ';'、條件語句、迴圈語句等等。表示式是語句的構建基礎。表示式的例子是賦值表示式、算術表示式、函式呼叫等等。樹程式碼的例子在下表中給出

GCC 樹程式碼
樹程式碼 樹類 說明 運算元
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_EXPRCONVERT_EXPR樹用於型別轉換。

NULL_TREE等同於NULL。函式debug_tree()將樹列印到stderr

當解析到一個新識別符號時,它會被新增到 GCC維護的字串池中。識別符號的樹程式碼是IDENTIFIER_NODE。當再次解析到相同的識別符號時,會返回相同的樹節點。函式get_identifier()返回識別符號的樹節點。

Parsing variable declaration.
解析變數宣告。

新的變數宣告是在一系列函式呼叫中處理的。首先,函式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 允許遍歷語句列表並對其進行操作

樹構造 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。

GIMPLE 表示基於 [2] 中描述的 SIMPLE。

根據這篇論文,目標是將樹表示為基本語句。

GIMPLE 樹
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 中定義。

RTL 表示式的類
說明
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]

每個目標架構都有自己的描述,用 struct gcc_target targetm 表示。預設初始化器位於檔案 targhooks.c 中。

後端會為指定的目標平臺生成彙編程式碼。函式output_asm_insn()會為寫入彙編檔案的每條指令呼叫。函式final_start_function()會在函式儲存到彙編檔案之前生成函式的序言。

降低傳遞

[編輯原始碼]

函式的處理包括它的降低,當一系列最佳化階段在函式 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 語句

[編輯原始碼]

讓我們考慮如何將 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() 中被 gimplify 化。其思想是將主體與決策部分分離,並生成 switch 標籤,以便在驗證條件後可以將執行重定向到相應的 case。我們將考慮存在預設標籤的情況。此函式有兩個指向語句列表的指標:pre_p,它代表副作用列表,以及 expr_p,它代表語句本身。

switch 的主體在 gimplify_to_stmt_list() 中被 gimplify 化。case 標籤儲存在變數 struct 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));


要點: 後端為指定的目標平臺生成彙編程式碼。
上一頁: 目錄 GNU C 編譯器架構 下一頁: 堆疊保護
  1. ^ https://web.archive.org/web/20160410185222/https://#/magazine/002dec04/features/gcc/
  2. ^ L. Hendren、C. Donawa、M. Emami、G. Gao、Justiani 和 B. Sridharan。基於結構化中間表示族的 McCAT 編譯器設計。在第五屆平行計算語言和編譯器研討會論文集中,1992 年。
華夏公益教科書