1. 程式人生 > >轉載:GCC原始碼分析(五)——指令生成

轉載:GCC原始碼分析(五)——指令生成

一、前言

  又有好久沒寫了,的確很忙。前篇介紹了GCC的pass格局,它是GCC中間語言部分的核心架構,也是貫穿整個編譯流程的核心。在完成優化處理之後,GCC必須做的最後一步就是生成最後的編譯結果,通常情況下就是彙編檔案(文字或者二進位制並不重要)。

  前面也講到了,GCC中間語言的核心資料結構是GENERIC、GIMPLE和RTL。其中的RTL就是和指令緊密相關的一種結構,它是指令生成的起點。

二、RTL和INSN

2.1 什麼是RTL,什麼是INSN

  RTL叫做暫存器轉移語言(Register Transfering Language)。說是暫存器,其實也包含記憶體操作。RTL被設計成一種函式式語言,由表示式和物件構成。其中物件指的是暫存器、記憶體和值(常數或者表示式的值),表示式就是對物件和子表示式的操作。這些在gcc internal裡面都有介紹。

  RTL物件和操作組成RTL表示式,子表示式加上操作組成複合RTL表示式。當一個RTL表示式表示一條中間語言指令時,這個RTL表示式叫做INSN。RTL表示式(RTL Expression)在gcc程式碼中縮寫為RTX,程式碼中的rtx型別就是指向RTL表示式的指標。所以insn就是rtx,但是rtx不一定是insn。

2.2 INSN的生成

  RTL是由gimple生成的,從gimple到RTL的轉換叫做“expand”。在整個優化的pass鏈中,這一步由pass_expand完成。該pass實現在gcc/cfgexpand.c中。它的execute函式gimple_expand_cfg很長,但是核心工作是對每個basic block進行轉換:

  1. FOR_BB_BETWEEN (bb, init_block->next_bb, EXIT_BLOCK_PTR, next_bb)

  2. bb = expand_gimple_basic_block (bb);

