C编译器剖析_6.3.1 汇编代码生成_由中间指令产生汇编代码的主要流程

来源:互联网 发布:考试100分软件 编辑:程序博客网 时间:2024/05/18 03:29

6.3.1  由中间指令产生汇编代码的主要流程

    在这一小节,我们可把关注的焦点放在“如何把某条中间代码翻译成汇编代码”上。UCC编译器的中间代码是如下所示的四元式,包括运算符和3个操作数。

         <运算符opcode,目的操作数DST,源操作数SRC1,源操作数SRC2>

    当然有些中间代码只需要用到opcode和DST就可以了,例如,无条件跳转指令“goto  BB2;”就不需要SRC1和SRC2。为了便于汇编代码的生成,UCC编译器在ucl\X86Linux.tpl中定义了许多汇编指令的模板。在X86平台上,无条件跳转指令的汇编指令为jmp,对应的指令模板如下所示:

         //无条件跳转

TEMPLATE(X86_JMP,     "jmp  %0")

    其中,X86_JMP是模板”jmp  %0”的编号,通过编号可找到对应的模板。模板中的“%0”充当占位符的作用,代表第0个操作数,即目的操作数DST,在汇编代码生成时,“%0”会被目的操作数DST所替代;而”%1”代表第1个操作数,即源操作数SRC1,“%2”代表第2个操作数,即源操作数SRC2。图6.3.1第29至31行的宏DST、SRC1和SRC2分别表示中间指令inst里的这3个操作数。假设跳转的目标是BB2,则根据模板“jmp  %0”,我们可产生汇编指令“jmp  BB2”。       

    下面,我们来举例介绍一下,如何由中间指令的运算符opcode出发,找到对应的汇编指令模板号。在中间代码层次,UCC编译器用ADD来表示加法指令,但在汇编层次,整数加法和浮点数加法对应的汇编指令是不一样的,如图6.3.1第22至25行所示。为了能通过中间代码的运算符ADD,快速地找到对应的汇编指令模板编号,ucl\X86Linux.tpl中的各模板是按一定顺序排列的。例如,当我们要进行两个double类型浮点数的加法运算时,其中间指令里的运算符opcode为ADD,类型为DOUBLE,通过第1至15行的函数TypeCode,我们可得到DOUBLE对应的类型编码为F8。以ADD和F8作为宏参数,通过第27行的宏ASM_CODE(),我们就可得到对应的模板号X86_ADDF8,如图6.3.1第26行所示。


