动手实现编译器(四)——目标代码生成器

在前几节中,我们实现了词法分析器、语法分析器、语义分析器和测试器,可以正确处理一条加减乘除取模运算语句。在这一节中,我们将用目标代码生成器替换测试器,实现真正的编译器。

测试器

/*测试器代码*/

// AST操作符
char *ASTop[] = {"+", "-", "*", "/", "%"};

// 给定一个AST,返回一个表达式
int interpretAST(struct ASTnode *n)
{
    int leftval, rightval;

    // 获得左、右子树值
    if (n->left)    leftval = interpretAST(n->left);
    if (n->right)   rightval = interpretAST(n->right);

    // 调试:打印将要做的事情
    if (n->op == A_INT)    printf("int %d\n", n->intvalue);
    else    printf("%d %s %d\n", leftval, ASTop[n->op], rightval);

    switch (n->op)
    {
        case A_ADD:    return leftval + rightval;
        case A_SUB:    return leftval - rightval;
        case A_MUL:    return leftval * rightval;
        case A_DIV:    return leftval / rightval;
        case A_MOD:    return leftval % rightval;
        case A_INT:    return n->intvalue;
        default:    fprintf(stderr, "Unknown AST operator %d\n", n->op);
                    exit(1);
    }
}

我们观察测试器的代码,不难发现,测试器的interpretAST()函数后序遍历给定的AST树,并执行节点上的op操作。如果op值是数学运算符之一,则将执行此数学运算。如果op值是整数符号,则返回整数值。不断计算子树的值,最终返回这棵树的值。

测试器改为代码生成器

很清楚,只要把对应的op操作修改为生成对应的汇编代码就可以了。所以,我们只需编写出特定功能的汇编代码生成函数,然后在执行op操作时调用对应的函数。
同时我们还要考虑到,测试器的函数中传递的值是所求树的值,是一个整数。然而,生成汇编代码后,不能实时计算出整数。因此,我们要把结果保存在一个寄存器中,然后在函数中传递储存结果的寄存器号。所以,代码生成器与测试器的另一个区别是在函数中传递的值,不在是树的值而是树的结果所在的寄存器号。
目标代码生成器的代码如下:

// 给定AST,生成汇编代码,返回值为结果所在寄存器号
int code_generator(struct ASTnode *n)
{
    int leftreg, rightreg;
    if (n->left)    leftreg = code_generator(n->left);
    if (n->right)   rightreg = code_generator(n->right);
    switch (n->op)
    {
        case A_ADD:    return (arm_add(leftreg,rightreg));
        case A_SUB:    return (arm_sub(leftreg,rightreg));
        case A_MUL:    return (arm_mul(leftreg,rightreg));
        case A_DIV:    return (arm_div(leftreg,rightreg));
        case A_MOD:    return (arm_mod(leftreg,rightreg));
        case A_INT:    return (arm_load(n->intvalue));
        default:    fprintf(stderr, "Unknown AST operator %d\n", n->op);
                    exit(1);
    }
}

//  生成汇编代码
void generate_code(struct ASTnode *n) 
{
    int reg;
    arm_preamble();				//预处理汇编代码
    reg = code_generator(n);	//生成汇编代码
    arm_print_reg(reg);			//打印寄存器汇编代码
    arm_postamble();			//尾处理汇编代码
}

汇编代码生成函数

注意:本文的目标平台为ARMv7-32

寄存器分配

任何CPU的寄存器数量都有限,我们必须分配一个寄存器来保存整数值,以及我们对它们执行的任何计算。但是一旦使用了一个值,我们通常可以丢弃该值,从而释放保存它的寄存器,然后我们可以将该寄存器重用于另一个值。
所以有三个函数处理寄存器分配:

将所有寄存器设置为可用:freeall_registers()
分配寄存器:alloc_register()
释放分配的寄存器:free_register()

ARMv7-A架构提供了16个32位通用寄存器(R0-R15)和一个程序状态寄存器CPSR(Current Program Status Register),在异常模式下,可以访问SPSR(Saved Program Status Register),在异常模式下,SPSR用于保存当前CPSR寄存器值。其中R0-R14可以用于普通数据存储,R15是程序计数器PC(program counter)。
具体功能见下表:

