1. 程式人生 > >GCC後端指令生成分析(1)

GCC後端指令生成分析(1)

主要研究GCC指令生成階段的各個步驟,重點在編譯器程式碼與機器描述之間的介面函式和資料結構。以一小段程式碼片段為示例,逐步追溯直至彙編程式碼生成的整個過程。

引言

機器描述檔案是形如*.md的檔案,其核心部分是兩類定義:“define_insn”和“define_expand”。

md檔案中define_insn模版示意圖
圖1 md檔案中define_insn模版示意圖

define_insn主要分為以下幾個部分。

  1. 名字,對應圖1中的“addqi3_cc”。
  2. 樣式(pattern),對應於圖1中的“[(set (reg:CC… … match_dup 2)))]”。規定了RTL指令體中的各種操作以及運算元的位置和運算元必須滿足的條件。
  3. 條件,對應於圖1中的“ix86_binary_operator_ok (PLUS, QImode, operands)”,指出此樣板有效的前提條件。
  4. 輸出模板(output template),對應於圖1中的“add{b}\t{%2, %0 | %0, %2}”。確定與此樣式相匹配的RTL指令的彙編輸出形式。可分為三種:單彙編模板、多彙編模板和C程式碼。前兩種給出的就是機器的彙編指令,第三種不能直接給出指令,C程式碼被編譯器吸收並且動態產生彙編模板。
  5. 指令屬性,對應於圖1中的“[(set attr … … )])”。給出與該樣式所匹配的指令的屬性。在編譯器的編譯過程中,編譯器主體與機器描述檔案相互動,最終生成彙編程式碼,完成編譯器的使命。

下文中將逐步介紹x86架構下(i386.md檔案)後端執行的流程,主要分為三個部分:RTL指令的生成及彙編程式碼的生成;md檔案的分析;編譯器後端輔助工具的分析。

RTL及彙編程式碼的生成

相關檔案

GCC編譯器從原始碼轉換到彙編程式碼的過程,主要經歷了三個階段的中間表示:GIMPLE→RTL→ASM。GIMPLE是比較高層次的語法樹;RTL代表指令的rtx表示式;ASM是彙編碼。
前端呼叫後端的函式介面主要是expand_*系列函式集,這一系列函式的作用是完成從tree到rtl的轉換。這些函式按照功能分類在以下一些檔案中被實現:stmt.c, calls.c, expr.c, explow.c, expmed.c, function.c, optabs.c, integrate.c。

  1. stmt.c中的expand_*函式的作用是將前端的語句級別上的語法樹轉換為等義的rtl。因此stmt.c中的這些expand_*函式是被前端parser最先呼叫的。換句話說,這些函式是“GCC後端”的第1功能層(頂層)。它們會呼叫exp*.c的一些expand_*函式來真正完成對“表示式”的求值(也即表示式的tree→rtx,並返回rtx),以及呼叫emit-rtl.c中gen_rtx和gen_reg_rtx等功能函式。
  2. calls.c也是語句級別上的,它將函式呼叫語句的tree轉換為rtl。
  3. function.c將函式一級的tree轉換為rtl。
  4. expr.c, explow.c, expmed.c完成對錶達式的tree到rtx的轉換。這些檔案中的expand_*函式基本可以歸納為第2功能層。它這裡會將一步引用insn_emit.c檔案中的gen_*等功能函式以及optabs.c檔案中的expand_*函式。
  5. optabs.c檔案中的expand_*函式,作用是將基本的一元操作和二元操作轉換為rtx。它屬於 expand_*函式集中的第3功能層。它這裡接著往下就會引用與平臺相關的一些函式模組(從 md 自動生成的一些程式檔案,如 insn-output.c、insn-emit.c以及其它 insn-*系列檔案)。

常量的生成

rtx包含不同的型別,主要有常量、指令等。指令rtx的生成,需要從md檔案中讀取資訊來實現;而常量的生成不需要。常量的生成步驟的追溯過程如下所描述。

  1. 在 GCC 原始碼中,通過呼叫 gen_rtx_raw_CONST_INT 來產生一個 rtx 的常量。
  2. 在編譯時生成的檔案genrtl.h(編譯器輔助工具gengenrtl從rtl.def中生成)中,使用了圖2中的巨集定義。
  3. gen_rtx_fmt_w_stat又通過呼叫rtx_alloc_stat來分配一個rtx,截止目前為止與機器無關。而gengenrtl工具如何從rtl.def生成genrtl.h的問題,將在下一節討論。

