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函数。
图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指令,我们已在前面的章节中分析过这个函数,这里不再重复。
图5.2.12 TranslateFunctionCall()
另一种后缀表达式是形如“a++和a--”的表达式,UCC编译器在语义检查时已把它们分别转换为a+=1和a-=1来处理,而+=和-=是赋值运算符,我们会在讨论赋值表达式的翻译时进行介绍。