第一个任务
这部分的实验要完成将 TAC 的指令序列转换成目标代码,本文中在这里对实现做了一些限制,假设数据类型只包含整数类型,不包含如浮点数、数组、结构和指针等其它数据类型(也不包括全局变量)
目标语言为汇编语言,这样做的目的就是尽可能简单地实现目标代码的生成并能运行程序观察运行结果,完成了一个可运行的编译程序
MIPS32 架构
MIPS32 的架构是一种基于固定长度的定期编码指令集,并采用导入/存储(load/store)数据模型
目标语言可选定 MIPS32 指令序列,可以在 SPIM Simulator 上运行
MIPS32 体系结构有 :
- 32个通用寄存器
- 2个特殊寄存器(整数乘除法寄存器)
- 32个浮点寄存器
- PC:程序计数器(在 MIPS32 下,PC 不是一个通用寄存器)
本实验只使用 t1-t9 s0-s7
,并且不区分功能
TAC 指令和 MIPS32 指令的对应关系如下表所示:
中间代码 | MIPS32 指令 |
---|---|
LABEL x | x: |
x :=#k | li reg(x),k |
x := y | move reg(x), reg(y) |
x := y + z | add reg(x), reg(y) , reg(z) |
x := y - z | sub reg(x), reg(y) , reg(z) |
x := y * z | mul reg(x), reg(y) , reg(z) |
x := y / z | div reg(y) , reg(z) mflo reg(x) |
GOTO x | j x |
RETURN x | move $v0, reg(x) jr $ra |
IF x==y GOTO z | beq reg(x),reg(y),z |
IF x!=y GOTO z | bne reg(x),reg(y),z |
IF x>y GOTO z | bgt reg(x),reg(y),z |
IF x>=y GOTO z | bge reg(x),reg(y),z |
IF x<y GOTO z | ble reg(x),reg(y),z |
IF x<=y GOTO z | blt reg(x),reg(y),z |
X:=CALL f | jal f move reg(x),$v0 |
- 其中 reg(x) 表示变量 x 所分配的寄存器
- 寄存器 v0 用来存放子程序的返回值
安装 MIPS 编译器和模拟器
安装编译器和依赖:
1 | sudo apt-get install emdebian-archive-keyring |
检测是否安装成功:
1 | mips-linux-gnu-gcc -dumpmachine |
安装 mips 虚拟机:
1 | sudo apt-get install qemu-system-mips |
下载内核映像和初始化文件:(不推荐)
1 | wget http://ftp.debian.org/debian/dists/stable/main/installer-mipsel/current/images/malta/netboot/initrd.gz |
创建虚拟磁盘:(不推荐)
1 | qemu-img create -f qcow2 hda.img 2G |
开启虚拟机:(不推荐)
1 | qemu-system-mips -M malta \ |
- 上面这种方法有点 BUG,目前没有启动成功
下载内核映像和初始化文件:(推荐)
1 | wget http://people.debian.org/\~aurel32/qemu/mips/debian_squeeze_mips_standard.qcow2 |
开启虚拟机:(推荐)
1 | qemu-system-mips -M malta -kernel vmlinux-2.6.32-5-4kc-malta -hda debian_squeeze_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" |
- user:user
- password:user
安装 libguestfs:用于挂载 qcow2 磁盘镜像
1 | sudo apt-get install libguestfs-tools |
挂载/卸载命令:
1 | sudo guestmount -a debian_squeeze_mips_standard.qcow2 -m /dev/sda1 rootfs |
寄存器分配算法
在目标生成阶段,一个很重要的工作就是寄存器的分配:
- 最为简单的就是朴素的寄存器分配算法,效率最低,也最容易实现
- 对于一个基本块采用的局部寄存器分配算法,实现起来相对不是太难,且效率上有一定提升
本实验采用朴素的寄存器分配算法:
- 将所有的变量或临时变量都放入内存中
- 每翻译一条中间代码之前都需要吧要用到的变量先加载到寄存器中,得到该代码的计算结果之后又需要将结果写回内存
可分配的寄存集合:(不区分功能)
1 | string regs[] = {"t1","t2","t3","t4","t5","t6","t7","t8","t9","s0","s1","s2","s3","s4","s5","s6","s7"}; |
核心算法如下:
1 | vector<string> variables; /* 变量数组 */ |
朴素寄存器分配的翻译
翻译的操作如下表:
中间代码 | MIPS32 指令 |
---|---|
x :=#k | li $t3 ,k sw $t3, x的偏移量($sp) |
x := y | lw $t1, y的偏移量($sp) move $t3 , $t1 sw $t3, x的偏移量($sp) |
x := y + z | lw $t1, y的偏移量($sp) lw $t2, z的偏移量($sp) add $t3 , $t1,$t2 sw $t3, x的偏移量($sp) |
x := y - z | l w $t1, y的偏移量($sp) l w $t2, z的偏移量($sp) sub $t3 , $t1,$t2 sw $t3, x的偏移量($sp) |
x := y * z | l w $t1, y的偏移量($sp) l w $t2, z的偏移量($sp) mul $t3 , $t1,$t2 sw $t3, x的偏移量($sp) |
x := y / z | l w $t1, y的偏移量($sp) l w $t2, z的偏移量($sp) mul $t3 , $t1,$t2 div $t1,$t2 mflo $t3 sw $t3, x的偏移量($sp) |
RETURN x | move $v0, x的偏移量($sp)jr $ra |
IF x==y GOTO z | l w $t1, x的偏移量($sp) l w $t2, y的偏移量($sp) beq $t1,$t2 ,z |
IF x!=y GOTO z | l w $t1, x的偏移量($sp) l w $t2, y的偏移量($sp) bne $t1,$t2 ,z |
IF x>y GOTO z | l w $t1, x的偏移量($sp) l w $t2, y的偏移量($sp) bgt $t1,$t2 ,z |
IF x>=y GOTO z | l w $t1, x的偏移量($sp) l w $t2, y的偏移量($sp) bge $t1,$t2 ,z |
IF x<y GOTO z | l w $t1, x的偏移量($sp) l w $t2, y的偏移量($sp) ble $t1,$t2 ,z |
IF x<=y GOTO z | l w $t1, x的偏移量($sp) l w $t2, y的偏移量($sp) blt $t1,$t2 ,z |
X:=CALL f |
对于函数调用 X:=CALL f
,需要完成开辟活动记录的空间、参数的传递和保存返回地址等,函数调用返回后,需要恢复返回地址,读取函数返回值以及释放活动记录空间(活动记录的空间布局没有一个统一的标准,可根据自己的理解保存好数据,并能正确使用即可)
通常,使用4个寄存器完成参数的传递,多余4个的参数使用活动记录空间,这里做了简单处理,所有参数都使用活动记录空间,具体步骤:
- 首先根据保存在函数调用指令中的 offset,找到符号表中的函数定义点,获取函数的参数个数i,这样就可得到在
X:=CALL f
之前的 i 个 ARG 形式的中间代码,获得 i 个实参值所存放的单元,取出后送到形式参数的单元中 - 根据符号表记录的活动记录大小,开辟活动记录空间和保存返回地址
- 使用
jal f
转到函数f处 - 释放活动记录空间和恢复返回地址
- 使用
sw $v0 , x
的偏移量($sp)
获取返回值送到 X 的存储单元中
TAC 转化为 MIPS32
这就是本实验最复杂的步骤了,首先需要一个函数来读取记录中间变量的文件:
1 | string Load_Inter(const string& filename){ |
另一个函数来记录所有变量到 variables
中:
1 | vector<string> variables; /* 变量数组 */ |
- 所有临时变量均分配在寄存器中
最后需要一个函数来逐行进行翻译,我们把这个函数拆分为多段来分析:
将每行 string 按空格存成数组:
1 | vector<string> line; |
stringstream
类相当于一个管道(队列),可以用它来分割字符串
处理简单指令:
1 | size_t func_argn; |
- 中间代码转化为 MIPS 的过程以函数为单位,读取到一个新的函数标签时,应该重置
reg_ok
和table
- 当一个函数调用其子函数时,程序会将寄存器信息保存在栈中,我们不用担心覆盖寄存器带来的影响
处理函数参数:
1 | string params[] = {"a0","a1","a2","a3"}; |
- 函数传参使用
a0-a3
(规定函数参数不超过4个) - 全局变量
func_argn
和func_param
用于记录当前参数的标号
处理函数调用:
1 | if (line[0] == "CALL"){ /* 函数调用 */ |
- 调用前保存寄存器,调用后恢复寄存器(
t1-t9 a0-a3
)
IF 判断语句:
1 | if (line[0] == "IF") { |
- 根据不同的条件,使用对应的汇编代码即可
赋值语句:
1 | if (line[1] == ":=") { |
- 根据不同的条件,使用对应的汇编代码即可
最后需要一个函数组织并输出 demo.s
汇编文件:
1 | void write_to_txt(const vector<string>& obj){ |
编译与结果
编译命令:
1 | g++ main.cpp -o test4 |
测试文件:
1 | int fast(int n) |
中间代码:
1 | FUNCTION fast : |
汇编代码:
1 | .data |