這裡寫圖片描述
圖2 生成常量的相關巨集定義

指令的生成

接下來,研究一個簡單的加法指令如何生成。從函式expand_ binop開始追溯分析。md檔案中關於加法指令的一個描述入口如圖3所示。
這裡寫圖片描述
圖3 一個加法指令模板

GCC輔助工具genemit將從這條expand指令生成五個不同模式(mode)的gen_*形式的函式到insn-emit.c檔案中,包括gen_addqi3, gen_addhi3, gen_addsi3, gen_adddi3, gen_addti3。genemit逐一讀取md檔案中的每條記錄,根據記錄的型別屬於insn、expand或者split,分別呼叫對應的gen_insn、gen_expand或者gen_split來生成insn-emit.c檔案中的gen_xxx函式(例如gen_addsi3)。
考慮生成單精度整數加法的運算指令。按照md檔案中對應條目的描述,產生這一RTL指令的函式是gen_addsi3,這是一個在生成GCC時所產生的中間檔案中定義的一個函式。使用gdb除錯編譯器,把斷點設定在該函式上,此時函式的呼叫棧如圖4所示。
這裡寫圖片描述
圖4 執行到gen_addsi3時的呼叫棧

這裡寫圖片描述
圖5 expand_binop_directly函式體

因為我們重點關注表示insn的rtx的生成,所以從呼叫棧中#3條目,也就是expand_binop_directly函式開始分析。該函式開頭的一段程式碼如圖5所示。以函式體中的第2條語句為重點分析物件,該語句通過呼叫函式find_widening_optab_handler,來獲得一個insn_code型別的變數icode的值。這個icode將作為一個重點的索引值,引導最終rtx的生成,它的作用如下。

  1. 作為一個索引值,從陣列insn_data中,來獲取當前二元操作的兩個運算元的模式。
  2. 圖3呼叫棧中#2號函式maybe_gen_insn以icode和運算元的個數、儲存運算元的陣列為引數,生成代表當前操作的insn作為返回值,賦給變數pat。
  3. 通過emit_insn(pat)把pat發射出去。

因此,將以icode為線索,追溯insn的具體產生過程。
首先需要解決的是棧中#4號函式expand_binop中optab型別的引數binoptab是怎麼產生的,因為這個binoptab,在expand_binop_directly函式中呼叫find_widening_optab_handler函式時,用作引數,以生成返回的icode。
在insn-opinit.h檔案中,optab被宣告為enum optab_tag型別。這個型別依據optabs.def檔案中各種巨集的定義,取其名字,作為enum optab_tag中的元素。insn-opinit.h是生成編譯器時動態產生的檔案,原始碼中的genopinit.c檔案被編譯為genopinit程式,該程式讀取optabs.def檔案,生成insn-opinit.h。binoptab的值是利用語法樹的code作為引數,呼叫函式optab_for_tree_code (code, type, optab_default)生成的。而語法樹的code,則是在檔案tree.def中定義的,同樣在tree-core.h檔案中,與optab型別的定義相似,以巨集定義的形式宣告tree_code,並用作struct GTY(()) tree_base 中的一個域。整個過程的示意圖如圖6所示。

這裡寫圖片描述
圖6 binoptab生成過程示意圖

綜上所述,binoptab是在optabs-tree.c檔案中,綜合了從tree.def和c-common.def中定義的語法樹上的操作碼和optab.def中定義的操作,建立兩種操作之間的關係。以樹的操作碼為輸入,生成optabs.def中定義的對應的操作碼。
接下來研究如何從optabs.def中定義的操作碼,查詢icode,也就是find_widening_optab_handler的簡要執行流程。icode所對應的資料型別insn_code定義在檔案insn-codes.h中,該檔案是編譯GCC的時候生成的,位於$build(生成GCC時的編譯目錄)之下。在編譯GCC時,GCC原始碼中的gencodes.c檔案被編譯成可執行檔案gencodes,被放置於$build/gcc/build目錄下。gencodes再讀取架構特定的機器描述檔案(在這裡為i386.md),生成insn-codes.h,放置在$build/gcc目錄下。
gencodes產生中間檔案的步驟如下。

  1. 通過呼叫read_md_rtx函式來讀取md檔案中的條目。
  2. 對於滿足下列兩個條件的條目,取其名字name組成CODE_FOR_name,作為enum insn_code列舉型別中的一個項。(1) 條目為define_insn或者define_expand型的;(2) 名字不為空,並且名字不以”*”開頭的。

