代码生成
数组定义
数据的内存空间是连续的,因此无论数组的原型是几维的,都可以看做是一个一维的大数组。例如,对于一个数组 ,可看做是 。访问 ,就是访问 。
我们需要考虑定义数组时,是作为局部变量还是全局变量:
当某个局部变量定义为数组时,数组空间分配在栈上。这与普通变量的定义基本没区别,同样需要算出在栈帧上的偏移量,只不过大小不再是固定的 4 字节,而是数组的元素个数再乘 4 (字节/元素),因此计算其他变量的偏移量时也要做相应修改。由于不支持数组的初始化,分配完栈帧后不用管它就行,即使这段栈空间保留了之前变量的信息,因此数组还有其他未初始化的局部变量的初值都是不确定的。
当某个全局变量定义为数组时,数组空间分配在程序的数据段。同样我们不考虑 BSS 段,直接将其放到 .data 中。作为全部变量时,数组中每个元素默认初始化为 0,可用如下汇编码来将连续一段内存初始化为 0:
.align 2
a:
.zero 400
其中 .zero
后面的数字即这段内存的大小(单位字节)。上述汇编码可由定义在全局的 int a[100];
或 int a[10][10];
生成。
在 RISC-V 指令集中,
addi
、lw
、sw
等 I 型和 S 型指令的立即数大小只有 12 位,因此我们不能定义一个很大的数组作为局部变量,否则那些分配栈帧、Load/Store 局部变量的指令的偏移量就会超出 12 位。你可以实现对这种情况的特殊处理,使用其他指令进行代替,不过我们的测试集保证了不会出现这种情况。
下标运算
指针下标
我们允许对指针和数组类型进行下标运算。首先来看指针的下标运算。指针的下标运算非常类似于解引用运算 *
,只是需要加上一个偏移,大小为下标乘上指针基类型的大小,即 p[i]
等价于 *(p + i * size)
。另外需要注意,我们在 lab11 中提到了左值的概念,*
运算后的结果可以是左值,类似地,指针的下标运算也可以是左值:
int *p;
p[0] = 1; // p[0] 是左值
int a = p[1]; // p[1] 是右值
int *q = &p[2]; // p[2] 是左值
于是,我们按照实现解引用运算的方法,即可实现指针的下标运算。
数组下标
然后再来看数组的下标运算。数组的下标运算可以分为两类:
- 当下标运算的次数等于数组的维度时,结果是数组中的元素。这种情况与指针的下标运算类似,可以作为右值,结果是数组中元素的值;也可以作为左值,结果是数组中元素的地址。
- 当下标运算的次数小于数组的维度时,结果是一个数组。由于任何数组可看做一个一维大数组,此时的结果也可以看做是原数组中的一个子数组。当之后对该结果进行转指针、取下标等运算时,都是相对于这个子数组来说的。之后要取子数组中的哪个元素现在还是未知的,因此无论之后是否有可能成为左值,这一步都应该返回子数组的地址。
对于第 1 种情况,与指针的下标运算类似处理;对于第 2 种情况,关键是要求出子数组的地址。由于下标运算在文法中是递归定义的,我们可以自然地得出地址的计算方法:
- 对于数组类型的变量
Ident
,地址即Ident
在栈或数据段的地址; - 对于子数组
array[i]
,地址是array
的地址加上i * sizeof(array[i])
。
例如,对于数组 int a[3][4][5]
,有:
a[i]
的地址是a + (i * 4 * 5) * sizeof(int)
;a[i][j]
的地址是a + [(i * 4 * 5) + (j * 5)] * sizeof(int)
;a[i][j][k]
的地址是a + [(i * 4 * 5) + (j * 5) + k] * sizeof(int)
。
总结一下,无论是指针还是数组,无论是左值还是右值,我们都可以用同一套代码框架来生成汇编码:
function visitSubscript(ctx) { // <postfix> ::= <postfix> '[' expr ']'
visitPostfix(postfix); // 计算要取下标的表达式的值,设结果保存在 t0
visitExpr(expr); // 计算下标的值,设结果保存在 t1
emitMul("t1", "t1", ctx.type.size); // t1 = t1 * sizeof(结果的类型)
emitAdd("t0", "t0", "t1"); // t0 = t0 + t1,此时的结果即下标运算后子数组或元素的地址
if (ctx.type == Array || ctx.isLValue) {
// do nothing // 如果结果是数组类型,或是左值,直接返回其地址
} else {
emitLoad(); // 否则,再生成一条 Load 指令来取出该地址中保存的值
}
}
function visitPrimary(ctx) {
if (ctx ::= Ident) { // <primary> ::= Identifier
emitAddress(Ident); // 计算变量 Ident 的地址
if (ctx.type == Array || ctx.isLValue) {
// do nothing // 如果变量是数组类型,或是左值,直接返回其地址
} else {
emitLoad(); // 否则,再生成一条 Load 指令来取得变量的值
}
} else {
// ...
}
}
指针算术运算
这一步非常简单,我们要支持的所有指针算术运算就这么多:
- 指针加整数,或整数加指针:只需增加一步,将整数操作数乘上指针基类型的大小。
- 指针减整数:与指针加法一样,只需增加一步,将整数操作数乘上指针基类型的大小。
- 指针减指针:先直接做减法,然后除以指针基类型的大小(要求两指针基类型相同)。
由于指针的基类型只能是
int
或指针,其大小都是 4 字节,你可以用slli
、srai
指令进行左移、右移来代替乘除运算。