图6.3.1 由中间指令到汇编代码的流程

     通过模板号X86_ADDF8,我们就可以在图6.3.1第33行查表,得到对应的汇编指令模板“faddl  %2”。当然在调用第32行的函数PutASMCode前,我们需要先为中间指令里的操作数分配必要的寄存器。图6.3.1第35至56行的while循环用来处理形如“faddl  %2”的汇编指令模板,在第43至50行,我们会将占位符“%0”、“%1”和“%2”替换为相应的操作数名称。如果操作数的值已经被加载到寄存器中,则在第46行输出对应寄存器的名称,形如“%eax”;否则在第48行调用我们在第6.1节分析过的函数GetAccessName,输出操作数的名称,形如“number”或者“20(%ebp)”。第41至42行处理模板中形如“%%eax”的字符串,在AT&T的汇编指令中,寄存器名前要加一个“%”,由于在UCC的汇编指令模板中,符号“%”已经被当作转义字符,因此用“%%”表示“%”本身。模板中的“%%eax”经第32行的PutASMCode函数处理后,会得到“%eax”,其中的“eax”会由第52至53行进行输出。

     需要注意的是,虽然UCC的原作者尽量使X86Linux.tpl中的汇编指令模板有序排列,但还是有一些中间指令不能用图6.3.1第27行的宏ASM_CODE来找到对应的模板编号。UCC编译器在生成汇编代码时会进行特殊处理,例如用于“自加”的中间指令INC,我们就通过“X86_INCI1 +TypeCode(inst->ty)”来找到与其对应的模板编号,而不是用宏ASM_CODE,如下所示。

         static  void  EmitInc(IRInst inst){

                  /****************************************

          TEMPLATE(X86_INCI1,  "incb %0")

          TEMPLATE(X86_INCU1,         "incb %0")

          TEMPLATE(X86_INCI2,  "incw %0")

          TEMPLATE(X86_INCU2,         "incw %0")                                  

          TEMPLATE(X86_INCI4,  "incl %0")

          TEMPLATE(X86_INCU4,         "incl %0")

          TEMPLATE(X86_INCF4, "fld1;fadds %0;fstps %0")

          *****************************************/

         PutASMCode(X86_INCI1+ TypeCode(inst->ty), inst->opds);

}       

    在第6.2节,我们介绍了用于分配寄存器的函数GetRegInternal,在此基础上,我们来进一步分析“为中间指令里的I4或U4类型操作数分配寄存器”的函数AllocateReg,如图6.3.2所示。第2行的参数index表示要为哪一个操作数分配寄存器,取值范围为{0,1,2},分别对应DST、SRC1和SRC2。UCC编译器只为临时变量分配寄存器,第5行对此进行检查。如果当前操作数是“已经分配过寄存器”的临时变量,则在第9行设置标志位,表示相应的寄存器会被用于“当前中间指令的翻译”。如果我们需要为目的操作数DST分配寄存器,而源操作数SRC1已在寄存器中,并且在当前中间指令之后,源操作数SRC1不再被使用,则可以让DST直接重用SRC1的寄存器,第12至18行对此进行处理。对于其他情况,我们在第19行调用GetReg函数分配一个4字节寄存器,并在第21行通过MOV指令把源操作数加载到寄存器中,在第23行调用AddVarToReg把临时变量p添加到寄存器reg的链表reg->link中。在UCC编译器内部,只有把临时变量p添加到寄存器reg对应的链表reg->link时,才意味着我们确实把寄存器reg“长期”分配给了临时变量p。在目前版本的UCC中,该链表最多只存放一个临时变量。


图6.3.2  为中间指令里的I4或U4类型操作数分配寄存器

    函数AddVarToReg的代码如图6.3.2第25至32行所示,第29至30行实现了链表插入操作,第31行用于记录“临时变量是存放在哪个寄存器中”。如果某个变量存放于寄存器中,当我们修改寄存器中的值时,我们并不马上写回内存,而是设置一个标志位neeedwb,表示需要在稍后进行回写WriteBack,这可通过第36行的函数ModifyVar来实现。

    当操作数为浮点数时,我们需要为之分配浮点寄存器,为了简单起见,UCC编译器只使用了X87的栈顶寄存器。浮点寄存器的管理并不是通过GetReg或AllocateReg等函数,我们会在汇编代码生成时,会对浮点数进行专门处理。当操作数为I1或U1类型时,我们通过图6.3.3第53行的GetByteReg来获得单字节寄存器;而当操作数为I2或者U2类型时,经由图6.3.3第56行的GetWordReg来获得双字节寄存器。

    接下来,我们以EmitMove函数为例来进行讨论,该函数所处理的中间指令如下所示,

         <MOV,  DST, SRC1,  NULL>

    该中间指令执行“DST  =  SRC1;”的赋值操作,函数EmitMove的代码如图6.3.3所示。

第4至7行用于处理浮点数之间的赋值,我们在第5行调用EmitX87Move函数来产生相应的汇编代码。对于结构体对象之间的赋值,我们通过第9行的EmitMoveBlock函数来处理。我们会在稍后以这两个函数进行分析。