寄存器号功能
R0-R7在任何模式下都对应相同的物理存储
R8-R12根据模式不同对应不同的物理存储
R13(SP)R13在所有模式下是操作系统堆栈指针,但在堆栈操作不需要时,它可以用作通用寄存器
R14(LR)R14(链接寄存器)保存您使用带链接分支 (BL) 指令时输入的子程序的返回地址。 当它不支持从子例程返回时,它也可以用作通用寄存器。 R14_svc、R14_irq、R14_fiq、R14_abt 和 R14_und 类似地用于在发生中断和异常时保存 R15 的返回值,或者在中断或异常例程中执行分支和链接指令时
R15(PC)R15 是程序计数器,保存当前程序地址(实际上,它总是在 ARM 状态下指向当前指令前 8 个字节,在 Thumb 状态下指向当前指令前 4 个字节,这是原始 ARM1 的三级流水线的遗产 处理器)。 在 ARM 状态下读取 R15 时,位 [1:0] 为零,位 [31:2] 包含 PC。 在 Thumb 状态下,位 [0] 始终读为零。
CPSR当前程序状态寄存器 (CPSR) 用于存储:APSR 标志、当前处理器模式、中断禁用标志、当前处理器状态,即ARM、Thumb、ThumbEE 或Jazelle、字节序、IT 块的执行状态位

本文使用的通用寄存器:R0-R11,其中R0-R3为参数寄存器,在调用函数时,用来存放前4个函数参数和返回值,多出的存堆栈中。R4-R11为通用寄存器,存放其他操作数。有一个带有实际寄存器名称的字符串表:

// 可用寄存器列表及其名称
int freereg[12];

这使得这些功能相当独立于 CPU 体系结构。
寄存器函数代码如下:

// 将所有寄存器设置为可用
void arm_freeall_registers()
{
    memset(freereg, 0, sizeof(freereg));
}

// 分配一个空闲的寄存器,返回寄存器的编号。 
// 如果没有可用的寄存器,则结束。
int arm_alloc_register()
{
    for (int i = 4; i < 12; i++)
    {
        if (freereg[i])
        {
            freereg[i]= 0;
            return i;
        }
    }
    fprintf(stderr, "Out of registers!\n");
    exit(1);
}

// 检查寄存器是否已经存在,
// 若存在,则释放;否则,报错。
void arm_free_register(int reg)
{
    if (freereg[reg] != 0)
    {
        fprintf(stderr, "Error trying to free register %d\n", reg);
        exit(1);
    }
    freereg[reg]= 1;
}

加载立即数

分配了一个寄存器,然后通过MOV指令将立即数加载到分配的寄存器中。代码如下:

// 将立即数加载到寄存器中,返回寄存器编号
int arm_load(int value)
{
    // 获得新的寄存器
    int r = arm_alloc_register();

    fprintf(Outfile, "\tmov\tr%d, #%d\n", r, value);
    return r;
}

寄存器加法

通过ADD指令将两个寄存器的值加在一起,结果保存在两个寄存器之一中,然后释放另一个,代码如下:

// 将两个寄存器相加并返回带有结果的寄存器编号
int arm_add(int r1, int r2)
{
    fprintf(Outfile, "\tadd\tr%d, r%d, r%d\n", r1, r1, r2);
    arm_free_register(r2);
    return r1;
}

注:此处可以优化,若有表达式1+2,我们要分别把1、2 MOV入寄存器再执行加法,实际上可以用ADD R1,R1,imm指令实现寄存器加立即数,那就可以减少一条MOV指令。

寄存器减法

同寄存器加法,代码如下:

// 第一个寄存器减去第二个寄存器并返回带有结果的寄存器编号
int arm_sub(int r1, int r2)
{
    fprintf(Outfile, "\tsub\tr%d, r%d, r%d\n", r1, r1, r2);
    arm_free_register(r2);
    return r1;
}

寄存器乘法

同寄存器加法,代码如下:

// 将两个寄存器相乘并返回带有结果的寄存器编号
int arm_mul(int r1, int r2)
{
    fprintf(Outfile, "\tmul\tr%d, r%d, r%d\n", r1, r1, r2);
    arm_free_register(r2);
    return r1;
}

寄存器除法

同寄存器加法,代码如下:

// 第一个寄存器除以第二个寄存器并返回带有结果的寄存器编号
int arm_div(int r1, int r2)
{
    fprintf(Outfile, "\tmov\tr0, r%d\n", r1);
    fprintf(Outfile, "\tmov\tr1, r%d\n", r2);
    fprintf(Outfile, "\tbl\t__aeabi_idiv\n");
    fprintf(Outfile, "\tmov\tr%d, r0\n", r1);
    arm_free_register(r2);
    return r1;
}

寄存器取模

寄存器取模比较复杂,原理是a % b = a - (a / b) * b,代码如下:

// 第一个寄存器除以第二个得到余数并返回带有结果的寄存器编号
int arm_mod(int r1, int r2)
{
    // r1 % r2 = r1 - (r1 / r2) * r2
    // 获得新的寄存器,保存r1
    int r = arm_alloc_register();
    fprintf(Outfile, "\tmov\tr%d, r%d\n", r, r1);
    fprintf(Outfile, "\tmov\tr0, r%d\n", r);
    fprintf(Outfile, "\tmov\tr1, r%d\n", r2);
    fprintf(Outfile, "\tbl\t__aeabi_idiv\n");
    fprintf(Outfile, "\tmov\tr%d, r0\n", r);
    fprintf(Outfile, "\tmul\tr%d, r%d, r%d\n", r, r, r2);
    fprintf(Outfile, "\tsub\tr%d, r%d, r%d\n", r1, r1, r);
    arm_free_register(r);
    arm_free_register(r2);
    return r1;
}

测试结果

为了直观的输出结果,我们补充上汇编预处理代码和尾代码

汇编预处理代码

// 汇编预处理代码
void arm_preamble()
{
    arm_freeall_registers();
    fputs(
        "\t.text\n"
        "\t.section\t.rodata\n"
        "\t.align  2\n"
        ".LC0:\n"
        "\t.ascii  \"%d\\012\\000\"\n"
        "\t.text\n"
        "\t.align  2\n"
        "\t.global main\n"
        "\t.type   main, %function\n"
        "main:\n"
        "\tpush    {fp, lr}\n"
        "\tadd     fp, sp, #4\n",
    Outfile);
}

汇编尾代码

// 汇编尾代码
void arm_postamble()
{
    fputs(
        "\tmov     r3, #0\n"
        "\tmov     r0, r3\n"
        "\tpop     {fp, pc}\n"
        ".L3:\n"
        "\t.word   .LC0\n"
        "\t.size   main, .-main\n",
    Outfile);
}

此外,还要增加一个输出函数,代码如下:

// 打印寄存器值
void arm_print_reg(int reg)
{
    fprintf(Outfile, "\tmov     r1, r%d\n", reg);
    fputs(
        "\tldr     r0, .L3\n"
        "\tbl      printf\n",
    Outfile);
    arm_free_register(reg);
}

修改主函数

同时修改增加全局变量:

FILE		    *Outfile;   //输出文件

修改main()函数,代码如下:

// 用法 compiler -o -s outfile infile
int main(int argc, char *argv[])
{
    struct ASTnode *n;
    if(argc != 5)
    {
        fprintf(stderr, "compiler -o -s outfile infile\n");
        exit(1);
    }
    init();
    if ((Infile = fopen(argv[4], "r")) == NULL)
    {
        fprintf(stderr, "Unable to open %s: %s\n", argv[4], strerror(errno));
        exit(1);
    }
    if ((Outfile = fopen(argv[3], "w")) == NULL)
    {
        fprintf(stderr, "Unable to create %s: %s\n", argv[4], strerror(errno));
        exit(1);
    }
    scan(&Token);			            // 从输入中获得第一个单词
    n = binexpr(OpPrec[Token.token]);   // 解析表达式
    printf("%d\n", interpretAST(n));	// 计算最终的结果
    generate_code(n);
}

输入:

2 + 3 * 4 / 5 % 6 - 7 + 8

输出:
out.s文件

	.text
	.section	.rodata
	.align  2
.LC0:
	.ascii  "%d\012\000"
	.text
	.align  2
	.global main
	.type   main, %function
main:
	push    {fp, lr}
	add     fp, sp, #4
	mov	r4, #2
	mov	r5, #3
	mov	r6, #4
	mul	r5, r5, r6
	mov	r6, #5
	mov	r0, r5
	mov	r1, r6
	bl	__aeabi_idiv
	mov	r5, r0
	mov	r6, #6
	mov	r7, r5
	mov	r0, r7
	mov	r1, r6
	bl	__aeabi_idiv
	mov	r7, r0
	mul	r7, r7, r6
	sub	r5, r5, r7
	add	r4, r4, r5
	mov	r5, #7
	sub	r4, r4, r5
	mov	r5, #8
	add	r4, r4, r5
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	mov     r3, #0
	mov     r0, r3
	pop     {fp, pc}
.L3:
	.word   .LC0
	.size   main, .-main

上机结果:

5

总结

现在我们已经实现了,计算一条加减乘除取模基本运算的编译器。在下一节中,我们将在代码中添加其他SysY语言的语句,使它成为一个真正的编译器。

  • 0
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值