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

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

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

原文链接

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值