GCC源码分析(五)——指令生成

原文链接:http://blog.csdn.net/sonicling/article/details/8246231


一、前言

  又有好久没写了,的确很忙。前篇介绍了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.   
  18.      if (def_p != NULL)  
  19. {  
  20.   /* Ignore this stmt if it is in the list of 
  21.      replaceable expressions.  */  
  22.   if (SA.values  
  23.       && bitmap_bit_p (SA.values,  
  24.                SSA_NAME_VERSION (DEF_FROM_PTR (def_p))))  
  25.     continue;  
  26. }  
  27.      last = expand_gimple_stmt (stmt); //<strong> </strong>这是真正干活的地方  
  28.      maybe_dump_rtl_for_gimple_stmt (stmt, last);  
  29.    }  

  进入到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.   
  9. if (fndecl  
  10.     && (attr = lookup_attribute ("error",  
  11.                  DECL_ATTRIBUTES (fndecl))) != NULL)  
  12.   error ("%Kcall to %qs declared with attribute error: %s",  
  13.      exp, identifier_to_locale (lang_hooks.decl_printable_name (fndecl, 1)),  
  14.      TREE_STRING_POINTER (TREE_VALUE (TREE_VALUE (attr))));  
  15. if (fndecl  
  16.     && (attr = lookup_attribute ("warning",  
  17.                  DECL_ATTRIBUTES (fndecl))) != NULL)  
  18.   warning_at (tree_nonartificial_location (exp),  
  19.           0, "%Kcall to %qs declared with attribute warning: %s",  
  20.           exp, identifier_to_locale (lang_hooks.decl_printable_name (fndecl, 1)),  
  21.           TREE_STRING_POINTER (TREE_VALUE (TREE_VALUE (attr))));  
  22.   
  23. /* Check for a built-in function.  */  
  24. if (fndecl && DECL_BUILT_IN (fndecl))  
  25.   {  
  26.     gcc_assert (DECL_BUILT_IN_CLASS (fndecl) != BUILT_IN_FRONTEND);  
  27.     return expand_builtin (exp, target, subtarget, tmode, ignore); // 内置函数  
  28.   }  
  29.      }  
  30.      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.   
  11. currently_expanding_call--;  
  12.   
  13. if (stack_usage_map_buf)  
  14.   free (stack_usage_map_buf);  
  15.   
  16. return target;  

  所谓尾调用就相当于 return tail_call(...);。这个是有专门优化的。但不管怎么优化,最后的insn被发射(emit)了:

  1. rtx  
  2. emit_insn (rtx x)  
  3. {  
  4.   rtx last = last_insn;  
  5.   rtx insn;  
  6.   
  7.   if (x == NULL_RTX)  
  8.     return last;  
  9.   
  10.   switch (GET_CODE (x))  
  11.     {  
  12.     // 忽略那些特殊的case  
  13.     default:  
  14.       last = make_insn_raw (x);  
  15.       add_insn (last); // 这里  
  16.       break;  
  17.     }  
  18.   
  19.   return last;  
  20. }  
  21. void  
  22. add_insn (rtx insn) // 一个标准的双链表插入算法  
  23. {  
  24.   PREV_INSN (insn) = last_insn;  
  25.   NEXT_INSN (insn) = 0;  
  26.   
  27.   if (NULL != last_insn)  
  28.     NEXT_INSN (last_insn) = insn;  
  29.   
  30.   if (NULL == first_insn)  
  31.     first_insn = insn;  
  32.   
  33.   last_insn = insn;  
  34. }  

