我们再分析条件表达式的翻译,以下给出了一段 C 程序及其对应的中间代码,
我们生成临时一个临时变量 t0 用来存放 b+2 或 b+3 的值,然后再把 t0 赋值给变量 c。
/************************************************************************
Syntax
conditional-expression:
logical-OR-expression
logical-OR-expression ? expression : conditional-expression
*************************************************************************/
static Symbol TranslateConditionalExpression(AstExpression expr)
{ //翻译c = a > 0? b+2: b+3;这样的语句,语法树为: ? a>0 (: b+2 b+3)
Symbol t, t1, t2;
BBlock trueBB, falseBB, nextBB;
t = NULL;
/**************************************************
int a ,b,c;
c = a > 0? b+2: b+3;
//对应的中间代码
if (a <= 0) goto BB7;
BB6: // trueBB
t1 : b + 2;
t0 = t1;
goto BB8;
BB7: // falseBB
t2 : b + 3;
t0 = t2;
BB8: // nextBB
c = t0;
***************************************************/
if (expr->ty->categ != VOID) //这个类型是表达式的类型,语法分析时候记录的是整个表达式结果值的类型
{
t = CreateTemp(expr->ty); //创建临时变量t
}
trueBB = CreateBBlock(); //创建3个块
falseBB = CreateBBlock();
nextBB = CreateBBlock();
// to be consistent with if(){}else{} 这儿又是套路TranslateBranchExpression()函数一样
TranslateBranch(Not(expr->kids[0]), falseBB,trueBB);
//产生if (a <= 0) goto BB7;指令。GenerateBranch(ty, trueBB, JZ, src1, NULL); “jz=jump if zero,即零标志为1就跳转,最终还是落实到了汇编自己判断这儿来了,因此中间代码翻译的目的就是把汇编期待的一切都给汇编准备好,汇编就直接生成即可
StartBBlock(trueBB);
t1 = TranslateExpression(expr->kids[1]->kids[0]); //翻译“b+2
if (t1 != NULL)
GenerateMove(expr->ty, t, t1); //产生 MOV 指令,“t0 = t1;”,
GenerateJump(nextBB); //产生无条件跳转指令“goto BB8;
StartBBlock(falseBB);
t2 = TranslateExpression(expr->kids[1]->kids[1]);
if (t2 != NULL)
GenerateMove(expr->ty, t, t2);
StartBBlock(nextBB);
return t;
}
由于局部变量的存储空间是运行时在栈中动态分配的,UCC 编译器需要在编译时产生
中间代码来实现对局部变量的初始化,而在对应的汇编代码中,由于无法预知相应的存储单
元,我们只能采用“ebp 寄存器+常量偏移”的模式来对其寻址。程序运行时,我们会在栈
中为被调用的函数分配一块内存,用于存放其形参、局部变量、函数返回地址等信息,这块
内存通常被称为“活动记录 activation record”或者“帧 frame”。大部分的 C 编译器,会使
用 x86 的 ebp 寄存器来指向当前函数的活动记录;而“常量偏移”则可由 C 编译器在编译
时,根据局部变量声明在函数中出现的先后顺序和所占内存大小来确定。(如果要生成调试信息给调试器用,那这些信息就很重要,调试时候能定位出变量的地址,从而查看变量地址)
对于以下局部数组 arr 的初始化,我们可以先把 arr 所占的栈空间清 0,然后再根据“初
值及其偏移”来产生初始化相应数组元素的指令。
int arr[8] = {10,20,30};
我们在语义检查时介绍过以下结构体 struct initData,其中的 offset 即可用于描述偏移,
而 expr 对应的表达式为初值,而 next 用于构造链表。
struct initData{
int offset; //偏移
AstExpression expr; //初值对应的表达式
InitData next;
};
typedef struct initData * InitData;
上述初值{10,20,30}经语义检查后,我们可得到以下链表,在中间代码生成阶段,我们
可由该链表产生“对相应数组元素进行初始化”的中间代码。
(表达式 10,偏移 0) --> (表达式 20,偏移 4) --> (表达式 30,偏移 8)
///对应的中间代码
arr : 40; //把 arr 所占 40 字节栈内存清 0
arr[0] = 10;
arr[4] = 20;
arr[8] = 30;
而到了汇编层次,局部变量的名字不再可用,我们改用“ebp 寄存器+常量偏移”的方
式来表示局部变量。对应的汇编代码如下所示:
pushl $40 //这个是把形参1入栈,需要传给memset函数
pushl $0 //这个是把形参2入栈,需要传给memset函数
leal -40(%ebp), %eax
pushl %eax //这个是把arr的基址拿到,形参3,需要传给memset函数
call memset //对数组 arr 清 0,相当于调用 memset(&arr[0], 0, 40);
addl $12, %esp
movl $10, -40(%ebp) //arr[0] = 10;
movl $20, -36(%ebp)
movl $30, -32(%ebp)
对于以下局部数组 num 来说,由于初值{1,2,3,4}覆盖了数组 num 所占的所有内存空间,
我们并不需要额外产生对数组 num 进行清 0 的指令。
int num[4] = {1,2,3,4};