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函数。

图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来处理,而+=和-=是赋值运算符,我们会在讨论赋值表达式的翻译时进行介绍。

时间: 2024-10-06 01:16:49

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

C编译器剖析_5.4.2 中间代码生成及优化_基本块的合并

5.4.2  基本块的合并 我们在第5.4.1节时给出了由基本块构成的双向链表和控制流图,为阅读方便,我们这里再次给出"图5.1.4 基本块的静态结构和动态结构".在这一小节中,我们试图把双向链表中相邻的基本块进行合并,当然这种合并需要满足一定条件,同时要保持程序的原有语义.在合并后,控制流图中的前驱与后继关系也要进行调整.我们需要改动的数据结构有图5.4.1中的双向链表和控制流图.需要注意的是,虽然基本块BB5和BB6在双向链表中相邻,但控制流却不会由BB5流入BB6,双向链表只是维

C编译器剖析_5.2.2 中间代码生成及优化_再论符号symbol与公共子表达式

5.2.2    再论符号symbol与公共子表达式 在介绍算术表达式的翻译前,让我们简单重温一下第2.5节中的"图2.5.4 公共子表达式"及"图2.5.5 valueDef和valueUse".为阅读方便,我们再次给出这两张图,更详细的说明请参见第2.5节.对于图2.5.4第2行的a+b,我们会由第7行的中间代码来对a+b进行求值,其结果存于临时变量t1中,之后在第3行中再次遇到表达式a+b时,a和b的值并没有发生变化,我们可在第9行直接把t1赋给变量d.由于我

C编译器剖析_5.3.1 中间代码生成及优化_If语句和复合语句的翻译

5.3.1   If语句和复合语句的翻译 我们先简单回顾一下对布尔表达式的翻译,我们通过调用TranslateBranch函数来产生跳转指令,从而实现布尔表达式的语义.在使用函数TranslateBranch(expr, bt, bn)时,有这么两个约定: (1) 当expr为真时,跳往bt基本块: (2) 紧随"函数TranslateBranch所生成的跳转指令"之后的基本块为bn. 在表达式的基础上,我们来讨论一下语句的翻译.图5.3.1用于if语句的翻译,第4至8行说明了如何翻译

C编译器剖析_5.3.2 中间代码生成及优化_switch语句的翻译

5.3.2.Switch语句的翻译 在这一小节中,我们来讨论一下switch语句的翻译,switch语句的产生式如下所示. SwitchStatement: switch( expr ) statement 当C程序员编写出如下代码时,UCC编译器会在语义检查阶段进行报错"error:The  break shall  appear  in  a  switch  or loop",从语法上来看,以下"case  3: b = 30;"是case语句,而"

C编译器剖析_5.1 中间代码生成及优化_简介

5.1  中间代码生成与优化_简介 在语法分析和语义检查阶段,我们始终在与语句Statement.表达式Expression和外部声明ExternalDeclaration这3个概念打交道.通过声明,我们最终建立起了相应的类型结构,并在符号表中保存了相关标识符的类型信息,到了中间代码生成阶段,我们就不再需要处理外部声明ExternalDeclaration了,只需要为语句和表达式生成相应的中间代码即可.文件ucl\tranexpr.c用于为表达式生成中间代码,文件名tranexpr是Transl

C编译器剖析_5.1 中间代码生成及优化_布尔表达式的翻译

5.2  中间代码生成与优化_布尔表达式的翻译 我们仍然按照语法分析和语义检查时的思路,先讨论表达式的翻译,再处理语句.表达式从概念上来说,可分为算术表达式和布尔表达式,在一些编程语言(例如Java)中对这两者是有严格区分的,算术表达式的结果是整数或浮点数,而布尔表达式的结果是逻辑上的真或假.布尔是英国数学家,由于布尔较早进行了关于"与或非"逻辑运算的研究,为了纪念这位先驱,在Java中引入了关键字boolean,而在C++中引入了bool关键字来表达逻辑上的真或假.C语言中并没有专门

C编译器剖析_6.3.6 汇编代码生成_为“取地址”产生汇编指令

6.3.6  为"取地址"产生汇编指令 在这一小节中,我们来讨论一下以下两条中间指令的翻译: (1)取地址指令<ADDR,DST,SRC1,NULL> 例如 <ADDR,t0,  number, NULL>,表示取number的地址并保存到临时变量t0中 (2)对象清零指令< CLR,DST,SRC1,NULL> 例如<CLR,arr,16,NULL>,表示把arr所占16字节的内存清零 我们先举一个例子来说明,对于图6.3.14第4行局

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

6.3.1  由中间指令产生汇编代码的主要流程 在这一小节,我们可把关注的焦点放在"如何把某条中间代码翻译成汇编代码"上.UCC编译器的中间代码是如下所示的四元式,包括运算符和3个操作数. <运算符opcode,目的操作数DST,源操作数SRC1,源操作数SRC2> 当然有些中间代码只需要用到opcode和DST就可以了,例如,无条件跳转指令"goto  BB2;"就不需要SRC1和SRC2.为了便于汇编代码的生成,UCC编译器在ucl\X86Linux

C编译器剖析_6.3.5 汇编代码生成_为类型转换产生汇编代码

6.3.5  为类型转换产生汇编代码 在这一小节中,我们来讨论一下整型和浮点型之间的类型转换.有些类型转换并不需要在汇编层次进行数据转换,例如int和unsigned  int之间的转换只是改变了表达式的类型,对数据本身并无影响,以下表达式"(unsigned int) a"对应的二进制数据为0xFFFFFFFF,而表达式"a"对应的二进制数据也为0xFFFFFFFF.但对相同内容的二进制数据来说,进行"有符号整数的右移"和"无符号整数