轉載: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進行轉換:
-
FOR_BB_BETWEEN (bb, init_block->next_bb, EXIT_BLOCK_PTR, next_bb)
-
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之類的,忽略之。直到迴圈最後一部分才進入正題:
-
if (is_gimple_call (stmt) && gimple_call_tail_p (stmt)) // 尾呼叫,特殊情況,忽略之
-
{
-
bool can_fallthru;
-
new_bb = expand_gimple_tailcall (bb, stmt, &can_fallthru);
-
if (new_bb)
-
{
-
if (can_fallthru)
-
bb = new_bb;
-
else
-
return new_bb;
-
}
-
}
-
else
-
{
-
def_operand_p def_p;
-
def_p = SINGLE_SSA_DEF_OPERAND (stmt, SSA_OP_DEF);
-
if (def_p != NULL)
-
{
-
/* Ignore this stmt if it is in the list of
-
replaceable expressions. */
-
if (SA.values
-
&& bitmap_bit_p (SA.values,
-
SSA_NAME_VERSION (DEF_FROM_PTR (def_p))))
-
continue;
-
}
-
last = expand_gimple_stmt (stmt); //這是真正幹活的地方
-
maybe_dump_rtl_for_gimple_stmt (stmt, last);
-
}
進入到expand_gimple_stmt裡面,這個函式不長,一眼可以看出來,核心是expand_gimple_stmt_1 (stmt);,這個函式分情況展開了stmt。其中GIMPLE_CALL對應的是expand_call_stmt。這個函式也不長,關鍵在最後。
-
if (lhs)
-
expand_assignment (lhs, exp, false); // lhs = func(args)
-
else
-
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:分支:
-
case CALL_EXPR:
-
/* All valid uses of __builtin_va_arg_pack () are removed during
-
inlining. */
-
if (CALL_EXPR_VA_ARG_PACK (exp))
-
error ("%Kinvalid use of %<__builtin_va_arg_pack ()%>", exp);
-
{
-
tree fndecl = get_callee_fndecl (exp), attr;
-
if (fndecl
-
&& (attr = lookup_attribute ("error",
-
DECL_ATTRIBUTES (fndecl))) != NULL)
-
error ("%Kcall to %qs declared with attribute error: %s",
-
exp, identifier_to_locale (lang_hooks.decl_printable_name (fndecl, 1)),
-
TREE_STRING_POINTER (TREE_VALUE (TREE_VALUE (attr))));
-
if (fndecl
-
&& (attr = lookup_attribute ("warning",
-
DECL_ATTRIBUTES (fndecl))) != NULL)
-
warning_at (tree_nonartificial_location (exp),
-
0, "%Kcall to %qs declared with attribute warning: %s",
-
exp, identifier_to_locale (lang_hooks.decl_printable_name (fndecl, 1)),
-
TREE_STRING_POINTER (TREE_VALUE (TREE_VALUE (attr))));
-
/* Check for a built-in function. */
-
if (fndecl && DECL_BUILT_IN (fndecl))
-
{
-
gcc_assert (DECL_BUILT_IN_CLASS (fndecl) != BUILT_IN_FRONTEND);
-
return expand_builtin (exp, target, subtarget, tmode, ignore); // 內建函式
-
}
-
}
-
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這個函式最後就有答案:
-
/* If tail call production succeeded, we need to remove REG_EQUIV notes on
-
arguments too, as argument area is now clobbered by the call. */
-
if (tail_call_insns)
-
{
-
emit_insn (tail_call_insns); // 尾呼叫的rtx
-
crtl->tail_call_emit = true;
-
}
-
else
-
emit_insn (normal_call_insns); // 正常呼叫的rtx
-
currently_expanding_call--;
-
if (stack_usage_map_buf)
-
free (stack_usage_map_buf);
-
return target;
所謂尾呼叫就相當於 return tail_call(...);。這個是有專門優化的。但不管怎麼優化,最後的insn被髮射(emit)了:
-
rtx
-
emit_insn (rtx x)
-
{
-
rtx last = last_insn;
-
rtx insn;
-
if (x == NULL_RTX)
-
return last;
-
switch (GET_CODE (x))
-
{
-
// 忽略那些特殊的case
-
default:
-
last = make_insn_raw (x);
-
add_insn (last); // 這裡
-
break;
-
}
-
return last;
-
}
-
void
-
add_insn (rtx insn) // 一個標準的雙鏈表插入演算法
-
{
-
PREV_INSN (insn) = last_insn;
-
NEXT_INSN (insn) = 0;
-
if (NULL != last_insn)
-
NEXT_INSN (last_insn) = insn;
-
if (NULL == first_insn)
-
first_insn = insn;
-
last_insn = insn;
-
}
其中first_insn和last_insn是巨集定義:
-
#define first_insn (crtl->emit.x_first_insn)
-
#define last_insn (crtl->emit.x_last_insn)
-
/* Datastructures maintained for currently processed function in RTL form. */
-
struct rtl_data x_rtl;
-
// 在function.h中定義的巨集
-
#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函式中,有一句特別顯眼:
-
/* Generate the actual call instruction. */
-
emit_call_1 (funexp, exp, fndecl, funtype, unadjusted_args_size,
-
adjusted_args_size.constant, struct_value_size,
-
next_arg_reg, valreg, old_inhibit_defer_pop, call_fusage,
-
flags, & args_so_far);
看不懂程式碼,看註釋也明白了,這不就是生成一個call insn嗎?進入看看:
-
#if defined (HAVE_call) && defined (HAVE_call_value)
-
if (HAVE_call && HAVE_call_value)
-
{
-
if (valreg)
-
emit_call_insn (GEN_CALL_VALUE (valreg,
-
gen_rtx_MEM (FUNCTION_MODE, funexp),
-
rounded_stack_size_rtx, next_arg_reg,
-
NULL_RTX));
-
else
-
emit_call_insn (GEN_CALL (gen_rtx_MEM (FUNCTION_MODE, funexp),
-
rounded_stack_size_rtx, next_arg_reg,
-
GEN_INT (struct_value_size)));
-
}
-
else
-
#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的實現。
-
/* Write the declarations for the routine to allocate RTL with FORMAT. */
-
static void
-
gendecl (const char *format) // 為每個gen_rtx_fmt_FFF_stat建立宣告
-
{
-
const char *p;
-
int i, pos;
-
printf ("extern rtx gen_rtx_fmt_%s_stat\t (RTX_CODE, ", format);
-
printf ("enum machine_mode mode");
-
/* Write each parameter that is needed and start a new line when the line
-
would overflow. */
-
for (p = format, i = 0, pos = 75; *p != 0; p++)
-
if (*p != '0')
-
{
-
int ourlen = strlen (type_from_format (*p)) + 6 + (i > 9);
-
printf (",");
-
if (pos + ourlen > 76)
-
printf ("\n\t\t\t\t "), pos = 39;
-
printf (" %sarg%d", type_from_format (*p), i++);
-
pos += ourlen;
-
}
-
printf (" MEM_STAT_DECL");
-
printf (");\n");
-
printf ("#define gen_rtx_fmt_%s(c, m", format); // 定義gen_rtx_fmt_FFF 到 gen_rtx_fmt_FFF_stat
-
for (p = format, i = 0; *p != 0; p++)
-
if (*p != '0')
-
printf (", p%i",i++);
-
printf (")\\\n gen_rtx_fmt_%s_stat (c, m", format);
-
for (p = format, i = 0; *p != 0; p++)
-
if (*p != '0')
-
printf (", p%i",i++);
-
printf (" MEM_STAT_INFO)\n\n");
-
}
-
/* Generate macros to generate RTL of code IDX using the functions we
-
write. */
-
static void
-
genmacro (int idx)
-
{
-
const char *p;
-
int i;
-
/* We write a macro that defines gen_rtx_RTLCODE to be an equivalent to
-
gen_rtx_fmt_FORMAT where FORMAT is the RTX_FORMAT of RTLCODE. */
-
if (excluded_rtx (idx))
-
/* Don't define a macro for this code. */
-
return;
-
printf ("#define gen_rtx_%s%s(MODE",
-
special_rtx (idx) ? "raw_" : "", defs[idx].enumname); // 定義gen_rtx_ENUM 到 gen_rtx_fmt_FFF
-
for (p = defs[idx].format, i = 0; *p != 0; p++)
-
if (*p != '0')
-
printf (", ARG%d", i++);
-
printf (") \\\n gen_rtx_fmt_%s (%s, (MODE)",
-
defs[idx].format, defs[idx].enumname);
-
for (p = defs[idx].format, i = 0; *p != 0; p++)
-
if (*p != '0')
-
printf (", (ARG%d)", i++);
-
puts (")");
-
}
-
/* Generate the code for the function to generate RTL whose
-
format is FORMAT. */
-
static void
-
gendef (const char *format) // 為每個gen_rtx_fmt_FFF_stat建立定義
-
{
-
const char *p;
-
int i, j;
-
/* Start by writing the definition of the function name and the types
-
of the arguments. */
-
printf ("rtx\ngen_rtx_fmt_%s_stat (RTX_CODE code, enum machine_mode mode", format);
-
for (p = format, i = 0; *p != 0; p++) // 遍歷format中的字元,每個字元對應一個引數
-
if (*p != '0')
-
printf (",\n\t%sarg%d", type_from_format (*p), i++);
-
puts (" MEM_STAT_DECL)");
-
/* Now write out the body of the function itself, which allocates
-
the memory and initializes it. */
-
puts ("{");
-
puts (" rtx rt;");
-
puts (" rt = rtx_alloc_stat (code PASS_MEM_STAT);\n");
-
puts (" PUT_MODE (rt, mode);");
-
for (p = format, i = j = 0; *p ; ++p, ++i) // 每個引數對應一個insn成員賦值語句。
-
if (*p != '0')
-
printf (" %s (rt, %d) = arg%d;\n", accessor_from_format (*p), i, j++);
-
else
-
printf (" X0EXP (rt, %d) = NULL_RTX;\n", i);
-
puts ("\n return rt;\n}\n");
-
}
所以總的說來,一個insn自底向上的構建的話,先由rtl.def構建原子的rtx,然後由Machine Description組裝insn或者insn 序列。
2.3 Basic Block中的insn
前面提到過,basic block中有兩套指令系統:gimple和RTL。那麼basic block中的RTL是從哪裡來的呢?還是回到expand_gimple_basic_block函式:
-
if (stmt || elt)
-
{
-
last = get_last_insn ();
-
// 此處省略若干字
-
/* Java emits line number notes in the top of labels.
-
??? Make this go away once line number notes are obsoleted. */
-
BB_HEAD (bb) = NEXT_INSN (last);
-
if (NOTE_P (BB_HEAD (bb)))
-
BB_HEAD (bb) = NEXT_INSN (BB_HEAD (bb)); // 看這裡
-
note = emit_note_after (NOTE_INSN_BASIC_BLOCK, BB_HEAD (bb));
-
maybe_dump_rtl_for_gimple_stmt (stmt, last);
-
}
-
else
-
note = BB_HEAD (bb) = emit_note (NOTE_INSN_BASIC_BLOCK); // 或者這裡
-
// 此處省略1000字
-
last = get_last_insn ();
-
if (BARRIER_P (last))
-
last = PREV_INSN (last);
-
if (JUMP_TABLE_DATA_P (last))
-
last = PREV_INSN (PREV_INSN (last));
-
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檔案被一系列工具翻譯成不同作用的程式碼:
-
[[email protected] gcc]# ls insn-*.h
-
insn-attr.h insn-codes.h insn-config.h insn-constants.h insn-flags.h insn-modes.h
-
[[email protected] gcc]# ls insn-*.c
-
insn-attrtab.c insn-emit.c insn-modes.c insn-output.c insn-preds.c
-
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來舉例子:
-
(define_expand "call"
-
[(call (match_operand:QI 0 "" "")
-
(match_operand 1 "" ""))
-
(use (match_operand 2 "" ""))]
-
""
-
{
-
ix86_expand_call (NULL, operands[0], operands[1], operands[2], NULL, 0);
-
DONE;
-
})
這個call insn要求第一個運算元是一個整數(QI),第二個和第三個引數自便,但是第三個引數是程式要使用的。從expand_call可以看出,第一個運算元是呼叫函式的地址,第二個運算元是引數堆疊大小,第三個運算元是引數列表(所有引數都在這第三個運算元裡)。這個expand被用於gimple_call到insn的轉換。
這條md定義被genemit工具轉換成了一個叫做gen_call的函式,函式體中除了準備引數之外,最核心的就是呼叫ix86_expand_call。這是轉換之後的結果:
-
/* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:13574 */
-
rtx
-
gen_call (rtx operand0,
-
rtx operand1,
-
rtx operand2)
-
{
-
rtx _val = 0;
-
start_sequence ();
-
{
-
rtx operands[3];
-
operands[0] = operand0;
-
operands[1] = operand1;
-
operands[2] = operand2;
-
#line 13579 "/usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md"
-
{
-
ix86_expand_call (NULL, operands[0], operands[1], operands[2], NULL, 0); // expand 的輸出程式碼會出現在gen_xxx函式中
-
DONE;
-
}
-
operand0 = operands[0];
-
operand1 = operands[1];
-
operand2 = operands[2];
-
}
-
emit_call_insn (gen_rtx_CALL (VOIDmode,
-
operand0,
-
operand1));
-
emit_insn (gen_rtx_USE (VOIDmode,
-
operand2));
-
_val = get_insns ();
-
end_sequence ();
-
return _val;
-
}
這是一個expand,用來生成insn,所以沒有對應的output。再看一個insn的例子:
-
(define_insn "x86_fnstsw_1"
-
[(set (match_operand:HI 0 "register_operand" "=a")
-
(unspec:HI [(reg:CCFP FPSR_REG)] UNSPEC_FNSTSW))]
-
"TARGET_80387" // 只能在允許80387指令情況下使用
-
"fnstsw\t%0" // asm指令模板
-
[(set (attr "length") (symbol_ref "ix86_attr_length_address_default (insn) + 2"))
-
(set_attr "mode" "SI")
-
(set_attr "unit" "i387")])
轉換成gen_xxx之後變成:
-
/* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:1361 */
-
rtx
-
gen_x86_fnstsw_1 (rtx operand0 ATTRIBUTE_UNUSED)
-
{
-
return gen_rtx_SET (VOIDmode,
-
operand0,
-
gen_rtx_UNSPEC (HImode,
-
gen_rtvec (1,
-
gen_rtx_REG (CCFPmode,
-
18)),
-
31));
-
}
asm模板不會出現在gen_xxx中,因為這個函式pass_expand是用來構建insn的。asm模板會轉換到insn-output.c中:
-
// struct insn_data 的初始化。
-
/* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:1361 */
-
{
-
"x86_fnstsw_1",
-
#if HAVE_DESIGNATED_INITIALIZERS
-
{ .single = // 單一的指令對應single,如果是多行指令,會生成對應的output函式,這裡就是 .function = { output_nnn }
-
#else
-
{
-
#endif
-
"fnstsw\t%0", // ASM輸出模板
-
#if HAVE_DESIGNATED_INITIALIZERS
-
},
-
#else
-
0,
-
0
-
},
-
#endif
-
(insn_gen_fn) gen_x86_fnstsw_1,
-
&operand_data[24],
-
1,
-
0,
-
1,
-
1
-
}
四、指令生成
在優化的pass序列的最後,有一個叫做pass_final的RTL Pass,這個pass負責將RTL翻譯為ASM。它的execute函式最核心的三行:
-
final_start_function (get_insns (), asm_out_file, optimize);
-
final (get_insns (), asm_out_file, optimize);
-
final_end_function ();
第一行輸出函式的頭,包括函式的彙編說明、stack frame的建立。第二行輸出指令序列;第三行結束函式,包括stack frame的銷燬、結束說明等。
final函式遍歷整個函式的insn序列,呼叫final_scan_insn輸出每一個insn。這個函式太長,要處理note、debug、frame等等亂七八糟的東西。但是中間最關鍵的一段是呼叫Machine Description來輸出ASM:
-
insn_code_number = recog_memoized (insn); // 找insn code number,就是insn的編號
-
cleanup_subreg_operands (insn);
-
// 此處省略若干行
-
/* Find the proper template for this insn. */
-
templ = get_insn_template (insn_code_number, insn); // 獲取define_insn的ASM輸出模板
-
/* If the C code returns 0, it means that it is a jump insn
-
which follows a deleted test insn, and that test insn
-
needs to be reinserted. */
-
if (templ == 0)
-
{
-
rtx prev;
-
// 繼續省略若干行
-
return prev;
-
}
-
/* If the template is the string "#", it means that this insn must
-
be split. */
-
if (templ[0] == '#' && templ[1] == '\0')
-
{
-
rtx new_rtx = try_split (body, insn, 0); // 去呼叫define_split
-
//又省略若干行
-
return new_rtx;
-
}
-
// 無關緊要的還是省略吧
-
/* Output assembler code from the template. */
-
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從輸入到輸出的流程進行了粗略的分析。一個編譯器最核心的是優化部分。具體的優化步驟在本系列中沒有提到,因為太多、太繁瑣、也太理論。以後可以考慮把教科書中提到的優化挑出來分析一下,但最近是沒有時間了,就此告一段落。