其中first_insn和last_insn是宏定义:

  1. #define first_insn (crtl->emit.x_first_insn)  
  2. #define last_insn (crtl->emit.x_last_insn)  
  3.   
  4. /* Datastructures maintained for currently processed<strong> function</strong> in RTL form.  */  
  5. struct rtl_data x_rtl;  
  6.   
  7. // 在function.h中定义的宏  
  8. #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.   
  3. static void  
  4. gendecl (const char *format) // <strong>为每个gen_rtx_fmt_FFF_stat创建声明</strong>  
  5. {  
  6.   const char *p;  
  7.   int i, pos;  
  8.   
  9.   printf ("extern rtx gen_rtx_fmt_%s_stat\t (RTX_CODE, ", format);  
  10.   printf ("enum machine_mode mode");  
  11.   
  12.   /* Write each parameter that is needed and start a new line when the line 
  13.      would overflow.  */  
  14.   for (p = format, i = 0, pos = 75; *p != 0; p++)  
  15.     if (*p != '0')  
  16.       {  
  17.     int ourlen = strlen (type_from_format (*p)) + 6 + (i > 9);  
  18.   
  19.     printf (",");  
  20.     if (pos + ourlen > 76)  
  21.       printf ("\n\t\t\t\t      "), pos = 39;  
  22.   
  23.     printf (" %sarg%d", type_from_format (*p), i++);  
  24.     pos += ourlen;  
  25.       }  
  26.   printf (" MEM_STAT_DECL");  
  27.   
  28.   printf (");\n");  
  29.   printf ("#define gen_rtx_fmt_%s(c, m", format);  // <strong>定义gen_rtx_fmt_FFF 到 gen_rtx_fmt_FFF_stat</strong>  
  30.   for (p = format, i = 0; *p != 0; p++)  
  31.     if (*p != '0')  
  32.       printf (", p%i",i++);  
  33.   printf (")\\\n        gen_rtx_fmt_%s_stat (c, m", format);  
  34.   for (p = format, i = 0; *p != 0; p++)  
  35.     if (*p != '0')  
  36.       printf (", p%i",i++);  
  37.   printf (" MEM_STAT_INFO)\n\n");  
  38. }  
  39.   
  40. /* Generate macros to generate RTL of code IDX using the functions we 
  41.    write.  */  
  42.   
  43. static void  
  44. genmacro (int idx)  
  45. {  
  46.   const char *p;  
  47.   int i;  
  48.   
  49.   /* We write a macro that defines gen_rtx_RTLCODE to be an equivalent to 
  50.      gen_rtx_fmt_FORMAT where FORMAT is the RTX_FORMAT of RTLCODE.  */  
  51.   
  52.   if (excluded_rtx (idx))  
  53.     /* Don't define a macro for this code.  */  
  54.     return;  
  55.   
  56.   printf ("#define gen_rtx_%s%s(MODE",  
  57.        special_rtx (idx) ? "raw_" : "", defs[idx].enumname); // <strong>定义gen_rtx_ENUM 到 gen_rtx_fmt_FFF</strong>  
  58.   
  59.   for (p = defs[idx].format, i = 0; *p != 0; p++)  
  60.     if (*p != '0')  
  61.       printf (", ARG%d", i++);  
  62.   
  63.   printf (") \\\n  gen_rtx_fmt_%s (%s, (MODE)",  
  64.       defs[idx].format, defs[idx].enumname);  
  65.   
  66.   for (p = defs[idx].format, i = 0; *p != 0; p++)  
  67.     if (*p != '0')  
  68.       printf (", (ARG%d)", i++);  
  69.   
  70.   puts (")");  
  71. }  
  72.   
  73. /* Generate the code for the function to generate RTL whose 
  74.    format is FORMAT.  */  
  75.   
  76. static void  
  77. gendef (const char *format) // <strong>为每个gen_rtx_fmt_FFF_stat创建定义</strong>  
  78. {  
  79.   const char *p;  
  80.   int i, j;  
  81.   
  82.   /* Start by writing the definition of the function name and the types 
  83.      of the arguments.  */  
  84.   
  85.   printf ("rtx\ngen_rtx_fmt_%s_stat (RTX_CODE code, enum machine_mode mode", format);  
  86.   for (p = format, i = 0; *p != 0; p++) // <strong>遍历format中的字符,每个字符对应一个参数</strong>  
  87.     if (*p != '0')  
  88.       printf (",\n\t%sarg%d", type_from_format (*p), i++);  
  89.   
  90.   puts (" MEM_STAT_DECL)");  
  91.   
  92.   /* Now write out the body of the function itself, which allocates 
  93.      the memory and initializes it.  */  
  94.   puts ("{");  
  95.   puts ("  rtx rt;");  
  96.   puts ("  rt = rtx_alloc_stat (code PASS_MEM_STAT);\n");  
  97.   
  98.   puts ("  PUT_MODE (rt, mode);");  
  99.   
  100.   for (p = format, i = j = 0; *p ; ++p, ++i)  // <strong>每个参数对应一个insn成员赋值语句。</strong>  
  101.     if (*p != '0')  
  102.       printf ("  %s (rt, %d) = arg%d;\n", accessor_from_format (*p), i, j++);  
  103.     else  
  104.       printf ("  X0EXP (rt, %d) = NULL_RTX;\n", i);  
  105.   
  106.   puts ("\n  return rt;\n}\n");  
  107. }  
  所以总的说来,一个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.       // 此处省略若干字  
  6.   
  7.       /* Java emits line number notes in the top of labels. 
  8.      ??? Make this go away once line number notes are obsoleted.  */  
  9.       BB_HEAD (bb) = NEXT_INSN (last);  
  10.       if (NOTE_P (BB_HEAD (bb)))  
  11.     BB_HEAD (bb) = NEXT_INSN (BB_HEAD (bb)); // <strong>看这里</strong>  
  12.       note = emit_note_after (NOTE_INSN_BASIC_BLOCK, BB_HEAD (bb));  
  13.   
  14.       maybe_dump_rtl_for_gimple_stmt (stmt, last);  
  15.     }  
  16.   else  
  17.     note = BB_HEAD (bb) = emit_note (NOTE_INSN_BASIC_BLOCK); // <strong>或者这里</strong>  
  18.   
  19.   // 此处省略1000字  
  20.   
  21.   last = get_last_insn ();  
  22.   if (BARRIER_P (last))  
  23.     last = PREV_INSN (last);  
  24.   if (JUMP_TABLE_DATA_P (last))  
  25.     last = PREV_INSN (PREV_INSN (last));  
  26.   BB_END (bb) = last; // <strong>还有这里</strong>  

  对应的,在函数体中间也有对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文件被一系列工具翻译成不同作用的代码:

[plain] view plaincopy
  1. [root@localhost gcc]# ls insn-*.h   
  2. insn-attr.h insn-codes.h insn-config.h insn-constants.h insn-flags.h insn-modes.h   
  3. [root@localhost 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来举例子:

[plain] view plaincopy
  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的例子:

[plain] view plaincopy
  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. // 此处省略若干行  
  5.   
  6.         /* Find the proper template for this insn.  */  
  7.     templ = get_insn_template (insn_code_number, insn); // 获取define_insn的ASM输出模板  
  8.   
  9.     /* If the C code returns 0, it means that it is a jump insn 
  10.        which follows a deleted test insn, and that test insn 
  11.        needs to be reinserted.  */  
  12.     if (templ == 0)  
  13.       {  
  14.         rtx prev;  
  15.   
  16. // 继续省略若干行  
  17.   
  18.             return prev;  
  19.       }  
  20.   
  21.     /* If the template is the string "#", it means that this insn must 
  22.        be split.  */  
  23.     if (templ[0] == '#' && templ[1] == '\0')  
  24.       {  
  25.         rtx new_rtx = try_split (body, insn, 0); // 去调用define_split  
  26.   
  27. //又省略若干行  
  28.   
  29.             return new_rtx;  
  30.       }  
  31.   
  32. // 无关紧要的还是省略吧  
  33.   
  34.         /* Output assembler code from the template.  */  
  35.     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从输入到输出的流程进行了粗略的分析。一个编译器最核心的是优化部分。具体的优化步骤在本系列中没有提到,因为太多、太繁琐、也太理论。以后可以考虑把教科书中提到的优化挑出来分析一下,但最近是没有时间了,就此告一段落。


猜你喜欢

转载自blog.csdn.net/doniexun/article/details/38324681