接下來的一個問題是,如何根據optab(本文中對應的是binoptab)的型別以及運算元的機器模式,查詢對應的icode。這一步是機器相關部分和機器無關部分的結合點。經過一系列的追溯,更深一層的查詢函式為定義在optabs-query.c中的widening_optab_handler (optab op, enum machine_mode to_mode, enum machine_mode from_mode)。三個引數分別為在optabs.def中定義的binop,以及兩個操作符的模式。該函式把這三個引數組合一個unsigned型別(4個位元組)的數scode,scode的4個位元組的組成為:前兩個位元組用於儲存binop,第三個位元組儲存from_mode,第四個位元組儲存to_mode。繼續追溯,最終是insn-opinit.c檔案中的lookup_handler函式實現了根據scode查詢icode的功能。該函式以其輸入引數scode為索引,在陣列pat上進行二分法查詢。陣列pat的資料型別是一個名為optab_pat的結構,該結構有兩個域,一個是型別為數值的scode,另一個就是型別為insn_code的icode。
struct optab_pat {
unsigned scode;
enum insn_code icode;
};
static const struct optab_pat pats[NUM_OPTAB_PATTERNS] = {
{ 0x010e0f, CODE_FOR_extendqihi2 },
{ 0x010e10, CODE_FOR_extendqisi2 },
{ 0x010e11, CODE_FOR_extendqidi2 },
{ 0x010f10, CODE_FOR_extendhisi2 },
{ 0x010f11, CODE_FOR_extendhidi2 },
{ 0x011011, CODE_FOR_extendsidi2 },
……
}
從上面的描述中,可以知道,有兩項重要的資料值得被研究。第一項是pats(如上述程式碼所示),也就是由元素scode和icode所組成的陣列。另一項是insn_data,可以用icode為索引在其中查詢運算元、運算元的模式等(圖5中所示)。pats被定義在生成GCC時編譯目錄下的檔案insn-opinit.c,是由程式genopinit讀取md檔案生成;而insn_data被定義在同樣目錄下的insn-output.c檔案中,是由genoutput程式讀取md檔案生成。
在獲得icode之後,用它和其他資料作為引數,呼叫maybe_gen_insn函式,可以生成insn指令。在函式maybe_gen_insn中,通過以下呼叫,來生成insn:
GEN_FCN(icode)(ops[0].value, ops[1].value);
而巨集GEN_FUN在檔案insn-opinit.h中,被定義為:
#define GEN_FUN(CODE)
(insn_data[code].genfun);
也就是說,最終是通過使用陣列insn_data中code所對應的元素的genfun成員函式來生成insn。在本文的加法例子下,對應的insn_data[code].genfun為gen_addsi3。而gen_addsi3則定義在insn-emit.c中,如前文所述,該檔案由程式genemit生成。
至此,insn的生成流程基本清楚了,如圖6所示,但是一些gen***型別的程式需要進一步分析。
這裡寫圖片描述
圖6 生成insn的概要流程圖

彙編程式碼的生成

彙編程式碼的生成比較簡單,主要在final.c檔案中處理。檔案中的final_scan_insn負責為一個insn生成彙編程式碼,最主要的操作分為以下兩步。

  1. 呼叫recog_memoized函式,識別insn,並獲取其程式碼編號insn_code_number。獲取insn_code_number的方式有兩種:一是從insn所儲存的資訊中獲取;另一種是呼叫recog函式來生成,INSN_CODE (insn) = recog (PATTERN (insn), insn, 0)。recog函式定義在insn-recog.c檔案中,與之前提到的insn-*.c形式的檔案類似,此檔案也是genrecog程式讀取機器描述檔案(md)生成的,具體內容在下一節中分析。
    2.根據insn_code_number和insn的值,呼叫get_insn_template,生成包含彙編指令的字串templ。templ = get_insn_template(insn_code_number, insn),這裡所要用到的相關資訊主要從insn_data中獲得。如前所述,insn_data資料定義在檔案insn-output.c中的生成,將在genoutput工具分析一節中詳細分析。