expand_gimple_basic_block會呼叫expan_gimple_stmt來展開每一個gimple語句,並將展開後的rtx連線在一起。首先就有一個問題:insn是怎麼生成的?

  此外,每個expand_xxx函式只負責一部分工作,有些函式有rtx型別的返回值,有些函式沒有返回值。那些有返回值的函式通常也不會有變數來儲存它們返回的insn。那麼就有另外一個問題:那些展開的insn到哪裡去了?

  為了弄清楚這兩個問題,首先要找到生成insn的地方。這是一項工程浩大的體力活,不妨從某個點來研究這個問題,比如就從函式呼叫的語句來入手吧。我們可以從expand_gimple_basic_block開始順藤摸瓜,來看看一個GIMPLE_CALL是如何翻譯成insn的。

  首先,expand_gimple_basic_block裡有一個對basic block裡的gimple statement的遍歷迴圈,在這個迴圈裡面,首先判斷了一些特殊的情況,比如debug之類的,忽略之。直到迴圈最後一部分才進入正題:

  1. if (is_gimple_call (stmt) && gimple_call_tail_p (stmt)) // 尾呼叫,特殊情況,忽略之

  2. {

  3. bool can_fallthru;

  4. new_bb = expand_gimple_tailcall (bb, stmt, &can_fallthru);

  5. if (new_bb)

  6. {

  7. if (can_fallthru)

  8. bb = new_bb;

  9. else

  10. return new_bb;

  11. }

  12. }

  13. else

  14. {

  15. def_operand_p def_p;

  16. def_p = SINGLE_SSA_DEF_OPERAND (stmt, SSA_OP_DEF);

  17. if (def_p != NULL)

  18. {

  19. /* Ignore this stmt if it is in the list of

  20. replaceable expressions. */

  21. if (SA.values

  22. && bitmap_bit_p (SA.values,

  23. SSA_NAME_VERSION (DEF_FROM_PTR (def_p))))

  24. continue;

  25. }

  26. last = expand_gimple_stmt (stmt); //這是真正幹活的地方

  27. maybe_dump_rtl_for_gimple_stmt (stmt, last);

  28. }

  進入到expand_gimple_stmt裡面,這個函式不長,一眼可以看出來,核心是expand_gimple_stmt_1 (stmt);,這個函式分情況展開了stmt。其中GIMPLE_CALL對應的是expand_call_stmt。這個函式也不長,關鍵在最後。

  1. if (lhs)

  2. expand_assignment (lhs, exp, false); // lhs = func(args)

  3. else

  4. expand_expr_real_1 (exp, const0_rtx, VOIDmode, EXPAND_NORMAL, NULL); // func(args)

  gimple call語句形如 lhs = func ( args ); 。其中,lhs是可以沒有的。所以如果存在lhs的話,就按賦值語句展開。否則的話就按表示式展開。賦值語句的右邊也是表示式,因此按賦值語句展開最終也會將“func(args)”部分按表示式展開。

  expand_gimple_expr_1函式很長,因為要處理的表示式型別比較多。其中我們關注的是case CALL_EXPR:分支:

  1. case CALL_EXPR:

  2. /* All valid uses of __builtin_va_arg_pack () are removed during

  3. inlining. */

  4. if (CALL_EXPR_VA_ARG_PACK (exp))

  5. error ("%Kinvalid use of %<__builtin_va_arg_pack ()%>", exp);

  6. {

  7. tree fndecl = get_callee_fndecl (exp), attr;

  8. if (fndecl

  9. && (attr = lookup_attribute ("error",

  10. DECL_ATTRIBUTES (fndecl))) != NULL)

  11. error ("%Kcall to %qs declared with attribute error: %s",

  12. exp, identifier_to_locale (lang_hooks.decl_printable_name (fndecl, 1)),

  13. TREE_STRING_POINTER (TREE_VALUE (TREE_VALUE (attr))));

  14. if (fndecl

  15. && (attr = lookup_attribute ("warning",

  16. DECL_ATTRIBUTES (fndecl))) != NULL)

  17. warning_at (tree_nonartificial_location (exp),

  18. 0, "%Kcall to %qs declared with attribute warning: %s",

  19. exp, identifier_to_locale (lang_hooks.decl_printable_name (fndecl, 1)),

  20. TREE_STRING_POINTER (TREE_VALUE (TREE_VALUE (attr))));

  21. /* Check for a built-in function. */

  22. if (fndecl && DECL_BUILT_IN (fndecl))

  23. {

  24. gcc_assert (DECL_BUILT_IN_CLASS (fndecl) != BUILT_IN_FRONTEND);

  25. return expand_builtin (exp, target, subtarget, tmode, ignore); // 內建函式

  26. }

  27. }

  28. return expand_call (exp, target, ignore); // 普通函式

  內建函式有內建函式的展開方法,這個以後有機會再講。這裡還是分析一下普通函式。前面的那個if 是用來檢查的,展開是由expand_call函式來完成。這個函式相當長,因為函式的引數、堆疊等等事務很繁瑣。但是至少可以確定的是,一句普通的函式呼叫絕對不是一個簡單的insn能實現的,它應該對應了一串insn,而且至少包括壓棧、呼叫、退棧這三部分。那麼這一串insn在哪裡?

  為了弄清楚這一串insn在程式碼中的哪個地方,就必須提到start_sequence ()、get_insns()、end_sequence()這三個沒有引數的函式。第一個函式開啟了一個新的insn sequence,第二個函式獲取這個sequence的第一個insn,因為sequence是雙鏈表,所以由第一個insn就可以訪問到後面的所有insn。最後一個函式關閉這個sequence,之後就不能再通過emit_xxx往這個sequence裡面插入insn了。原因現在還說不清楚,因為這個跟第二個問題相關,就是insn去哪裡了?

  那麼insn到哪裡去了?在expand_call這個函式最後就有答案:

  1. /* If tail call production succeeded, we need to remove REG_EQUIV notes on

  2. arguments too, as argument area is now clobbered by the call. */

  3. if (tail_call_insns)

  4. {

  5. emit_insn (tail_call_insns); // 尾呼叫的rtx

  6. crtl->tail_call_emit = true;

  7. }

  8. else

  9. emit_insn (normal_call_insns); // 正常呼叫的rtx

  10. currently_expanding_call--;

  11. if (stack_usage_map_buf)

  12. free (stack_usage_map_buf);

  13. return target;

  所謂尾呼叫就相當於 return tail_call(...);。這個是有專門優化的。但不管怎麼優化,最後的insn被髮射(emit)了:

  1. rtx

  2. emit_insn (rtx x)

  3. {

  4. rtx last = last_insn;

  5. rtx insn;

  6. if (x == NULL_RTX)

  7. return last;

  8. switch (GET_CODE (x))

  9. {

  10. // 忽略那些特殊的case

  11. default:

  12. last = make_insn_raw (x);

  13. add_insn (last); // 這裡

  14. break;

  15. }

  16. return last;

  17. }

  18. void

  19. add_insn (rtx insn) // 一個標準的雙鏈表插入演算法

  20. {

  21. PREV_INSN (insn) = last_insn;

  22. NEXT_INSN (insn) = 0;

  23. if (NULL != last_insn)

  24. NEXT_INSN (last_insn) = insn;

  25. if (NULL == first_insn)

  26. first_insn = insn;

  27. last_insn = insn;

  28. }

