ucc编译器分析与总结(5) 中间代码生成

本文深入探讨编译原理中的中间代码生成阶段,包括基本框架、表达式与语句的处理,以及临时变量、短路运算等高级特性,旨在解析编译器如何将高级语言转化为中间形式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.基本框架

经过语义检查后,现在已经有了包含符号和类型的完整语法树,现在要做的事情就是继续沿着语法树走一遍,对每条语句和表达式生成相应的中间代码。

中间代码以三地址码的形式表示,由两个源操作数,一个目的操作数和一个运算符组成。然后ucc在生成中间代码时以基本块为单位,每个基本块包含若干条中间代码,基本块的开头都会有类似BB1:这样的标签。

从静态上来看,所有基本块按顺序通过双向链表连接在一起,但从动态上来看,代码并非沿着基本块从上到下的执行,这里存在一些跳转,所以存在控制流转移的两个基本块间会生成一条有向边,一个结点可以有多条有向边对应多个源结点或目的结点,这样所有基本块和所有有向边就组成了一个控制流图,下图很直观地表达了基本块的静态结构和动态结构:

在这里插入图片描述

现在再来看基本块的结构体

struct bblock
{
    struct bblock *prev;
    struct bblock *next;
    Symbol sym;
    // successors
    CFGEdge succs;
    // predecessors
    CFGEdge preds;
    struct irinst insth;
    // number of instructions
    int ninst;
    // number of successors
    int nsucc;
    // number of predecessors
    int npred;
    int ref;
};

其中perv和next分别是基本块作为静态结构时,双向链表的前一个结点和后一个结点。insth存储的是中间代码,多条中间代码也是通过双向链表来连接的。succs和preds存储的是当前基本块在控制流图的后驱结点和前驱结点。控制流图的结构体如下:

typedef struct cfgedge
{
    BBlock bb;
    struct cfgedge *next;
}*CFGEdge;

由于前驱结点和后驱结点不止一个,我们看到结构体中还有一个next成员把多个前驱结点或后驱结点串联在一起,现在来看一下生成有向边的代码:

static void AddSuccessor(BBlock bb, BBlock s)
{
    CFGEdge e;

    ALLOC(e);
    e->bb = s;
    e->next = bb->succs;
    bb->succs = e;
    bb->nsucc++;
}

void DrawCFGEdge(BBlock head, BBlock tail)
{
    AddSuccessor(head, tail);
    AddPredecessor(tail, head);
}

现在要生成一条有向边,在head结点中添加后继结点,在tail结点中添加前驱结点。在AddSuccessor函数中先新建一条有向边,并把后继结点赋值给e->bb,然后当前结点可能有多个后继结点,通过e->next = bb->succs;把新的后继结点加入到链表,然后更改后继结点的链表表头,最后把后继结点的数量加一,AddPredecessor函数也是类似的。

解析时CurrentBB代表当前中间代码生成后存储的基本块,通过StartBBlock函数更改当前基本块。

2.表达式的中间代码

这部分内容很多而且繁琐,就不一一细说了,挑几个有代表性的地方说一下,对应的代码在tranexpr.c文件。

2.1 TranslatePrimaryExpression

函数最后返回expr->val.p,这是一个Symbol类型,由语义检查的函数CheckPrimaryExpression可知expr->val.p存储的确实是基本结点对应的符号。

2.2 位域的读写

先来看ReadBitField函数

static Symbol ReadBitField(Field fld, Symbol p)
{
    int size = 8 * fld->ty->size;
    int lbits = size - (fld->bits + fld->pos);
    int rbits = size - (fld->bits);

    p = Simplify(T(INT), LSH, p, IntConstant(lbits));
    p = Simplify(GetBitFieldType(fld), RSH, p, IntConstant(rbits));

    return p;
}

现在要读如下结构体体中的位域b

typedef struct aa{
    int a:8;
    int b:19;
    int c:5;
}AA;

整个结构体的内存分配如下图
在这里插入图片描述
b对应的是蓝色区域,size为类型所占的bit数,lbits为c所占的bit数为5,rbits为a和c的总bit长度13,然后想要读取b的内容,先左移lbits位把c挤掉(要注意这里左移不是指图片中的方向),然后再右移rbits把b的起始地址移到开头。Simplify可以简单看做调用TryAddValue添加一条中间代码,只不过多了一些优化部分,TryAddValue还涉及到临时变量,将在后面分析。k = sad.b;生成的中间代码如下:

	t22 : sad[0] << 5;
	t23 : t22 >> 13;
	k = t23;

现在来看写位域WriteBitField函数的代码,代码关键部分如下:


