一、前言
项目来源:
该项目是著名课程Nand2Tetris的课程项目,总共分12部分,从零开始构建属于自己的hack计算机。该文项目属于第七个子项目。
项目路线介绍:
在硬件部分,你将进入 01 的世界,用与非门构造出逻辑电路,并逐步搭建出一个 CPU 来运行一套课程作者定义的简易汇编代码。在软件部分,你将编写一个编译器,将作者开发的一个名为Jack的高级语言编译为可以运行在虚拟机上的字节码,然后进一步翻译为汇编代码。你还将开发一个简易的 OS,让你的计算机支持输入输出图形界面。至此,你可以用 Jack 开发一个俄罗斯方块的小游戏,将它编译为汇编代码,运行在你用与非门搭建出的 CPU 上,通过你开发的 OS 进行交互。
二、项目介绍
1.涉及知识
疑问:
在没有实际去完成本次项目时,我一直有这样一个疑问,为什么人们把从高级语言到汇编语言的编译过程拆分成两段:翻译+汇编,在一个连贯的过程中创建出一个中间态——虚拟机环境,按照整个编译(高级语言->机器语言)的结果来说,采用虚拟机模式的工程量并不比直接将高级语言翻译成机器语言的模式低,然而现在这种却模式不断流行起来,说到底来说,是两种模式的难度不一样。
答案:
语言的跨度越大,语言之间的粘性越低。这就造成一种结果,从高级语言到机器语言,可能同样效果的命令但是样式却天差地别,这就让人很难受。打个比方,人或许可以从婴儿的样貌看出和成年的联系,那如果是从婴儿和老人进行挂钩呢?所以我们需要有一个成年的样貌来支持起高级语言到机器语言的巨大跨越性。
而从另一方面来讲,使用VM模式很符合面向接口编程的习惯,比如我们把虚拟机比作一个接口,,接口的功能是标准化,这就有了中间态语言可以调用不同实现接口的前提,而接口的具体实现就要根据具体业务场景进行设计了,有win客户的、Linux客户的、mac客户的
进阶理解:
所以讲到这对虚拟机就可以有另一种认识了,虚拟机是不存在的(不是“物理学以及不存在”那种意思),意思是虚拟机其实只是对底层机器的用法而已,像本章中,我想讲得浮夸点,虚拟机就是一种对底层结构的高傲的指手画脚!虚拟机说,要有段内存用来放静态数据,所以static段出现了;虚拟机说,要有堆栈,所以又有段内存莫名奇妙的被带上了“堆栈部门”的帽子;等等等
2.实践要求
目标:
- VMTranslator的前半段
要求:推荐使用一门高级语言,来完成一个可以读取.vm文件(只有计算命令和内存存取命令),并将其翻译成.asm文件的VM翻译程序
细节:
- 推荐使用书里面的分成三个模块来实现(可以把CommandType模块独立出去)
- Parser:分析.vm文件,封装输入代码的访问,其功能有二:1.解析命令的类型;2.对命令进行洁净化处理
- CodeWriter:翻译.vm文件,封装输出代码的通道
- VMTranslator:主程序,程序的入口
- 个人对于这个翻译器的理解是:加强版HackAssembler,它的工作模式依旧是:获取命令和命令类型->根据类型对命令进行不一样的翻译工作,不一样的点是汇编器的翻译工作相近性高,而这个翻译器低一点,比如:一个Hack命令对应一条01命令,而一个VM命令可能需要几条hack命令来达成。
- 整个代码中最难的是CodeWriter模块,如果这个模块只是单纯按照书里给的接口文档编写,那么代码会非常臃肿,我们需要把一些使用频繁的工作架出去,如:堆栈的出入栈操作、对通用寄存器的操作
- 这个程序中会多次用到R13和R14两个temp寄存器,因为出/入栈操作要有临时的存放点,地址计算也要有
三、项目展示
1.程序结构图
2.实现
2.1.Parser
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Scanner;
/**
* 该类是待翻译文件的入口处理类,用于洁净读入的命令并将其进行分类
*/
public class Parser {
/** 带解析文件*/
private File file;
private Scanner scanner;
private String currentCommand;
/** 构造器
* 初始化待解析文件以及将输入流标准化*/
public Parser(File file, FileInputStream fiStream) {
this.file=file;
this.scanner = new Scanner(fiStream);
}
/**
* 判断是否还有命令
* @return true or false
*/ public boolean hasMoreCommands() {
if(this.scanner.hasNextLine()) {
return true;
}else {
return false;
}
}
/**
* 为当前命令进行注入操作
*/
public void advance() {
if(hasMoreCommands()) {
/* when we don't get the command loop*/
do{
this.currentCommand = this.scanner.nextLine();
}while(!getCommand());
}else {
this.scanner.close();
}
}
/**
*这是每条命令都要经过的一个洁净命令的方法,因为我们获得的命令并不一定是命令,它有可能是以下的诱惑信息
* 1.空行
* 2.注释(前面可能有空格符)
* 3.带有命令的空行,形式为:命令//注释信息
* 4.前面后者后面带有空格符的命令
* @throws IOException
*/
private boolean getCommand(){
String stringLine = this.currentCommand;
/* 判断是否为空行*/
if(stringLine.equals("")) {
return false;
}
/* 对命令进行注释判断,如果是注释语句则返回false*/
String head = stringLine.substring(0, 2); // 获取前两个字符串
String headChar = stringLine.trim().substring(0,2); // 获取排空格符号后的前两个字符串
if(head.equals("//") || headChar.equals("//")) {
return false;
}
/* 判断了命令不是注释后,还要观察命令后面是否带有注释,有则截取前面的有效命令*/
if(stringLine.contains("//")) {
String subStr = stringLine.substring(0, stringLine.indexOf("//"));
this.currentCommand = subStr.trim();
return true;
}
/*去除前后的空格符再为当前命令赋值*/
this.currentCommand = stringLine.trim();
return true;
}
/**
* 获取命令的类型
* @return
*/ public CommandType commandType() {
String commandLabel = "";
if(this.currentCommand.contains(" ")) {
commandLabel = this.currentCommand.substring(0, this.currentCommand.indexOf(" "));
}else {
commandLabel = this.currentCommand;
}
switch (commandLabel) {
case "add": case "sub": case "neg":
case "eq": case "gt": case "lt":
case "and": case "or": case "not":
return CommandType.C_ARITHMETIC;
case "pop":
return CommandType.C_POP;
case "push":
return CommandType.C_PUSH;
case "label":
return CommandType.C_LABEL;
case "goto":
return CommandType.C_GOTO;
case "if-goto":
return CommandType.C_IF;
case "function":
return CommandType.C_FUNCTION;
case "call":
return CommandType.C_CALL;
case "return":
return CommandType.C_RETURN;
default:
break;
}
return null;
}
/**
* 获得当前命令的第一个参数 <br/>
* 如果命令是return命令, 则不用调用该方法.
* @return add,sub..etc * 如果命令类型是运算型的, 返回命令本身.
*/ public String args1() {
// if(commandType().equals(CommandType.C_RETURN)) {
// return null;
// }
if(commandType().equals(CommandType.C_ARITHMETIC)) {
return this.currentCommand;
}
String args1 = "";
if(commandType().equals(CommandType.C_LABEL)||
commandType().equals(CommandType.C_GOTO)||
commandType().equals(CommandType.C_IF)) {
args1 = this.currentCommand.substring(this.currentCommand.indexOf(" ")+1, currentCommand.length());
}else{
args1 = this.currentCommand.substring(this.currentCommand.indexOf(" ")+1, this.currentCommand.lastIndexOf(" "));
}
return args1;
}
/**
* 返回当前命令的第二个参数,如果有的话. <br/>
* 当前仅当命令类型是这四种时,才会调用此方法 c_push,c_pop,c_function,c_call.
* @return */ public String args2() {
return this.currentCommand.substring(this.currentCommand.lastIndexOf(" ")+1, this.currentCommand.length());
}
public File getFile() {
return file;
}
public Scanner getScanner() {
return scanner;
}
public String getCurrentCommand() {
return currentCommand;
}
}
2.2.CodeWriter
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
public class CodeWriter {
/** 解析文件*/
private File file;
private File outputFile;
private BufferedWriter writer;
/** 该命令用于标记l-command, 很多命令会用到跳转命令,届时会有多次用到标签,而自增的i可以避免标签名重复*/
int i = 0;
/** 为写入流进行注入*/
public CodeWriter(File file) throws FileNotFoundException {
this.setFile(file);
setFileName(file.getPath());
this.writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile)));
}
/**
* 该类用来处理输出文件的路径问题.
* @param fileName
* the input file path
*/ public void setFileName(String fileName) {
String outputFilePath = fileName.substring(0,fileName.lastIndexOf("\\"))+"\\"+fileName.substring(fileName.lastIndexOf("\\")+1, fileName.indexOf("."))+".asm";
this.outputFile = new File(outputFilePath);
}
/**
* 获取栈顶的值.
* @throws IOException
*/
private void getTopSP() throws IOException {
this.writer.write("// get the top element of stack\r\n"
+ "@SP\r\n"
+ "M=M-1\r\n"
+ "A=M\r\n"
+ "D=M\r\n");
}
/**
* 获取值后进行压栈操作.
* @throws IOException
*/
private void pushValueIntoStack(String value) throws IOException {
this.writer.write("// push the value into stack\r\n"
+ "@SP\r\n"
+ "A=M\r\n"
+ "M="+value+"\r\n"
+ "@SP\r\n"
+ "M=M+1\r\n");
}
/**
* 根据地址块计算基地址在加上偏移量
* @param arg1
* 地址块
* @param arg2
* 偏移量
* @throws IOException
*/
private void doPushByArg(String arg1,String arg2) throws IOException {
this.writer.write("@"+arg1+"\r\n"
+ "D=M\r\n"
+ "@"+arg2+"\r\n"
+ "A=D+A\r\n"
+ "D=M\r\n");
pushValueIntoStack("D");
}
/**
* 将临时值存储在通用寄存器中。
* @param reg
* 选定寄存器
* @throws IOException
*/
private void storeValueInGR(String reg) throws IOException {
this.writer.write("// store the result temporarily\r\n"
+ "@"+reg+"\r\n"
+ "M=D\r\n");
}
/**
* 获取堆栈的前两个元素,并将它们存储在R13和R14中
* @throws IOException
*/
private void getTopTwoElementOfStackAndStoreThemInReg() throws IOException {
// get the top first element of stack
getTopSP();
storeValueInGR("R14");
// get the top second element of stack
getTopSP();
storeValueInGR("R13");
}
/**
* 将D寄存器的值放入R13寄存器所存的地址指向的内存单元<br/>
* * @throws IOException
*/
private void storeTheRegValueIntoMemory() throws IOException {
this.writer.write("// store the top value\r\n"
+ "@R13\r\n"
+ "A=M\r\n"
+ "M=D\r\n");
}
/**
* 从R13获取值. 主要用于计算
* @throws IOException
*/
private void getValueFromR13() throws IOException {
this.writer.write("@R13\r\n"
+ "D=M\r\n");
}
/**
* 在获取R13的值的情况下把R14和R13相加
* @throws IOException
*/
private void R13AddR14() throws IOException {
getValueFromR13();
this.writer.write("@R14\r\n"
+ "D=D+M\r\n");
}
/**
* 在获取R13的值的情况下把R14和R13相减:R13-R14
* @throws IOException
*/
private void R13MinusR14() throws IOException {
getValueFromR13();
this.writer.write("@R14\r\n"
+ "D=D-M\r\n");
}
/**
* 翻译计算命令
* @param
* @throws IOException
*/
public void writeArithmetic(Parser parser) throws IOException {
// 获取计算操作类型:是加、是减、还是等判断
String commandLabel = parser.args1();
switch (commandLabel) {
case "add":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
getTopTwoElementOfStackAndStoreThemInReg();
R13AddR14();
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
case "sub":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
getTopTwoElementOfStackAndStoreThemInReg();
R13MinusR14();
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
case "eq":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
getTopTwoElementOfStackAndStoreThemInReg();
R13MinusR14();
/*依据相除的结果进行判断跳转语句,如果结果为零,即eq为true,那么进行压栈0,否则进行压栈-1*/
this.writer.write("@EQ"+i+"\r\n"
+ "D;JEQ\r\n");
pushValueIntoStack("0");
this.writer.write("@ENDEQ"+i+"\r\n"
+ "0;JMP\r\n"
+ "(EQ"+i+")\r\n");
pushValueIntoStack("-1");
this.writer.write("(ENDEQ"+(i++)+")\r\n");
this.writer.write("\r\n");
break;
case "gt":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
/*对栈顶两元素进行相减,结果大于零,则gt(x<y)不成立,否则继续程序进行到0;JMP无条件跳转*/
getTopTwoElementOfStackAndStoreThemInReg();
R13MinusR14();
this.writer.write("@GT"+i+"\r\n"
+ "D;JGT\r\n");
pushValueIntoStack("0");
this.writer.write("@ENDGT"+i+"\r\n"
+ "0;JMP\r\n"
+ "(GT"+i+")\r\n");
pushValueIntoStack("-1");
this.writer.write("(ENDGT"+(i++)+")\r\n");
this.writer.write("\r\n");
break;
case "lt":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
/*对栈顶两元素进行相减,结果小于零,则gt(x>y)不成立,否则继续程序进行到0;JMP无条件跳转*/
getTopTwoElementOfStackAndStoreThemInReg();
R13MinusR14();
this.writer.write("@LT"+i+"\r\n"
+ "D;JLT\r\n");
pushValueIntoStack("0");
this.writer.write("@ENDLT"+i+"\r\n"
+ "0;JMP\r\n"
+ "(LT"+i+")\r\n");
pushValueIntoStack("-1");
this.writer.write("(ENDLT"+(i++)+")\r\n");
this.writer.write("\r\n");
break;
case "and":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
/*先将栈顶两个元素提取出来,再进行与运算后压回栈*/
getTopTwoElementOfStackAndStoreThemInReg();
getValueFromR13();
this.writer.write("@R14\r\n"
+ "D=D&M\r\n");
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
case "or":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
/*先将栈顶两个元素提取出来,再进行或运算后压回栈*/
getTopTwoElementOfStackAndStoreThemInReg();
getValueFromR13();
this.writer.write("@R14\r\n"
+ "D=D|M\r\n");
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
case "not":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
/*先将栈顶元素提取出来,再进行非运算后压回栈*/
getTopSP();
this.writer.write("D=!D\r\n");
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
case "neg":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
/*先将栈顶元素提取出来,再进行取反运算后压回栈,取反操作是将D = 0 - D*/
getTopSP();
this.writer.write("@0\r\n"
+ "D=A-D\r\n");
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
default:
break;
}
}
/**
* 用于翻译内存操作命令的方法
* @throws IOException
*/
public void writePushPop(Parser parser) throws IOException {
/*PUSH操作可细分为:先计算目标块的地址->根据地址取值->压栈*/
if(parser.commandType().equals(CommandType.C_PUSH)) {
String arg1 = parser.args1();
String arg2 = parser.args2();
switch (arg1) {
case "argument":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
doPushByArg("ARG", arg2);
this.writer.write("\r\n");
break;
case "local":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
doPushByArg("LCL", arg2);
this.writer.write("\r\n");
break;
case "static":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
/*以'文件名.偏移量'的形式获取静态变量的符号*/
this.writer.write("@"+this.file.getName().substring(0, this.file.getName().indexOf(".")+1)+arg2+"\r\n"
+ "D=M\r\n");
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
case "constant":
/*直接获取常量,第二个参数就是数值本身*/
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
this.writer.write("@"+arg2+"\r\n"
+ "D=A\r\n");
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
case "this":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
doPushByArg("THIS", arg2);
this.writer.write("\r\n");
break;
case "that":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
doPushByArg("THAT", arg2);
this.writer.write("\r\n");
break;
case "pointer":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
/*
* D = THIS的地址
* agr2 = arg2 + D * D= M[THIS地址+arg2]
* 压栈*/
this.writer.write("@THIS\r\n"
+ "D=A\r\n"
+ "@"+arg2+"\r\n"
+ "A=D+A\r\n"
+ "D=M\r\n");
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
case "temp":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
// Because the temp segment store the content directly, so we needn't to compute the address.
this.writer.write("@R5\r\n"
+ "D=A\r\n"
+ "@"+arg2+"\r\n"
+ "A=D+A\r\n"
+ "D=M\r\n");
pushValueIntoStack("D");
this.writer.write("\r\n");
break;
default:
break;
}
}else if(parser.commandType().equals(CommandType.C_POP)) {
/*PUSH操作可细分为:先计算目标块的地址->将地址放在通用寄存器->出栈顶元素->根据通用寄存器上的地址把D寄存器上的栈顶元素进行存储*/
String arg1 = parser.args1();
String arg2 = parser.args2();
switch (arg1) {
case "argument":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
this.writer.write("@ARG\r\n"
+ "D=M\r\n"
+ "@"+arg2+"\r\n"
+ "D=D+A\r\n");
storeValueInGR("R13");
getTopSP();
storeTheRegValueIntoMemory();
this.writer.write("\r\n");
break;
case "local":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
this.writer.write("@LCL\r\n"
+ "D=M\r\n"
+ "@"+arg2+"\r\n"
+ "D=D+A\r\n");
storeValueInGR("R13");
getTopSP();
storeTheRegValueIntoMemory();
this.writer.write("\r\n");
break;
case "static":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
getTopSP();
this.writer.write("@"+this.file.getName().substring(0, this.file.getName().indexOf(".")+1)+arg2+"\r\n"
+ "M=D\r\n");
this.writer.write("\r\n");
break;
case "constant":
// 常数单元并没有可以放置的内存块,常数单元只是一个数,要什么都可以在位范围内定的
break;
case "this":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
this.writer.write("@THIS\r\n"
+ "D=M\r\n"
+ "@"+arg2+"\r\n"
+ "D=D+A\r\n");
storeValueInGR("R13");
getTopSP();
storeTheRegValueIntoMemory();
this.writer.write("\r\n");
break;
case "that":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
this.writer.write("@THAT\r\n"
+ "D=M\r\n"
+ "@"+arg2+"\r\n"
+ "D=D+A\r\n");
storeValueInGR("R13");
getTopSP();
storeTheRegValueIntoMemory();
this.writer.write("\r\n");
break;
case "pointer":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
this.writer.write("@THIS\r\n"
+ "D=A\r\n"
+ "@"+arg2+"\r\n"
+ "D=D+A\r\n");
storeValueInGR("R13");
getTopSP();
storeTheRegValueIntoMemory();
this.writer.write("\r\n");
break;
case "temp":
this.writer.write("// vm command:"+parser.getCurrentCommand()+"\r\n");
this.writer.write("@5\r\n"
+ "D=A\r\n"
+ "@"+arg2+"\r\n"
+ "D=D+A\r\n");
storeValueInGR("R13");
getTopSP();
storeTheRegValueIntoMemory();
this.writer.write("\r\n");
break;
default:
break;
}
}else {
System.out.println("this command is not push and pop!");
}
}
/**
* 关闭输出流
* @throws IOException
*/
public void close() throws IOException {
this.writer.close();
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
public BufferedWriter getWriter() {
return writer;
}
public void setWriter(BufferedWriter writer) {
this.writer = writer;
}
}
2.3.VMTranslator
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class VMTranslator {
private Parser parser;
private CodeWriter codeWriter;
public VMTranslator(File file) throws FileNotFoundException {
this.parser = new Parser(file, new FileInputStream(file));
this.codeWriter = new CodeWriter(file);
}
public Parser getParser() {
return parser;
}
public CodeWriter getCodeWriter() {
return codeWriter;
}
public void doWrite() throws IOException {
// 获取命令
this.parser.advance();
CommandType commandType = this.parser.commandType();
if(commandType.equals(CommandType.C_ARITHMETIC)) {
codeWriter.writeArithmetic(this.parser);
}else if(commandType.equals(CommandType.C_PUSH) || commandType.equals(CommandType.C_POP)) {
codeWriter.writePushPop(this.parser);
}
this.codeWriter.getWriter().flush();
}
public static void main(String[] args) throws IOException {
// *** 根据自身磁盘结构修改 *** File file = new File("C:\\Users\\LinKa\\Desktop\\StackTest.vm");
VMTranslator vmTranslator = new VMTranslator(file);
Parser parser = vmTranslator.getParser();
do {
vmTranslator.doWrite();
}while(parser.hasMoreCommands());
}
}
2.4.CommandType
public enum CommandType {
C_ARITHMETIC,C_PUSH,C_POP,C_LABEL,C_GOTO,C_IF,C_FUNCTION,C_RETURN,C_CALL
}