用JS搭建自己的汇编模拟器
写在前面:
由于自己的计组课上学期实在是划水了,很多内容没有好好的理解和掌握,但是能让我划起水,还是因为课水太多了啊!刚好自己学过前端看到这个课程就想写写看,复习一下计组。这个项目是拿Angular写的,我没学过Angular,所以就只看了大概,没有具体的构建。具体实现看我后面的更新吧!😴
有关模拟器(Simulator)
我看的这篇文章,里头的模拟器是用Angular.js进行构建的,可以运行在很多设备上的浏览器中。这个模拟器有很多限制和简化,但是他有着许多模拟器(emulator)最基础的结构。
这个虚拟机包含有一下几个组件:
- 256 byte的内存。
- 8-bit的CPU。
- 输出控制台(console output)
总体来说,他不是一个强大的机器但是已经足够我们使用。
有关CPU
通用寄存器
CPU中有着四个通用寄存器。
首先,CPU如何知道下一条指令是什么呢?这就需要使用到IP(instruction pointer)这个寄存器了,在IP这个寄存器中,存放着下一条指令在内存中的位置。
标志位
其次,我们还需要用到标志位,标志位的作用就体现在,比如我们需要使用到IF-ELSE等判断语句的时候,我们就可以通过标志位来判定我们的结果是否满足我们的要求。这里我们设置三个标志位:Zero(Z)、Carry(C)、Fault(F)
这三个标志位分别起到这些作用:
- Zero:当我们的指令的结果是零,那么这个标志位将会被置为1,否则就会置为0。
- Carry:当我们的指令产生了进位,这个标志位就会被置为1。
- Fault:当程序出现错误,例如在除法运算中,零被置于分母,那么就会将这个标志位置为1。
堆栈寄存器
堆栈指针SP为内存中当前堆栈的首地址提供了栈顶地址,存储和使用数据时将会对其进行改变。
上面我们定义了这些寄存器、标志位等,那么接下来我们要设计一个用于初始化CPU的函数,可以进行重置整个系统,并将上面我们定义的这些数据初始化。
var gpr, ip, sp, zero, carry, fault;
function reset() {
gpr = [0, 0, 0, 0];
self.maxSP;
ip = 0;
zero = false;
carry = false;
fault = false;
}
有关存储器(memory)
在这里我们将用一个简单的数组来进行模拟真实的内存,在这里需要注意的是,我们将每个数组单元模拟为一个字节的大小,但是实际上,JS的每个数组单元可以容纳大得多的数据。因此在CPU那里,我们设置了限制,以保证每个值都在0—255之间。
存储器将有三个功能:Load、Store、Reset。
- Load:可以从给定的地址检索一个字节,并将其读出。
- Store :可以将指定的值写入指定的地址。
- Reset:将所有的内存值重置为零,用于初始化和重置。
var memory = Array(256);
function load(address) {
if (address < 0 || address >= memory.length) {
throw "Memory access violation. Address: " + address;
}
return memory[address];
};
function store(address, value) {
if (address < 0 || address >= memory.length) {
throw "Memory access violation. Address: " + address;
}
memory[address] = value;
};
function reset() {
for(var i=0; i < memory.length; i++) {
mmeory[i] = 0;
}
};
根据这些我们就可以大致模拟出CPU的代码结构了:
function step() {
if (fault) {
throw "FAULT. Reset to CPU continue.";
}
var instr = memory.load(ip);
switch(instr) {
case opcodes.ADD_REG_TO_REG:
// Read operand 1: Target register
var regTo = memory.load(ip+1);
// Read operand 2: Source register
var regFrom = memory.load(ip+2);
// Execute instruction. Add values of both registers
var value = processResult(readRegister(regTo) + readRegister(regFrom));
// Write the new value back to the target register
writeRegister(regTo, value);
// Increase instruction pointer
ip += ip+3;
break;
case opcodes.ADD_REGADDRESS_TO_REG:
...
case opcodes.ADD_ADDRESS_TO_REG:
...
case opcodes.ADD_NUMBER_TO_REG:
...
case ...
...
default:
throw "Invalid opcode: " + instr;
}
}
function processResult(value) {
zero = false;
carry = false;
if (value >= 256) {
carry = true;
value % 256;
} else if (value === 0) {
zero = true;
} else if (value < 0) {
carry = true;
value = 255 - (-value) % 256;
}
return value;
};
有关控制台输出
这里我们约定控制台显示内存中最后的24个字节,所以最后让程序在控制台上输出数据,只需要将数据写入内存的最后24个字节
有关汇编程序
在完成上面的虚拟机的基础组件之后,我们还需要最后的一个部分,也是最重要的一个部分:怎么将代码汇编形成CPU指令。
我们的汇编程序将解析操作数,并生成CPU指令。
我们将使用正则表达式来进行解析代码,但是这只是一个很简单的一个解决方案,因为我们不可能用正则表达式来完全解析代码,但是为了简单起见我们还是使用这种方法去避免生成复杂的AST(抽象语法树)
我们会将汇编代码的每一行分配成不同代码组:
// Matches: "label: INSTRUCTION (["')OPERAND1(]"'), (["')OPERAND2(]"')
// GROUPS: 1 2 3 7
var regex = /^[\t ]*(?:([.A-Za-z]\w*)[:])?(?:[\t ]*([A-Za-z]{2,4})(?:[\t ]+(\[(\w+((\+|-)\d+)?)\]|\".+?\"|\'.+?\'|[.A-Za-z0-9]\w*)(?:[\t ]*[,][\t ]*(\[(\w+((\+|-)\d+)?)\]|\".+?\"|\'.+?\'|[.A-Za-z0-9]\w*))?)?)?/;
// Regex group indexes
var GROUP_LABEL = 1;
var GROUP_OPCODE = 2;
var GROUP_OPERAND1 = 3;
var GROUP_OPERAND2 = 7;
在我们的程序中,我们的每一条指令都在单独的行上面。这使得我们很容易去解析他们,我们可以这样去解析每一行代码:
- 一行一行地分开,然后逐个处理。
- 审查这一行是否包含有有效地指令。
- 如果生成了有效地指令,我们将根据指令读取操作数,并且每个操作数将包含有0~2个操作数。并根据上面构建的许多函数去查找读取并操作操作数。
- 根据操作数类型(是间接寻址还是直接寻址或者是其他的方式)和操作码确定相应的操作码。
- 向代码串中添加最后的指令,包括操作数。然后跳转到步骤一直到所有的行都被解析。
function run(code) {
var opCode;
var lines = code.split('\n');
var code = [];
for (var i = 0, l = lines.length; i < l; i++) {
var match = regex.exec(lines[i]);
if (match[GROUP_OPCODE]) {
var instr = match[GROUP_OPCODE].toUpperCase();
switch (instr) {
case 'ADD':
var op1 = readOperand(match[GROUP_OPERAND1]);
var op2 = readOperand(match[GROUP_OPERAND2]);
if (op1.type === "register" && op2.type === "register")
opCode = OpCodes.ADD_REG_TO_REG;
else if (op1.type === "register" && op2.type === "regaddress")
opCode = OpCodes.ADD_REGADDRESS_TO_REG;
else if (op1.type === "register" && op2.type === "address")
opCode = OpCodes.ADD_ADDRESS_TO_REG;
else if (op1.type === "register" && op2.type === "number")
opCode = OpCodes.ADD_NUMBER_TO_REG;
else
throw "ADD does not support this operands";
code.push(opCode, op1.value, op2.value);
break;
case ...
case ...
default:
throw "Not a valid instruction: " + instr;
}
}
}
return code;
};
有关UI
其实到这里,所有的主体大致都已经讲完了,UI这块就是去如何展示这些代码了。所以也就不再赘述了。
这里就贴一下Schweigi的实例地址