上一节中,我们已经实现了逻辑运算和负数,在本节中,我们将要实现比较难的局部变量。
现在,我们所有的变量对所有函数都是全局可见的。我们想添加一个本地范围对于变量,这样每个函数都有自己的变量,不能被其他函数看到。此外,在递归函数的情况下,同一个函数的每个实例都有自己的局部变量。
要为同一函数的多个实例创建本地作用域,为了提供一个地方来存储函数的参数,我们需要一个堆。
让我们从局部变量和全局变量之间的区别开始。全局变量必须对所有函数可见,但局部变量只是对一个函数可见。
SubC使用一个符号表来存储有关本地和全局变量。全局变量在一端分配,局部变量存储在另一个。有代码来确保有中间两端没有碰撞。我们可以借鉴这一点。
在将局部符号优先于全局符号方面,我们可以首先搜索符号表的本地端,如果我们没有找到符号,然后我们可以搜索全局端。而且,一旦我们完成解析一个函数,我们可以简单地擦除符号表的本地端。
我们将如何确定参数或局部变量在堆栈中的位置,一旦它们被复制或放置在那里?为此,我将添加一个 posn
字段到每个本地符号表条目。这将指示变量距离栈基指针的偏移地址。
修改符号表
为了区分全局变量和局部变量,我们修改局部变量结构体。
// 变量类型
enum
{
C_GLOBAL = 1, // 全局变量
C_LOCAL // 局部变量
};
// 符号表结构体
struct symbol_table
{
char *name; // 符号名
int type; // 类型void或int
int stype; // 结构类型
int endlabel; // 函数的结束标签
int size; // 数组大小
int class; // 符号类别
int posn; // 距离栈基指针的偏移地址
};
我们添加了class
和posn
字段。如上一部分所述,posn
为负数,并保存与堆栈基指针的偏移量,即局部变量存储在堆栈中。
在这部分,我只实现了局部变量,没有实现参数。另请注意,我们现在有标记为C_GLOBAL或C_LOCAL的符号。
因此,符号表的名称也发生了变化,其中的索引改变了
int Globals = 0; // 下一个空闲全局变量槽的位置
int Locals = SYMBOL_NUM - 1; // 下一个空闲局部变量槽的位置
从视觉上看,全局符号存储在符号表的左侧,“Globs”指向下一个空闲全局符号槽,而“Locls”指向下一个空闲局部符号槽。
0xxxx......................................xxxxxxxxxxxxNSYMBOLS-1
^ ^
| |
Globals Locals
我们现在有的find_global()
和new_global()
函数,用于查找或分配全局符号,我们现在新增find_local()
和new_local()
函数,来查找或分配局部符号。他们有代码来检测Locals
和Globals
之间的冲突。
// 检查符号s是否在全局符号表中。
// 返回其插槽位置或-1
int find_global(char *s)
{
int i;
for (i = 0; i < Globals; i++)
{
if (*s == *Tsym[i].name && !strcmp(s, Tsym[i].name))
return i;
}
return -1;
}
// 获取新的全局符号槽的位置
int new_global()
{
int p;
if ((p = Globals++) >= Locals)
{
fprintf(stderr, "Too many global symbols on line %d\n", Line);
exit(1);
}
return p;
}
// 确定符号s是否在本地符号表中,
// 返回其插槽位置或-1
int find_local(char *s)
{
int i;
for (i = Locals + 1; i < SYMBOL_NUM; i++)
{
if (*s == *Tsym[i].name && !strcmp(s, Tsym[i].name))
return i;
}
return -1;
}
// 获取新的本地符号槽的位置,
// 否则位置用完
int new_local()
{
int p;
if ((p = Locals--) <= Globals)
{
fprintf(stderr, "Too many local symbols on line %d\n", Line);
exit(1);
}
return p;
}
现在有一个通用函数update_sym()
来设置符号表条目中的所有字段。update_sym()
函数由add_global()
和add_local()
调用。这些首先尝试查找现有符号,如果找不到则分配一个新符号,然后调用update_sym()
来设置该符号的值。最后,有一个新函数find_symbol()
,它在符号表的局部和全局部分搜索符号。
// 更新符号表
void update_sym(int slot, char *name, int type, int stype, int class, int endlabel, int size, int posn)
{
if (slot < 0 || slot >= SYMBOL_NUM)
{
fprintf(stderr, "Invalid symbol slot number in update_sym() on line %d\n", Line);
exit(1);
}
Tsym[slot].name = strdup(name);
Tsym[slot].type = type;
Tsym[slot].stype = stype;
Tsym[slot].class = class;
Tsym[slot].endlabel = endlabel;
Tsym[slot].size = size;
Tsym[slot].posn = posn;
}
// 将全局变量添加到符号表,并返回符号表中的位置
int add_global(char *name, int type, int stype, int endlabel, int size)
{
int y;
// 如果已经在符号表中,则返回现有位置
if ((y = find_global(name)) != -1)
return y;
// 获得一个新的位置,并填入信息和返回位置
y = new_global();
update_sym(y, name, type, stype, C_GLOBAL, endlabel, size, 0);
return y;
}
// 将局部变量添加到符号表,并返回符号表中的位置
int add_local(char *name, int type, int stype, int endlabel, int size)
{
int y, posn;
// 如果已经在符号表中,则返回现有位置
if ((y = find_local(name)) != -1)
return y;
// 获得一个新的位置,并填入信息和返回位置
y = new_local();
posn = arm_get_localoffset(type, 0);
update_sym(y, name, type, stype, C_LOCAL, endlabel, size, posn);
return y;
}
// 确定符号s是否在符号表中,
// 返回其插槽位置或-1
int find_symbol(char *s)
{
int y;
y = find_local(s);
if (y == -1)
y = find_global(s);
return y;
}
在调用代码中,用find_symbol()
函数代替find_global()
。
修改语法分析
我们需要能够解析全局和局部变量声明。 解析它们的代码(目前)是相同的,所以我在函数中添加了一个标志。
// 分析变量声明
void var_declaration(int type, int islocal)
{
/*......*/
while(1)
{
// 现在是标识符,如果下一个标记是"["
if (Token.token == T_LBRACKET)
{
/*......*/
if (Token.token == T_INT)
{
// 将此添加为已知数组并在汇编中生成其空间
if (islocal)
{
id = add_local(Text, type, S_ARRAY, 0, Token.intvalue);
}
else
{
id = add_global(Text, type, S_ARRAY, 0, Token.intvalue);
}
arm_global_sym(id);
}
/*......*/
}
else
{
// 将其添加为已知标量并在汇编中生成其空间
if (islocal)
{
id = add_local(Text, type, S_VARIABLE, 0, 1);
}
else
{
id = add_global(Text, type, S_VARIABLE, 0, 1);
}
// 如果下一个是"=",为变量赋初值
/*......*/
}
/*......*/
}
}
目前在我们的编译器中有两次对 var_declaration()
的调用。一个在global_declarations()
中的解析全局变量声明,修改为以下代码。
var_declaration(type, 0);
另一个在single_statement()
中的这个解析局部变量声明,修改为以下代码:
var_declaration(type, 1);
此外,还要修改前面的global类函数为对应的global类或local类函数之一,这个比较麻烦,有些代码还要稍微做适应性调整,这里不再详细展开。
修改汇编代码
对于每个局部变量,我们需要为其分配一个位置并将其记录在符号表的“posn”字段中。为此,我们有新的变量和两个操作他的函数。
// 下一个局部变量相对于堆栈基指针的位置,
// 将偏移量存储为正数,
// 以便更容易地对齐堆栈指针
int localOffset;
int stackOffset;
// 解析新函数时重置新局部变量的位置
void arm_reset_locals()
{
localOffset = 0;
}
// 获取下一个局部变量的位置,
// 使用size来分配参数
int arm_get_localoffset(int type, int size)
{
// 现在只需将偏移量减少至少4个字节并在堆栈上分配
localOffset += 4;
return -localOffset;
}
现在,我们在堆栈上分配所有局部变量。它们与每个字节之间至少有4个字节对齐。
一旦我们将函数的名称添加到符号表中,但在我们开始解析局部变量声明之前,在function_declaration()
中调用 arm_reset_locals()
。 这将“localOffset”设置回零。
接下来,我们要载入变量,全局变量从数据段载入,局部变量从堆栈载入。
// 确定变量与.L2标签的偏移量
void set_varglobal_offset(int id)
{
int offset = 0;
for (int i = 0; i < id; i++)
{
if (Tsym[i].stype == S_VARIABLE && Tsym[i].class == C_GLOBAL) offset += 4;
}
fprintf(Outfile, "\tldr\tr3, .L2+%d\n", offset);
}
// 将变量中的值加载到寄存器中,并返回寄存器编号
int arm_load_global(int id)
{
// 获得一个新的寄存器
int r = arm_alloc_register();
// 获得变量偏移地址
set_varglobal_offset(id);
fprintf(Outfile, "\tldr\tr%d, [r3]\n", r);
return r;
}
// 确定变量与堆栈基址指针的偏移量
void set_varlocal_offset(int id)
{
int offset = 0;
for (int i = 0; i <= id; i++)
{
if (Tsym[i].stype == S_VARIABLE && Tsym[i].class == C_LOCAL) offset += Tsym[i].size * 4;
}
fprintf(Outfile, "\tsub\tr3, fp, #-%d\n", offset + 4);
}
// 将变量中的值加载到寄存器中,并返回寄存器编号
int arm_load_local(int id)
{
// 获得一个新的寄存器
int r = arm_alloc_register();
// 获得变量偏移地址
set_varlocal_offset(id);
fprintf(Outfile, "\tldr\tr%d, [r3]\n", r);
return r;
}
// 将变量中的值加载到寄存器中,并返回寄存器编号
int arm_load_var(int id)
{
// 获得一个新的寄存器
int r = arm_alloc_register();
// 获得变量偏移地址
if(Tsym[id].class == C_GLOBAL)
{
set_varglobal_offset(id);
}
else if(Tsym[id].class == C_LOCAL)
{
set_varlocal_offset(id);
}
else
{
fprintf(stderr, "Bad function type in arm_stor_global():%d on line %d\n", Line);
exit(1);
}
fprintf(Outfile, "\tldr\tr%d, [r3]\n", r);
return r;
}
用arm_load_var()
函数代替原来的arm_load_global()
函数。对应调用set_var_offset()
函数的函数也要作出修改,用以下代码代替set_var_offset(id);
的语句。
if(Tsym[id].class == C_GLOBAL)
{
set_varglobal_offset(id);
}
else if(Tsym[id].class == C_LOCAL)
{
set_varlocal_offset(id);
}
else
{
fprintf(stderr, "Bad function type in arm_stor_global():%d on line %d\n", Line);
exit(1);
}
}
现在我们正在使用堆栈上的位置,我们最好将堆栈指针向下移动到保存局部变量的区域下方。因此,我们需要修改函数前导码和后导码中的堆栈指针。
// 生成函数前言
void arm_function_preamble(char *name, int varlocalnum)
{
stackOffset = varlocalnum * 4 + 4;
fprintf(Outfile,
"\t.text\n"
"\t.align 2\n"
"\t.globl\t%s\n"
"\t.type\t%s, %%function\n"
"%s:\n"
"\tpush {fp, lr}\n"
"\tadd fp, sp, #4\n"
"\tsub\tsp, sp, #%d\n", name, name, name, stackOffset);
}
// 生成函数结尾
void arm_function_postamble(int id)
{
arm_label(Tsym[id].endlabel);
fputs("\tsub\tsp, fp, #4\n" "\tpop\t{fp, pc}\n" "\t.align\t2\n", Outfile);
}
这里的varlocalnum
变量表示函数的传入参数,暂时我们只考虑传入一个参数。
测试结果
输入:
int a = 4, b = 3; // a = 4, b = 3
/*计算a+b的值,
并打印在频幕上*/
int main()
{
int c = 5, d = 9;
print a + c;
print b + d;
return c;
}
输出(out.s):
.text
.global __aeabi_idiv
.section .rodata
.align 2
.LC0:
.ascii "%d\012\000"
.text
.global a
.data
.align 2
.type i, %object
.size i, 4
a: .word 4
.text
.global b
.data
.align 2
.type i, %object
.size i, 4
b: .word 3
.text
.align 2
.globl main
.type main, %function
main:
push {fp, lr}
add fp, sp, #4
sub sp, sp, #8
mov r4, #5
sub r3, fp, #12
str r4, [r3]
mov r4, #9
sub r3, fp, #8
str r4, [r3]
ldr r3, .L2+0
ldr r4, [r3]
sub r3, fp, #12
ldr r5, [r3]
add r4, r4, r5
mov r1, r4
ldr r0, .L3
bl printf
ldr r3, .L2+4
ldr r4, [r3]
sub r3, fp, #8
ldr r5, [r3]
add r4, r4, r5
mov r1, r4
ldr r0, .L3
bl printf
sub r3, fp, #12
ldr r4, [r3]
mov r0, r4
b L1
L1:
sub sp, fp, #4
pop {fp, pc}
.align 2
.L3:
.word .LC0
.L2:
.word a
.word b
输出(out):
9
12
总结
在这一节,我们实现了局部变量,实现的步骤不是很难,但需要将前面的调用global的函数改为调用global或local函数其中之一,这个比较麻烦。在下一节中,我们将会实现函数参数。