static Symbol WriteBitField(Field fld, Symbol dst, Symbol src)
{
    int fmask = (1 << fld->bits) - 1;
    int mask = fmask << fld->pos;
    Symbol p;

    //... ...
    //源操作数左移8位
    src = Simplify(T(INT), LSH, src, IntConstant(fld->pos));
    //将所有非掩码区清0,即移位后的源操作数在蓝色部分清0
    src = Simplify(T(INT), BAND, src, IntConstant(mask));
    //将目的操作数在蓝色区域清0,生成一个临时变量
    p = Simplify(T(INT), BAND, dst, IntConstant(~mask));
    //将临时变量与源操作数或运算,相当于把源操作数的值放在蓝色区域
    p = Simplify(T(INT), BOR, p, src);
    //把临时变量p赋值给目的操作数
    GenerateMove(fld->ty, dst, p);

    //... ...
}

现在以代码sad.b = k;为例,生成的中间代码如下:

	t16 : k << 8;
	t17 : t16 & 134217472;  这里是2^27-2^8
	t18 : sad[0] & -134217473;
	t19 : t18 | t17;
	sad[0] = t19;

2.3 临时变量

在符号的类型中专门定义了临时变量的类型,与之对应的是ValueDef结构体,存储了二元运算表达式中的源操作数和目的操作数,临时变量的用处是在源操作数没有更改时可以直接拿来用而不必再算一次,更多这部分内容的介绍可以看《C编译器剖析》的2.5节。

下面来解读TryAddValue的代码,详见注释

Symbol TryAddValue(Type ty, int op, Symbol src1, Symbol src2)
{
    int h = ((unsigned)src1 + (unsigned)src2 + op) & 15;
    //通过hash表找到存储临时变量表达式的桶
    ValueDef def = FSYM->valNumTable[h];
    Symbol t;
 
    //查找该表达式是否存在对应的临时变量  
    while (def)
    {
        if (def->op == op && (def->src1 == src1 && def->src2 == src2))
            break;
        def = def->link;
    }
    //如果存在,就不必计算,直接用def->dst
    if (def && def->ownBB == CurrentBB && def->dst != NULL)
        return def->dst;

new_temp:
    //新建临时变量,生成赋值语句
    t = CreateTemp(ty);
    GenerateAssign(ty, t, op, src1, src2);
    //然后把临时变量的def记录到符号表里
    def = AsVar(t)->def;
    def->link = FSYM->valNumTable[h];
    FSYM->valNumTable[h] = def;
    return t;
}

2.4 短路运算

对于形如c = a||b或 c = a&&b的表达式并非对右边先求值,然后再赋值给左边,而是先计算a的值,如果这时已经能确定整个表达式的值,那么b就没有必要继续计算。代码的实现在TranslateBranchExpression函数里,源代码如下

static Symbol TranslateBranchExpression(AstExpression expr)
{
    BBlock nextBB, trueBB, falseBB;
    Symbol t;

    t = CreateTemp(expr->ty);
    nextBB = CreateBBlock();
    trueBB = CreateBBlock();
    falseBB = CreateBBlock();

    TranslateBranch(expr, trueBB, falseBB);
    StartBBlock(falseBB);    
    GenerateMove(expr->ty, t, IntConstant(0));    
    GenerateJump(nextBB);
    StartBBlock(trueBB);
    GenerateMove(expr->ty, t, IntConstant(1));
    StartBBlock(nextBB);

    return t;
}

现在以k = k||2代码举例来说明,在生成赋值表达式TranslateAssignmentExpression的函数里解析 k||2时会调用TranslateBranchExpression函数,在TranslateBranch函数中会调用如下代码

case OP_OR:
    rtestBB = CreateBBlock();
    //生成中间代码,如果k非0则生成跳往trueBB
    TranslateBranch(expr->kids[0], trueBB, rtestBB);
    //现在进入rtestBB基本块
    StartBBlock( rtestBB);
    //生成中间代码,如果2为非0,则跳向trueBB
    TranslateBranch(expr->kids[1], trueBB, falseBB);
    break;
default:
    src1 = TranslateExpression(expr);
    GenerateBranch(ty, trueBB, JNZ, src1, NULL);

TranslateBranch返回后则进入falseBB基本块,这时生成的代码意味着 k||2表达式左右两边都为0,此时把0赋值给k跳向nextBB基本块,接着生成在falseBB和nextBB之间生成trueBB的代码,以下是k = k||2对应生成的中间代码

	if (k) goto BB3;
BB1:    // rtestBB
 	goto BB3;
 	//这里本该是falseBB,优化之后BB2并没有显示出来
	k = 0;
	goto BB4;
BB3: //trueBB
 	k = 1;
BB4://nextBB
 	...

2.5 偏移地址

在处理数组相关代码时要计算偏移地址,主要在 TranslateArrayIndex和Offset函数里完成

