这篇博客的目标是制作一个简单的模拟器,能够把代码变成CPU指令,并且在一个虚拟的机器上运行。这样来学习汇编语言再好不过了,因为无需顾虑操作系统相关的细节。
模拟器
模拟器使用JavaScript Angular,可以在任何终端的网页浏览器里运行。尽管做了很多简化以及有很多限制,基本结构和所有的模拟器是一样的。这个虚拟的机器包含下面的组成部分。
- 内存(256字节)。内存里储存程序代码以及可供程序运行时存储使用。
- 8位的CPU。CPU从内存读取指令并执行。
- 控制台输出。控制台输出使用内存映射即把内存特定部分映射到控制台输出。如果要输出内容,只要往指定区域写入数据即可。
CPU
模拟器的核心是CPU。这个CPU包含4个通用寄存器(GP),他们的职责是存储执行命令时用到的值。CPU如何知道执行哪条命令呢?我们使用一个指令指针(IP)指向要执行的命令。技术上来说,IP只是一个包含了更多附加功能的寄存器。IP存储下一条指令在内存中的位置信息,在每个CPU指令周期里,CPU会读取一条指令并执行。
为了运行更多的程序,比如提供if-else功能,CPU需要基于上一次运算结果作出决定。这些结果存储为1位的标志位。我们的CPU包含3个不同的标志位。
- Zero(Z)。零标志,最重要的一个。如果指令结果是0,那么这位置为1,否则为0。
- Carry(C)。进位标志,如果一条指令进位了,就置为1。
- Fault。错误,如果一条指令导致CPU错误状态(比如除以0)就置为1。在错误的状态下,CPU停止并且不再继续执行指令。
最后我们升级我们的CPU,给它添加一个栈指针寄存器(sp)。SP指向内存中当前栈的位置。随着程序运行进行存储、函数实现时增大或减小。
首先我们定义所有的寄存器,指针和标志位。然后一个reset函数来初始化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;
}
每个CPU指令周期,执行一条命令。指令可能是加(Addition),减(Subtraction),跳(Jump Branching分支),乘(Multiplication),除(Division)等等。
对每种操作有一个特定的指令。两个寄存器相加的情形和寄存器与常数相加的情形是不同的,使用的是不同的指令。意味着即使是我们的简单模拟器,也会有很多指令。就加运算(Addition)来说,我们要实现4种指令。
一条指令包含操作码(opcode)和操作数(operand)。第一个操作数通常是目标(target),第二个是源(source)。如果一条指令只有一个操作数,那么它既是目标也是源。
举例,加法指令定义如下
[opcode] [oprand1] [oprand2]
0x0a reg, reg
0x0b reg, [address]
0x0c reg, address
0x0d reg, constant
为了便于使用,我们把操作码用更有意义的名字来表示。例如0x0a替换为ADD_REG_TO_REG。完整列表在opcode.js
所有支持指令的文档在simulator documentation
代码首先用IP从内存读取下一条指令。然后,从内存提取指令里的操作数。计算出结果后设置全部的CPU标志位的值。指令执行完成,IP增加后,CPU周期就算结束了。
function step() {
if (fault?) {
throw "FAULT. Reset to CPU continue."
}
var instr = memory.load(ip)
switch(instr) {
case opcodes.ADD_REG_TO_REG:
// 读取第一个操作数:目标寄存器
var regTo = memory.load(ip+1);
// 读取第二个操作数:源寄存器
var regFrom = momory.load(ip+2);
// 执行指令。寄存器相加
var value = processResult(readRegister(regTo) + readRegister(regFrom));
// 把值写到目标寄存器
writeRegister(regTo, value);
// 增加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;
};
你会注意到指令指针(IP)增加3而不是1。这是因为一条指令占用1个字节存储操作码,而操作数也要各自额外占用1个字节。所以加指令一共占用3字节的内存。
完整代码在这:cpu.js。这里包含额外的检查,确保寄存器和内存地址是合法的。
下一篇会实现内存,控制台输出,界面以及如何把汇编代码变成指令。