C编译器剖析_5.2.4 中间代码生成及优化_后缀表达式的翻译

5.2.4 后缀表达式的翻译

    在前面的章节中,我们介绍了用于对数组元素和结构体成员进行访问的函数Offset,其接口如下所示,参数addr代表了基地址,参数voff代表可变偏移,而参数coff则代表常量偏移。

         Symbol Offset(Type ty, Symbol addr,Symbol voff, int coff);

    函数Offset的基本想法是产生以下中间代码,我们要先对addr、voff和coff进行相加,得到目标地址(addr+voff+coff),然后再进行“提领Dereference”操作。

         t1:    coff+voff;         

         t2:    addr+t1;            //t2中存放目标地址

         t3:    *t2;                    //提领操作,即间接寻址

    在遇到如下arr[2]这样的只包含常量偏移的数组元素时,我们可把上述addr看成是“addr:&arr;”,coff为8,而voff为NULL,对C语言中数组元素arr[2]的访问,在中间代码层次可用一个符号“arr[8]”来表示即可,不必产生上述中间代码。Offset函数内会对这种情况进行判断,然后调用CreateOffset函数来得到一个名为“arr[8]”的符号对象。

         int   arr[4];

         a = arr[2];

    当我们遇到形如ptr->b的表达式时,我们可把ptr看成上述addr参数,而成员b在结构体中的偏移为常量4,通过Offset函数,我们产生“t1: ptr+4;  t2: *t1;”这样的中间代码,

其中的符号t2即代表了C程序员要访问的ptr->b。

         typedef{

                   int a;         //偏移为0

                   int b;         //偏移为4

         }Data ;

         Data dt;    Data * ptr = &dt;   ptr->b = 3;   dt.b = 5;

     而对于dt.b的处理,与前文对arr[2]的处理类似,在中间代码层次我们用符号“dt[4]”来表示即可,不必进行间接寻址。当要访问的结构体成员是位域时,则会复杂一些。

         struct {

             int a;         //偏移为0

             //以下各成员构成的位模式为“b4_b3_b2_b1”,

             //其中,低6位为b1,高9位为b4

             int b1:6;   //偏移为4,pos为0

             int b2:8;   //偏移为4,pos为6

             int b3:9;   //偏移为4,pos为14

             int b4:9;   //偏移为4,pos为23

} dt2;

int val;

val = dt2.b2;              //读取位域成员

    对上述结构体对象dt2来说,当我们要读取其成员位域dt2.b2时,在中间代码层次我们可用符号“dt2[4]”来表示dt2.b2所在的内存单元。由于UCC编译器总是把位域成员存于一个int型的整数中,符号“dt2[4]”代表的是一个32位的int型整数对应的内存单元。为了读取dt2.b2的值,我们需要进行移位操作,先把整数dt2[4]左移18位(由32-8-6可得到18),即把高18位的b3和b4给“挤走”,然后再算术右移24位(通过32-8可得到24),把低位的b1也“挤走”,此时得到的临时变量t2就是程序员需要读取的dt2.b2,如下所示。

//左移18位,低18位补0,得到位模式b2_b1_0..0,

t1: dt2[4]<<18;

//算术右移24位,得到位模式0…0_b2 或者 s…s_b2,其中s为b2的符号位,

//若b2为无符号数则高24位补0;否则高位补上b2的符号位s

t2: t1>>24;               

    UCC编译器中的函数ReadBitField用于产生这些移位操作,由此可对结构体对象的位域成员进行读操作;而对于位域成员的写操作,我们会在翻译赋值表达式时进行讨论。

    我们再举个例子来说明一下函数名,对于如下C代码,UCC编译器在遇到“ptr = f;”中的函数名f时要生成一条用于取地址的中间代码“t0:&f”,之后再生成“t0 =ptr;”的中间代码,这样在UCC生成汇编代码时就会选用“leal  f, %eax”来获取函数f的首地址并存于eax寄存器中,而如果错误地生成形如“ptr = f;”的中间代码,则对应的汇编指令为“movl f, %ebx; movl %ebx, ptr;”, 这会错误地把函数f代码区的前4个字节的内容送到ptr中。不过,按汇编中call指令的语法,我们可生成形如“call  f”的汇编指令来调用函数f,因此在遇到C语句“f();”中的函数名f时,我们不必为函数名f产生取地址指令,直接用返回函数名f即可。

void f(void){

}

void * ptr;

int main(int argc,char * argv[]){

         ptr = f;

         f();

         return 0;

}

// UCC生成的中间代码和汇编代码如下所示

t0 :&f;      // leal  f, %eax ;   不可以用movl  f, %eax

ptr = t0;    //  movl %eax, ptr

f();         //  call  f

    有了这些基础后,我们就可以来分析一下图5.2.11,其中第1行的函数TranslateArrayIndex用于翻译数组索引,第7至17行的Do循环对常量偏移和可变偏移进行累加,分别存于coff和voff中,第18行调用TranslateExpression来得到数组的首地址,第19行调用Offset函数来产生访问数组元素的中间代码。第22至29行的函数TranslatePrimaryExpression用来翻译基本表达式(常量和ID等),对于数组名和函数名,我们在第27行调用AddressOf函数用来生成取地址的指令,对于其他的标识符,我们在语义检查时已经查过符号表,此处直接在第28行返回expr结点中保存的符号即可。第56行的函数ReadBitField用于产生左移和右移运算的中间代码,第58行计算左移的位数,第59行计算右移的位数,第60行调用的Simplify会进行一些简单的优化工作,并把左移和右移的表达式当作公共子表达式来重用,我们已在前面的章节分析过Simplify函数。

C编译器剖析_5.2.4 中间代码生成及优化_后缀表达式的翻译_第1张图片

图5.2.11 数组元素和结构体成员的翻译

    图5.2.11第30行的TranslateMemberAccess用于为结构体成员的访问生成中间代码,第37至40行用于计算“形如ptr->a的结构体成员”的基地址和常量偏移,而第41至47行则为计算出形如dt.a.b的结构体成员的基地址和常量偏移。在确定基地址和常量偏移后,我们就可以在第49行调用Offset函数来在必要时生成“提领Dereference”指令。如果要对结构体位域成员进行读操作,则第51行的条件会成立,此时我们在第52行调用ReadBitField来产生相应的移位指令。

    接下来,我们来讨论形如“f(a+b,c*d,e);”的函数调用的翻译,相关代码如图5.2.12所示,如果我们在第7行遇到的expr->kids[0]为函数名f对应的语法树结点,则通过第7行的TranslateExpression,我们实际调用的是图5.2.11第22行的TranslatePrimaryExpression函数,在这情况下我们不需要为函数名f产生取地址指令,直接返回f即可,通过在图5.2.11第6行设置isfunc为0,我们可以使图5.2.11第26行的if条件不成立。第9至15行的while循环用来依次对各个实参表达式进行计算,并把计算所得的结果通过第13行插入到向量args中,如果返回值不是void类型,我们就在第18行创建一个临时变量用来存放函数的返回值,第20行调用GenerateFunctionCall函数来生成CALL指令,我们已在前面的章节中分析过这个函数,这里不再重复。

C编译器剖析_5.2.4 中间代码生成及优化_后缀表达式的翻译_第2张图片

图5.2.12 TranslateFunctionCall()

    另一种后缀表达式是形如“a++和a--”的表达式,UCC编译器在语义检查时已把它们分别转换为a+=1和a-=1来处理,而+=和-=是赋值运算符,我们会在讨论赋值表达式的翻译时进行介绍。

你可能感兴趣的:(C编译器剖析)