其中first_insn和last_insn是巨集定義:

  1. #define first_insn (crtl->emit.x_first_insn)

  2. #define last_insn (crtl->emit.x_last_insn)

  3. /* Datastructures maintained for currently processed function in RTL form. */

  4. struct rtl_data x_rtl;

  5. // 在function.h中定義的巨集

  6. #define crtl (&x_rtl)

  原來,生成的insns被插入了當前函式的insn連結串列中。這個連結串列包含了當前函式的所有insn,而且是按儲存順序存放的。如果有跳轉的話,會有對應的jump insn和label insn。如果把insn就看作是彙編的話,這個連結串列其實就是“彙編”序列了。

  ok,回到前面提到的start_sequence/get_insns/end_sequence這一組函式。由於emit_xxx函式都是向first_insn/last_insn插入,而新的sequence也要藉助於emit_xxx來插入,也就是說在start_sequence和end_sequence這兩個呼叫中間,所有的emit_xxx必須向這個sequence發射insn。方法只有一個:那就是讓first_insn/last_insn指向當前正在構建的sequence,當這個sequence構建完成之後,再把它還原。(相當笨拙而無奈的設計,因為emit_xxx數量眾多,不容得罪)

  至此,insn去哪裡的問題解決了,但是第一個問題還在:insn如何被構建出來的?繼續順藤摸瓜。在expand_call函式中,有一句特別顯眼:

  1. /* Generate the actual call instruction. */

  2. emit_call_1 (funexp, exp, fndecl, funtype, unadjusted_args_size,

  3. adjusted_args_size.constant, struct_value_size,

  4. next_arg_reg, valreg, old_inhibit_defer_pop, call_fusage,

  5. flags, & args_so_far);

  看不懂程式碼,看註釋也明白了,這不就是生成一個call insn嗎?進入看看:

  1. #if defined (HAVE_call) && defined (HAVE_call_value)

  2. if (HAVE_call && HAVE_call_value)

  3. {

  4. if (valreg)

  5. emit_call_insn (GEN_CALL_VALUE (valreg,

  6. gen_rtx_MEM (FUNCTION_MODE, funexp),

  7. rounded_stack_size_rtx, next_arg_reg,

  8. NULL_RTX));

  9. else

  10. emit_call_insn (GEN_CALL (gen_rtx_MEM (FUNCTION_MODE, funexp),

  11. rounded_stack_size_rtx, next_arg_reg,

  12. GEN_INT (struct_value_size)));

  13. }

  14. else

  15. #endif

  這只是emit_call_1的一小部分。gen_rtx_MEM就是建立一個記憶體地址對應的rtx,這裡用來獲取被呼叫的函式地址(注意,這裡的地址使用符號表示,因為函式到底會被安排在哪裡目前還不知道,給它安排個符號,讓彙編器和聯結器去翻譯成真實的地址)。那麼這個GEN_CALL是什麼?至少在gcc 被 built 之前是不知道的。但是可以告訴你的是,它由一個叫做Machine Description的東西來決定。這裡的GEN_CALL呼叫的是gen_call函式,這個函式定義在insn-emit.c中,而這個檔案實在build的時候由Machine Description生成的。在i386平臺的Machine Description中,gen_call函式轉而去呼叫ix86_expand_call,因此真正的call insn是由這個函式來完成的。而這個函式又呼叫了一堆 gen_rtx_XXX來組裝insn,這一堆gen_rtx_XXX是從gcc/rtl.def檔案自動生成的。

  rtl.def 檔案是由一串巨集組成的,這個巨集形如DEF_RTL_EXPR(ENUM, NAME, FORMAT, CLASS)。ENUM是列舉名,gen_rtx_XXX中的XXX部分就是這個列舉名;NAME是識別名,用在其他地方識別rtl;FORMAT是引數格式,代表這個rtx有多少個引數,每個引數是什麼型別。比如0代表常數0,e代表表示式等等。CLASS是型別。

  在gcc目錄下有個叫做gengenrtl.c的檔案,他有自己的main函式,所以是一個獨立的程式。該程式就是將rtl.def翻譯成genrtl.h和genrtl.c兩個檔案,前者聲明瞭gen_rtx_XXX到gen_rtx_fmt_FFF_stat的對應關係,其中FFF就是巨集裡面的FORMAT引數,gen_rtx_CALL對應的就是gen_rtx_fmt_ee_stat;後者定義了gen_rtx_fmt_FFF_stat的實現。

  1. /* Write the declarations for the routine to allocate RTL with FORMAT. */

  2. static void

  3. gendecl (const char *format) // 為每個gen_rtx_fmt_FFF_stat建立宣告

  4. {

  5.   const char *p;

  6.   int i, pos;

  7.   printf ("extern rtx gen_rtx_fmt_%s_stat\t (RTX_CODE, ", format);

  8.   printf ("enum machine_mode mode");

  9.   /* Write each parameter that is needed and start a new line when the line

  10.      would overflow.  */

  11.   for (p = format, i = 0, pos = 75; *p != 0; p++)

  12.     if (*p != '0')

  13.       {

  14.     int ourlen = strlen (type_from_format (*p)) + 6 + (i > 9);

  15.     printf (",");

  16.     if (pos + ourlen > 76)

  17.       printf ("\n\t\t\t\t      "), pos = 39;

  18.     printf (" %sarg%d", type_from_format (*p), i++);

  19.     pos += ourlen;

  20.       }

  21.   printf (" MEM_STAT_DECL");

  22.   printf (");\n");

  23.   printf ("#define gen_rtx_fmt_%s(c, m", format); // 定義gen_rtx_fmt_FFF 到 gen_rtx_fmt_FFF_stat

  24.   for (p = format, i = 0; *p != 0; p++)

  25.     if (*p != '0')

  26.       printf (", p%i",i++);

  27.   printf (")\\\n        gen_rtx_fmt_%s_stat (c, m", format);

  28.   for (p = format, i = 0; *p != 0; p++)

  29.     if (*p != '0')

  30.       printf (", p%i",i++);

  31.   printf (" MEM_STAT_INFO)\n\n");

  32. }

  33. /* Generate macros to generate RTL of code IDX using the functions we

  34. write. */

  35. static void

  36. genmacro (int idx)

  37. {

  38. const char *p;

  39. int i;

  40. /* We write a macro that defines gen_rtx_RTLCODE to be an equivalent to

  41. gen_rtx_fmt_FORMAT where FORMAT is the RTX_FORMAT of RTLCODE. */

  42. if (excluded_rtx (idx))

  43. /* Don't define a macro for this code. */

  44. return;

  45. printf ("#define gen_rtx_%s%s(MODE",

  46. special_rtx (idx) ? "raw_" : "", defs[idx].enumname); // 定義gen_rtx_ENUM 到 gen_rtx_fmt_FFF

  47. for (p = defs[idx].format, i = 0; *p != 0; p++)

  48. if (*p != '0')

  49. printf (", ARG%d", i++);

  50. printf (") \\\n gen_rtx_fmt_%s (%s, (MODE)",

  51. defs[idx].format, defs[idx].enumname);

  52. for (p = defs[idx].format, i = 0; *p != 0; p++)

  53. if (*p != '0')

  54. printf (", (ARG%d)", i++);

  55. puts (")");

  56. }

  57. /* Generate the code for the function to generate RTL whose

  58. format is FORMAT. */

  59. static void

  60. gendef (const char *format) // 為每個gen_rtx_fmt_FFF_stat建立定義

  61. {

  62. const char *p;

  63. int i, j;

  64. /* Start by writing the definition of the function name and the types

  65. of the arguments. */

  66. printf ("rtx\ngen_rtx_fmt_%s_stat (RTX_CODE code, enum machine_mode mode", format);

  67. for (p = format, i = 0; *p != 0; p++) // 遍歷format中的字元,每個字元對應一個引數

  68. if (*p != '0')

  69. printf (",\n\t%sarg%d", type_from_format (*p), i++);

  70. puts (" MEM_STAT_DECL)");

  71. /* Now write out the body of the function itself, which allocates

  72. the memory and initializes it. */

  73. puts ("{");

  74. puts (" rtx rt;");

  75. puts (" rt = rtx_alloc_stat (code PASS_MEM_STAT);\n");

  76. puts (" PUT_MODE (rt, mode);");

  77. for (p = format, i = j = 0; *p ; ++p, ++i) // 每個引數對應一個insn成員賦值語句。

  78. if (*p != '0')

  79. printf (" %s (rt, %d) = arg%d;\n", accessor_from_format (*p), i, j++);

  80. else

  81. printf (" X0EXP (rt, %d) = NULL_RTX;\n", i);

  82. puts ("\n return rt;\n}\n");

  83. }

  所以總的說來,一個insn自底向上的構建的話,先由rtl.def構建原子的rtx,然後由Machine Description組裝insn或者insn 序列。