图6.3.3  EmitMove()

    当DST和SRC1为char(或者unsigned char)时,我们执行图6.3.3第13至22行的代码。按照X86汇编指令的寻址要求,同一条汇编指令的两个操作数不可以都在内存中。因此,当我们面对以下赋值语句“a = b;”时,我们要在第18行通过GetByteReg函数获取一个单字节的寄存器{al,cl,dl},然后在第19行产生movb指令,把源操作数b从内存加载到寄存器中,之后在第20行产生另一条movb指令,把寄存器中的值传给目的操作数a,对该寄存器的使用到此就已结束,我们并未把该寄存器“长期”分配给a或者b。当然,如果源操作数是整数常量,我们在第16行产生一条movb指令即可,例如以下的“movb  $49,a”。

         char   a, b;

         a = b;

         a = '1';

         /////////对应的汇编代码//////////

         movb b, %al               // a = b;

         movb %al, a

         movb $49, a              // a = '1';

    对于形如“a+b”或者“~a”的算术表达式而言,在语义检查时,UCC会把低于int型的整型操作数隐式地提升为int型,因此公共子表达式的类型实际上为I4或者U4。图6.3.3第32至48行用来处理I4或U4类型的赋值操作“DST=SRC1;”。图6.3.3第34行用来处理形如“x = 2015;”的赋值,x可以是临时变量t也可是有名变量a。第36至37行调用AllocateReg函数为DST和SRC1分配4字节寄存器。而第38至43行用来处理形如“a=b;”的有名变量之间的赋值,按x86汇编指令的寻址要求,我们可在第41行通过GetReg函数来获取一个寄存器来做中转,从而产生两条movl指令。第45行用于处理形如“t = x;”或者“x = t;”的赋值,其中t为临时变量,由于临时变量的值在寄存器中,此时我们产生一条movl汇编指令即可。当DST为临时变量时,由于进行了“t=x;”的赋值操作,t对应寄存器中的内容会发生变化,我们会在第48行调用ModifyVar函数来设置回写标志位。

    图6.3.3第23至31行用来处理I2或U2类型的赋值操作“DST=SRC1;”,基本的流程与第13至22行类似,我们就不再啰嗦。接下来,我们来分析一下图6.3.3中用到的EmitX87Move和EmitMoveBlock这两个函数。

    图6.3.4第1至25行的函数EmitX87Move用于实现F4或F8浮点型的赋值操作“DST = SRC1;”,当SRC1的值不在x87栈顶寄存器时,我们通过第4行的SaveX87Top把当前栈顶寄存器的值进行回写,然后在第6行产生汇编指令,把SRC1从内存中加载到x87栈顶寄存器。执行到第8行时,SRC1的值已保存在栈顶寄存器中,当DST不是临时变量时,如果SRC1是临时变量且在“DST = SRC1;”之后SRC1还要被使用,我们就在第11行把x87栈顶寄存器的值传送到DST,但不把栈顶寄存器弹出,即该寄存器中仍然保存SRC1的值;否则在第17行把x87栈顶寄存器的值传送到DST,然后把栈顶寄存空对空出栈,之后在第18行设置X87Top为NULL,表示x87栈顶寄存器中没有保存任何临时变量的值。如果DST是临时变量,我们就在第21行设置其回写标志位needwb,在第22行把X87Top改为DST,表示临时变量DST的值被保存在x87栈顶寄存器中。


图6.3.4  EmitX87Move和EmitMoveBlock

    图6.3.4第26至55行的EmitMoveBlock用于结构体对象之间的赋值,对于第39行的“a = b;”来说,结构体对象a要占40字节,对应的汇编代码在第43至46行,其中用到了寄存器{edi, esi, ecx},esi用于存放b的地址,edi用于存放a的地址,ecx存放要复制的字节个数,第46行的“rep  movsb”实现了内存复制。由于用到了{edi, esi, ecx}这几个寄存器,我们需要在第30至32行通过SpillReg函数进行必要的回写操作。第48行根据类型信息,得到要复制的字节个数,第54行产生进行内存复制的汇编指令。

0 0