static Symbol TranslateArrayIndex(AstExpression expr)
{
    do
    {
        if (p->kids[1]->op == OP_CONST)
        {
            coff += p->kids[1]->val.i[0];
        }
        else if (voff == NULL)
        {
            voff = TranslateExpression(p->kids[1]);
        }
        else
        {
            voff = Simplify(voff->ty, ADD, voff, TranslateExpression(p->kids[1]));
        }
        p = p->kids[0];
        // see  case OP_DEREF in CheckUnaryExpression()
    }while (p->op == OP_INDEX);
    
    addr = TranslateExpression(p);
    dst = Offset(expr->ty, addr, voff, coff);
    return expr->isarray ? AddressOf(dst) : dst;
}
static Symbol Offset(Type ty, Symbol addr, Symbol voff, int coff)
{
    if (voff != NULL)
    {
        voff = Simplify(T(POINTER), ADD, voff, IntConstant(coff));
        addr = Simplify(T(POINTER), ADD, addr, voff);
        return Deref(ty, addr);
    }
    ... ...
}

以int a[5][3]; a[k][2] = 7;为例,首先会遍历该数组元素的语法树,得到voff为k12和coff为8,然后addr获取的是a的地址,在offset函数里把把12k加上8得到t9,得到最终地址t10,最终取出地址中的存放内容由Deref完成,生成的中间代码如下

t7 : k * 12;
t8 :&a;
t9 : t7 + 8;
t10 : t8 + t9;
*t10 = 7;

2.6 函数调用

函数调用的生成代码为GenerateFunctionCall() 函数,生成中间代码的运算符为CALL,返回值为recv、函数名faddr,函数参数args

inst->opcode = CALL;
inst->opds[0] = recv;
inst->opds[1] = faddr;
inst->opds[2] = (Symbol)args;
AppendInst(inst);

2.7 类型转换

在ucc中把每一种格式转换都定义成了一个运算符,以enum OPCode的形式表示,例如int转float的运算符为CVTI4F4,
OPCODE(CVTI4F4, “(float)(int)”, Cast)
根据表达式得到opcode的值,然后生成强制转换运算的中间代码,把src赋值dst

dst = CreateTemp(ty);	
GenerateAssign(ty, dst, opcode, src, NULL);
return dst;

3.语句的中间代码

3.1 if语句的生成

代码如下

nextBB = CreateBBlock();
trueBB = CreateBBlock();
falseBB = CreateBBlock();

//这里第3个参数trueBB没用
TranslateBranch(Not(ifStmt->expr), falseBB, trueBB);

StartBBlock(trueBB);
TranslateStatement(ifStmt->thenStmt);
GenerateJump(nextBB);

StartBBlock(falseBB);
TranslateStatement(ifStmt->elseStmt);
StartBBlock(nextBB);

if语句的生成还是很好理解的,在当前代码块,如果ifStmt->expr不满足则跳转到falseBB,否则继续往下执行,这时应该是满足的,所以生成trueBB的代码,执行完生成跳转到nextBB的代码,接着生成falseBB的代码,最后生成nextBB的代码。

3.2 for语句的生成

for语句不难理解,说明见代码注释

static void TranslateForStatement(AstStatement stmt)
{
    AstForStatement forStmt = AsFor(stmt);

    forStmt->loopBB = CreateBBlock();
    forStmt->contBB = CreateBBlock();
    forStmt->testBB = CreateBBlock();
    forStmt->nextBB = CreateBBlock();

    //for循环初始化
    if (forStmt->initExpr)
    {
        TranslateExpression(forStmt->initExpr);
    }
    //跳到for循环终止判断
    GenerateJump(forStmt->testBB);
    //生成循环体代码
    StartBBlock(forStmt->loopBB);
    TranslateStatement(forStmt->stmt);
    //生成计数值代码
    StartBBlock(forStmt->contBB);
    if (forStmt->incrExpr)
    {
        TranslateExpression(forStmt->incrExpr);
    }
    //生成循环终止判断代码
    StartBBlock(forStmt->testBB);
    //如果循环终止,则跳到nextBB,否则跳到loopBB
    if (forStmt->expr)
    {
        TranslateBranch(forStmt->expr, forStmt->loopBB, forStmt->nextBB);
    }
    else
    {
        GenerateJump(forStmt->loopBB);
    }

    StartBBlock(forStmt->nextBB);
}

接下来如果碰到break代码,先从栈中去除break对应的for循环的语法树结点brkStmt->target,然后生成跳转到nextBB的代码,GenerateJump(AsLoop(brkStmt->target)->nextBB);,即跳出for循环

3.3 switch语句的生成