2.3 Basic Block中的insn

  前面提到過,basic block中有兩套指令系統:gimple和RTL。那麼basic block中的RTL是從哪裡來的呢?還是回到expand_gimple_basic_block函式:

  1.   if (stmt || elt)

  2.     {

  3.       last = get_last_insn ();

  4. // 此處省略若干字

  5.       /* Java emits line number notes in the top of labels.

  6.      ??? Make this go away once line number notes are obsoleted.  */

  7.       BB_HEAD (bb) = NEXT_INSN (last);

  8.       if (NOTE_P (BB_HEAD (bb)))

  9.     BB_HEAD (bb) = NEXT_INSN (BB_HEAD (bb)); // 看這裡

  10.       note = emit_note_after (NOTE_INSN_BASIC_BLOCK, BB_HEAD (bb));

  11.       maybe_dump_rtl_for_gimple_stmt (stmt, last);

  12.     }

  13.   else

  14.     note = BB_HEAD (bb) = emit_note (NOTE_INSN_BASIC_BLOCK); // 或者這裡

  15. // 此處省略1000字

  16. last = get_last_insn ();

  17. if (BARRIER_P (last))

  18. last = PREV_INSN (last);

  19. if (JUMP_TABLE_DATA_P (last))

  20. last = PREV_INSN (PREV_INSN (last));

  21. BB_END (bb) = last; // 還有這裡

  對應的,在函式體中間也有對BB_HEAD(bb)的賦值,是設定basic block的insn序列的起始。BB_HEAD 排除了基本塊開頭的LABEL,BB_END排除了基本塊最後的跳轉表。所以每個基本塊的insn序列就是函式insn序列的子序列。不同基本塊的insn序列不會相交,甚至可能不會連著,因為中間還隔著LABEL和跳轉表。

  pass_expand之後的pass基本上都是RTL Pass了。這些pass要麼通過get_first_insn()/get_last_insn()來遍歷整個函式的insn列表(包含Label和跳轉),要麼用FOREACH_BB、BB_HEAD、BB_END來遍歷每個基本塊內部的insn(不包含Label和跳轉)。

