简单的汇编模拟器教程(JavaScript)部分1[译]

来源:互联网 发布:入门单板吉他推荐 知乎 编辑:程序博客网 时间:2024/06/01 15:18

这篇博客的目标是制作一个简单的模拟器,能够把代码变成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,        reg0x0b        reg,        [address]0x0c        reg,        address0x0d        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。这里包含额外的检查,确保寄存器和内存地址是合法的。

下一篇会实现内存,控制台输出,界面以及如何把汇编代码变成指令。

原文链接

0 0