在部分1我们写了CPU部分。在第二部分也是最后一部分,我们会写内存,控制台输出和用户界面。
内存
我们使用一个简单的数组来表示内存。每个地址存储一个JavaScript数值。理论上,内存可以储存超过一个字节大小的值,但我们的CPU代码确保所有值都是0~255.
内存有3个函数。Load从指定地址取出一个字节,Store把一个字节写入指定地址。两个函数都会在指定地址超过有效地址空间时抛出错误。第三个函数把所有内存值设为0.我们用它来初始化或是重置模拟器。
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++) {
memory[i] = 0;
}
};
完整代码在memory.js
控制台输出
最后一部分是控制台输出。它能显示24个字符,把内存的最后24个字符映射到输出。所以程序需要什么输出,把数据写到内存的最后24字节就行了。
汇编器
在我们完成我们的虚拟机的最后一部分后,我们还少了一块。我们如何把代码转化成CPU指令?
我们需要写自己的汇编器。汇编器会遍历每一行代码,解析操作符然后生成CPU指令。
为了解析代码,我们使用正则表达式。这只是简化的途径,因为不可能用一个正则型解析所有代码,我们会需要生成一个抽象语法树。这里我们只使用正则型。
正则型定义如下
// 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;
汇编器有一个主函数名为run。它接受代码作为参数。解析代码然后生成指令。返回值是一个指令数组,然后可以装入内存。这个返回的数组就是我们随后可以在模拟器中运行的程序。
在汇编语言中,每一条指令需要写在独立的行上。这样便于我们解析。运行汇编器可以:
- 逐行分割代码。每一行独立处理
- 确保每行有一条合法指令
- 根据指令读取操作数。每条指令有0-2个操作数。函数readOperand会解析这个值来查看操作数是寄存器还是内存地址还是常数。返回数据来源(寄存器,寄存器地址,内存地址,常数)和值
- 根据指令和操作数类型确定正确的操作码。正如第一部分所写,每个操作数对应一条指令。
- 增加最后一条指令包括操作数到代码数组。
- 回到步骤1,知道所有代码行都被解析,返回代码数组。
function run(code) {
var code = [];
var opCode;
var lines = code.split('\n');
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;
};
完整代码在asm.js
用户界面
用户界面,我推荐使用一种JavaScript框架。这会使得工作变得更容易。这里我们选择Angular。
我们使用两列的设计来展示模拟器。左边的列包含汇编代码输入和运行/停止键。右边的列包含CPU寄存器和标志位,内存,以及控制台输出。内存部分可以直接看到。
主要的用户界面元素是run/stop, step 和reset键。这些按钮控制模拟器。运行键会调用汇编器来生成和装载代码到内存,用一个计时器来运行一个CPU周期。stop键停止CPU周期计时器。step键使得用户可以逐步执行CPU周期来debug。最后一个reset键可以把整个模拟器重置到初始态。
HTML
<button ng-click="runOrStopSimulator()">{{ simulatorTimer && 'Stop' || 'Run' }}</button>
<button ng-click="runSimulatorOneStep()">Step</button>
<button ng-click="resetSimulator()">Reset</button>
JS Code:
var simulatorTimer = undefined;
var simulatorClockSpeed = 300;
function runOrStopSimulator() {
if (!assembler.isAssembled) {
assembler.run();
}
simulatorTimer = setInterval(runSimulatorOneStep, simulatorClockSpeed);
};
function runSimulatorOneStep() {
if (!assembler.isAssembled) {
assembler.run();
}
cpu.step();
};
function resetSimulator() {
if (simulatorTimer !== undefined) {
clearInterval(simulatorTimer);
}
simulatorTimer = undefined;
cpu.reset();
memory.reset();
};
下面的代码包含一个显示内存的简化版本代码。Angular之美在于,内存可以直接和用户界面表示绑定,所有内存的变化可以实时在用户界面显示出来。
<div class="console">
<div style="float:left;" class="console-character"
ng-repeat="m in memory | startFrom: 232 track by $index">
<span>{{ convertToChar(m) }}</span>
</div>
</div>
<div class="memory"
ng-repeat="m in memory track by $index">
<div ng-class="getMemoryCellCss($index)"
ng-switch="isInstruction($index)">
<small ng-switch-default>{{ m | number:displayHex }}</small>
<a ng-switch-when="true" ng-click="jumpToLine($index)">
<small>{{ m | number:displayHex }}</small>
</a>
</div>
</div>
完整代码在index.html