switch语句是所有语句生成当中最复杂的,首先获得switch判断表达式的sym,然后遍历每一条case语句,将其插入到桶的数据结构。跳转到case语句使用的是跳转表技术,把case语句的block组成一张表,根据case值获得表的偏移地址,然后直接跳转到该地址,这个只适用于case值密度较高时的情况,例如case是1、3、4、200的情况,这样使用跳转表就会浪费大量空间,所以将密度较高的划分在一个桶里,即1、3、4组成一个桶,200组成一个桶。

if (bucket && (bucket->ncase + 1) * 2 > (val - bucket->minVal))

在case的数量*2大于最大值和最小值,说明密度较高,把case插入到桶里,否则新建一个桶,case的值是按从小到大的顺序排列的,当case的数理增大时,大于当前桶的最大值和上一个桶的最小值,这时调用MergeSwitchBucket与上一个桶合并

接下来就是调用TranslateSwitchBuckets检查每一个桶生成中间代码。首先从中间的那个桶开始,把桶里的所有case基本块放在dstBBs里,然后根据choice也就是传进来的switch判断表达式的sym值确认choice是否在这个桶的取值范围,如果不在则生成跳转代码,如果在则生成跳转表代码。接下来通过递归生成其他桶的跳转代码。

先在来举个例子,如下的switch代码

 switch(k+1){
 case 5:
 case 2:
    k++;
    p++;
    break;
 case 1:
    k++;
    break;
  case 201:
    k += 5;
    break;
  case 203:
    k += 7;
    break;
 defalut:
    break;
 }

生成的中间代码如下:

	t29 : k + 1;
	if (t29 < 1) goto BB22;
BB13:
 	if (t29 > 5) goto BB15;
BB14:
 	t30 : t29 - 1;//第一个桶
	goto (BB19,BB18,BB22,BB22,BB18,)[t30];
BB15:
 	if (t29 < 201) goto BB22;
BB16:
 	if (t29 > 203) goto BB22;
BB17:
 	t31 : t29 - 201;//第二个桶
	goto (BB20,BB22,BB21,)[t31];
BB18:
 	++k;
	t35 : p + 4;
	p = t35;
	goto BB22;
BB19:
 	++k;
	goto BB22;
BB20:
 	t38 : k + 5;
	k = t38;
	goto BB22;
BB21:
 	t39 : k + 7;
	k = t39;
BB22:
Compiler Construction Experiment 1 Implementing a Scanner for TINY+ You are to write a lexical analyzer/scanner for the language TINY+. Goals 1The input of the scanner is a source code file and the output of the scanner is a stream of tokens. 2Your scanner should go for longest possible match i.e. a string ‘:=’ is to be identified as ‘ass-symbol’ rather than ‘:’ and ‘=’. 3Token is represented as (Kind, Value). We use the following symbols to denote different kinds of tokens KEY denotes reserved words SYM denotes special symbols ID denotes identifiers NUM denotes numeric constants STR denotes string constants 4Check lexical errors: giving meaning error messages and the lines where errors occur. The kinds of lexical errors are: Illegal character, that is, scanner may recognize a character that is not in the alphabet of TINY+, such as $ is an illegal character The right bracket of a STRING is lost, such as ' scanner The right delimiter of a comment is lost, such as: {this is an example Requirements 1Write your program in C or C++ 2This experiment must be finished in 4 periods. You will submit a report and the source code Example output for some TINY+ programs Test1 or and int bool char while do if then else end repeat until read write , ; := + - * / ( ) = a2c 123 'EFG' The scanner should give the outputs: (KEY, or) (KEY, and) (KEY, int) (KEY, bool) (KEY, char) (KEY, while) (KEY, do) (KEY, if) (KEY, then) (KEY, else) (KEY, end) (KEY, repeat) (KEY, until) (KEY, read) (KEY, write) (SYM, ,) (SYM, ;) (SYM, :=) (SYM, +) (SYM, -) (SYM, *) (SYM, /) (SYM, ( ) (SYM, )) (SYM, ) (SYM, =) (ID, a2c) (NUM, 123) (STR, EFG) Test2 {this is an example} int A,B; bool C1, C2, C3; char D; D:= 'scanner'; while A<=B do A:=A*2 end The scanner should give the outputs: (KEY, int) (ID, A) (SYM, ,) (ID, B) (SYM, ;) (KEY, bool) (ID, C1) (SYM, ,) (ID, C2) (SYM, ,) (ID, C3) (SYM, ;) (KEY, char) (ID, D) (SYM, ;) (ID, D) (SYM, :=) (STR, scanner) (SYM, ;) (KEY, while) (ID, A) (SYM, <=) (ID, B) (KEY, do) (ID, A) (SYM, :=) (ID, A) (SYM, *) (NUM, 2) (KEY, end)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值