动手实现编译器(十七)——局部变量

上一节中,我们已经实现了逻辑运算和负数,在本节中,我们将要实现比较难的局部变量。
现在,我们所有的变量对所有函数都是全局可见的。我们想添加一个本地范围对于变量,这样每个函数都有自己的变量,不能被其他函数看到。此外,在递归函数的情况下,同一个函数的每个实例都有自己的局部变量。
要为同一函数的多个实例创建本地作用域,为了提供一个地方来存储函数的参数,我们需要一个堆。
让我们从局部变量和全局变量之间的区别开始。全局变量必须对所有函数可见,但局部变量只是对一个函数可见。
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;			        // 距离栈基指针的偏移地址
};

我们添加了classposn字段。如上一部分所述,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()函数,来查找或分配局部符号。他们有代码来检测LocalsGlobals之间的冲突。

// 检查符号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函数其中之一,这个比较麻烦。在下一节中,我们将会实现函数参数。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值