三、Machine Description

  針對每個CPU平臺,gcc有對應的Machine Description用指導指令生成。這些程式碼放在gcc/config/<平臺名稱>的目錄下,比如intel平臺的在gcc/config/i386/。一個Machine Description檔案是對應平臺的核心,比如gcc/config/i386/i386.md檔案。

  一個md檔案中可以定義很多東西,比如constant、attr、insn、expand等等。constant是給一個編號起一個名字,其他地方如果要用到這個編號,可以用名字代替。比如i386.md中每個暫存器有一個編號;attr是目標平臺的屬性,比如有些什麼擴充套件指令集、有些什麼功能、或者被禁用了那些功能等等;insn和expand是md檔案的主體,用來定義insn,不同的是前者的輸出是asm,用於指令生成;後者的輸出是insn sequence;用於GIMPLE轉RTL。

  每個insn和expand有這麼幾個要素:名字、RTL模板、條件、輸出模板。名字是insn的識別名,比如rtl.def中CALL的識別名是call,所以對應的insn就是md檔案裡的define_expand call;RTL模板是RTX的規格,它有兩個作用:1.判斷是否匹配某個insn,2.指出每個運算元的屬性(大小、使用情況,前置後置條件);條件被用來檢查該insn的前置條件,如果不符合,那就有問題;輸出模板是該insn的彙編輸出格式,用於最後的指令發射。

  要注意的是md檔案定義的是insn pattern,具體的insn是由expand_xxx、emit_xxx、gen_rtx_xxx、gen_xxx那一堆函式生成的。所以md檔案裡的insn只有兩個作用:1.檢查insn;2.輸出asm

  那麼md檔案是如何融入到gcc中的呢?還是靠build!和前面講的rtl.def生成genrtl.h、genrtl.c類似,md檔案被一系列工具翻譯成不同作用的程式碼:

  1. [[email protected] gcc]# ls insn-*.h

  2. insn-attr.h insn-codes.h insn-config.h insn-constants.h insn-flags.h insn-modes.h

  3. [[email protected] gcc]# ls insn-*.c

  4. insn-attrtab.c insn-emit.c insn-modes.c insn-output.c insn-preds.c

  5. insn-automata.c insn-extract.c insn-opinit.c insn-peep.c insn-recog.c

  這裡只說三個檔案:insn-recog.c包含了RTL模板匹配的程式碼,用來檢查rtx的合法性;insn-emit.c包含了insn的構建程式碼;insn-output.c包含了insn對應的asm輸出。這三個檔案分別由gcc/genrecog.c、gcc/genemit.c 和 gcc/genoutput.c編譯出來的三個程式來生成,不妨還是那上面的call來舉例子:

  1. (define_expand "call"

  2. [(call (match_operand:QI 0 "" "")

  3. (match_operand 1 "" ""))

  4. (use (match_operand 2 "" ""))]

  5. ""

  6. {

  7. ix86_expand_call (NULL, operands[0], operands[1], operands[2], NULL, 0);

  8. DONE;

  9. })

  這個call insn要求第一個運算元是一個整數(QI),第二個和第三個引數自便,但是第三個引數是程式要使用的。從expand_call可以看出,第一個運算元是呼叫函式的地址,第二個運算元是引數堆疊大小,第三個運算元是引數列表(所有引數都在這第三個運算元裡)。這個expand被用於gimple_call到insn的轉換。

  這條md定義被genemit工具轉換成了一個叫做gen_call的函式,函式體中除了準備引數之外,最核心的就是呼叫ix86_expand_call。這是轉換之後的結果:

  1. /* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:13574 */

  2. rtx

  3. gen_call (rtx operand0,

  4.         rtx operand1,

  5.         rtx operand2)

  6. {

  7.   rtx _val = 0;

  8.   start_sequence ();

  9.   {

  10.     rtx operands[3];

  11.     operands[0] = operand0;

  12.     operands[1] = operand1;

  13.     operands[2] = operand2;

  14. #line 13579 "/usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md"

  15. {

  16.   ix86_expand_call (NULL, operands[0], operands[1], operands[2], NULL, 0); // expand 的輸出程式碼會出現在gen_xxx函式中

  17.   DONE;

  18. }

  19.     operand0 = operands[0];

  20.     operand1 = operands[1];

  21.     operand2 = operands[2];

  22.   }

  23.   emit_call_insn (gen_rtx_CALL (VOIDmode,

  24.         operand0,

  25.         operand1));

  26.   emit_insn (gen_rtx_USE (VOIDmode,

  27.         operand2));

  28.   _val = get_insns ();

  29.   end_sequence ();

  30.   return _val;

  31. }

