在前几节中,我们实现了词法分析器、语法分析器、语义分析器和测试器,可以正确处理一条加减乘除取模运算语句。在这一节中,我们将用目标代码生成器替换测试器,实现真正的编译器。
测试器
/*测试器代码*/
// 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语言的语句,使它成为一个真正的编译器。