C编译器学习笔记(五)从语法树到汇编

结合文法,相信很容易看到语法树的代码。但接下来的代码,因为没有类似文法一样的东西指导我们,代码量多起来就会迷糊。我们要着眼于数据,分析语法树中的数据流向了哪里,这样可以在大量的代码中抓住要点。

语义检查

检查语法树,构造出ty这个数据结构。

CheckDeclarationSpecifiers(func->specs);
CheckDeclarator(func->dec);
ty = DeriveType(func->dec->tyDrvList, func->specs->ty,&func->coord);

在函数CheckDeclarator中,会将函数的参数放在funcDec->sig->params中。

如果函数之前没被定义过,就添加到符号表中

func->fsym = (FunctionSymbol)AddFunction(func->dec->id, ty, sclass,&func->coord);

接下来的一堆代码

RestoreParameterListTable();
{
    Vector v= ((FunctionType)ty)->sig->params;
    
    FOR_EACH_ITEM(Parameter, param, v)
        AddVariable(param->id, param->ty, param->reg ? TK_REGISTER : TK_AUTO,&func->coord);
    ENDFOR

    FSYM->locals = NULL;
    FSYM->lastv = &FSYM->locals;
}

这是处理那种返回值比较复杂的情况用的,例如void (*h(int a1,int a2))(void){} 这种返回函数指针的情况。暂且忽略。

代码

CheckCompoundStatement(func->stmt);

语句检查将局部作用域里面的声明加入对应作用域的符号表,然后检查语句是否有语义错误。

我们发现语义检查除了检查语义错误外,最大的作用是将声明加入到符号表中。

image

params 记录了函数形参,locals 用于记录局部变量,valNumTable[16]则是一个哈希表,用于快速查找形如“a+b”的公共子表达式。

由于同一个函数名或变量名可以在不同作用域中被多次声明,我们需要把同一个符号加入到不同的符号表中,为此,我们引入结构体 bucketLinker,我们往Table中添加的是 bucketLinker 对象,该对象的 sym 域指向了对应的符号。

每一个作用域都有一个符号表,这些符号表通过outer相连:

image

生成中间代码

声明部分在语义检查过程中添加到符号表中了。

语句和表达式将会变成由BBlock组成的控制流图,BBlock内部的语句都是顺序执行的,而分支、跳转都是在BBlock之间来回跳转。

image

bblock中有一个链表,里面都是irinst结构体,里面有对应的指令的三地址码。

临时变量也是在这一步创建的。
分支语句的跳转指令是在这一步生成的。

中间代码到汇编

中间代码与汇编相比,还有哪些差别呢?

那么为每条伪指令都写一个函数,逐条翻译成汇编指令吗?大可不必。因为很多指令有相同的地方,我们可分为15类,每一类用一个函数来处理即可。

EmitMove
EmitAssign
EmitBranch
EmitJump
EmitIndirectJump
EmitInc
EmitDec
EmitAddress
EmitDeref
EmitCast
EmitIndirectMove
EmitCall
EmitReturn

大多数比较简单,但EmitCall、EmitReturn相对比较复杂,因为函数调用与返回涉及寄存器的保存和恢复。这在后面单独介绍。

posted @ 2022-12-14 23:40:16
评论加载中...
发表评论