這是一個expand,用來生成insn,所以沒有對應的output。再看一個insn的例子:

  1. (define_insn "x86_fnstsw_1"

  2. [(set (match_operand:HI 0 "register_operand" "=a")

  3. (unspec:HI [(reg:CCFP FPSR_REG)] UNSPEC_FNSTSW))]

  4. "TARGET_80387" // 只能在允許80387指令情況下使用

  5. "fnstsw\t%0" // asm指令模板

  6. [(set (attr "length") (symbol_ref "ix86_attr_length_address_default (insn) + 2"))

  7. (set_attr "mode" "SI")

  8. (set_attr "unit" "i387")])

轉換成gen_xxx之後變成:

  1. /* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:1361 */

  2. rtx

  3. gen_x86_fnstsw_1 (rtx operand0 ATTRIBUTE_UNUSED)

  4. {

  5. return gen_rtx_SET (VOIDmode,

  6. operand0,

  7. gen_rtx_UNSPEC (HImode,

  8. gen_rtvec (1,

  9. gen_rtx_REG (CCFPmode,

  10. 18)),

  11. 31));

  12. }

asm模板不會出現在gen_xxx中,因為這個函式pass_expand是用來構建insn的。asm模板會轉換到insn-output.c中:

  1. // struct insn_data 的初始化。

  2. /* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:1361 */

  3. {

  4. "x86_fnstsw_1",

  5. #if HAVE_DESIGNATED_INITIALIZERS

  6. { .single = // 單一的指令對應single,如果是多行指令,會生成對應的output函式,這裡就是 .function = { output_nnn }

  7. #else

  8. {

  9. #endif

  10. "fnstsw\t%0", // ASM輸出模板

  11. #if HAVE_DESIGNATED_INITIALIZERS

  12. },

  13. #else

  14. 0,

  15. 0

  16. },

  17. #endif

  18. (insn_gen_fn) gen_x86_fnstsw_1,

  19. &operand_data[24],

  20. 1,

  21. 0,

  22. 1,

  23. 1

  24. }

四、指令生成

  在優化的pass序列的最後,有一個叫做pass_final的RTL Pass,這個pass負責將RTL翻譯為ASM。它的execute函式最核心的三行:

  1. final_start_function (get_insns (), asm_out_file, optimize);

  2. final (get_insns (), asm_out_file, optimize);

  3. final_end_function ();

  第一行輸出函式的頭,包括函式的彙編說明、stack frame的建立。第二行輸出指令序列;第三行結束函式,包括stack frame的銷燬、結束說明等。

final函式遍歷整個函式的insn序列,呼叫final_scan_insn輸出每一個insn。這個函式太長,要處理note、debug、frame等等亂七八糟的東西。但是中間最關鍵的一段是呼叫Machine Description來輸出ASM:

  1. insn_code_number = recog_memoized (insn); // 找insn code number,就是insn的編號

  2. cleanup_subreg_operands (insn);

  3. // 此處省略若干行

  4.  /* Find the proper template for this insn. */

  5. templ = get_insn_template (insn_code_number, insn); // 獲取define_insn的ASM輸出模板

  6. /* If the C code returns 0, it means that it is a jump insn

  7. which follows a deleted test insn, and that test insn

  8. needs to be reinserted. */

  9. if (templ == 0)

  10. {

  11. rtx prev;

  12. // 繼續省略若干行

  13.  return prev;

  14. }

  15. /* If the template is the string "#", it means that this insn must

  16. be split. */

  17. if (templ[0] == '#' && templ[1] == '\0')

  18. {

  19. rtx new_rtx = try_split (body, insn, 0); // 去呼叫define_split

  20. //又省略若干行

  21.  return new_rtx;

  22. }

  23. // 無關緊要的還是省略吧

  24.  /* Output assembler code from the template. */

  25. output_asm_insn (templ, recog_data.operand); // 按照模板輸出asm

  指令生成的最關鍵一步是這段程式碼的第一個工作:識別insn。這一個工作很令人費解:既然insn是由md來生成的,那麼生成的時候就應該知道這個insn該由md裡面的哪一條定義提供asm輸出,為什麼還要識別呢?因為有的insn並不是全靠RTL來生成。就比如上面說的call,雖然他提供了expand的方法,但是真實的工作是由定義在gcc/config/i386/i386.c檔案中的ix86_expand_call函式來完成。這個函式手工生成了一系列insn來完成函式呼叫的工作,那麼這些insn如何來輸出?

  所以gcc提供了genrecog生成recog函式來完成insn的識別。識別的方法就是將md檔案中的所有RTL表示式當作模式串集合,看真實的insn複合哪一個RTL表示式,那麼這個insn就有對應的定義輸出。recog函式返回對應insn的編號,然後按這個編號去找md的定義,並找到asm輸出模板,於是有了上面這段輸出程式碼。

  recog函式的核心就是一棵硬編碼的決策樹。genrecog首先會掃描全部的md定義,抽取所有的RTL模式串,分解為一串predicates,然後將這些predicates插入到決策樹中。recog函式就是一邊輸入未知insn的predicates,一邊從樹根開始做決策(其實就是跳轉),直到遇到樹葉完成決策。

  在此之後的兩個pass只是清理一下資料結構。由此整個pass鏈呼叫完畢,gcc完成了從GENERIC到GIMPLE,再到RTL,最後到ASM的轉換。

五、總結

  這個系列對gcc從輸入到輸出的流程進行了粗略的分析。一個編譯器最核心的是優化部分。具體的優化步驟在本系列中沒有提到,因為太多、太繁瑣、也太理論。以後可以考慮把教科書中提到的優化挑出來分析一下,但最近是沒有時間了,就此告一段落。