使用JavaScript实现幻想CPU的模拟器

仿真器是一种很酷的技术,允许用户在另一个系统之上运行完全不同的系统。

模拟器有广泛的应用程序,例如在ARM设备上运行x86程序,在x86 Windows桌面上运行ARM Android应用程序,甚至在Raspberry PI上的模拟计算机/控制台上运行您最喜欢的复古游戏。

当一个完整的系统被仿真时,仿真器软件需要处理该系统的所有硬件设备。这些不仅包括CPU,还包括视频系统、输入设备等。然而,仿真器的核心概念仍然是对CPU的仿真。

在本文中,我们将探讨CPU是如何工作的,以及如何通过实现一个简单的机它将运行为幻想CPU编写的程序。

想系统学习前端web的朋友,欢迎私信奕辰获取免费学习教程。

1.评论文章,没字数限制,一个字都行! 2.关注奕辰,成为的粉丝! 3.私信奕辰:“web”,“1”获取前端学习面试资料。

大家一起学习(群主会不定时更新学习资料,以及面试题文档)

或者可以添加我的个人微信号:wdnmd__xxx

小助理微信:lyf___1201

程序只是一系列字节。
众所周知,CPU是机器的核心,它们的主要任务是执行程序。程序只不过是计算机内存中的一系列指令。

此时,您可能会倾向于相信您的CPU了解JavaScript。虽然这是一个非常诱人的概念,但事实并非如此。如果出现了新版本的JavaScript,就会改变CPU!

事实上,CPU只理解有限的一组指令。这些是CPU工程团队设计的CPU要理解的指令。

作为用户,您可以直接使用这些指令创建程序,或者使用更高级别的语言编写代码,然后使用编译器/系统将程序转换为CPU理解的指令集。

不管用户是如何创建程序的,指令以一系列字节的形式在内存中结束,如下所示:

11,0,10,42,6,255,30,0,11,0,0,11,1,1,11,3,1,60,1,10,2,0,20,
2,1,60,2,10,0,1,10,1,2,11,2,1,20,3,2,31,2,30,2,41,3,2,19,31,0,50

CPU的作用是从内存中获取这些指令并执行它们。

在这一点上,请再看看上面的数列。这是一个真正的程序,为我们的幻想CPU。为了了解它的功能,我们需要在软件中实现一个虚拟CPU,然后我们将使用它来执行程序。

幻想cpu高级规格
我们将要在软件中模仿的CPU是一个幻想的CPU。它并不存在于现实世界中,但它非常接近真实CPU的工作方式。这台机器部分地受到了这篇文章的启发。

CPU有4个通用数字寄存器:R0、R1、R2、R3(将这些寄存器视为可以存储数字的变量)。除了这些寄存器之外,CPU还可以操作堆栈,使其能够在堆栈中上下推送或弹出值。

CPU由指令操作。有些指令没有操作数,而另一些指令有几个操作数(把操作数看作参数)。

一系列的指令构成了一个程序。指令在程序中编码如下:

INSTRUCTION1 [OPERAND1] [OPERAND2] [OPERAND3] INSTRUCTION2 [OPERAND1] ...

每条指令都有一个与其相关联的唯一编号。为了简单起见,指令码、操作数和偶数地址都是规则数。因此,不需要字节或任何其他数据类型。一切都是数字!

因此,我们的程序是一系列数字。每个数字占用一个单元格的内存。例如,具有3个操作数的指令将占用4个程序存储器单元(一个用于指令代码,三个用于操作数)。

指令
现在让我们看看CPU接受的指令集。尽管所有CPU(包括我们的幻想一)都以二进制形式执行指令,但CPU工程团队通常将名称/助记符与CPU识别的指令相关联。

Using mnemonics makes the task of writing programs much easier for humans. If one writes programs using the instructions mnemonics it is said that he codes in assembly language. It only takes a simple step to transform these programs written in assembly language into binary instructions (e.g. machine language).

指令和记忆我们的幻想CPU是相当简单和直观的。让我们花时间来描述以下所有内容。毕竟,如果我们想要将CPU模拟成软件,我们需要知道所有的CPU指令。

注意:在每条指令下指定用于编码该特定指令的编号。还将寄存器R0、R1、R2、R3编码为0、1、2、3):

将regsrc中的值加载到regdst中。例如regdst=regsrc

MOVR reg_dst, reg_src
MOVR = 10

将数值加载到寄存器regdst中。例如regdst=值

MOVV reg_dst, value
MOVV = 11

将regsrc的值添加到regdst的值,并将结果存储在reg_dst中。

ADD reg_dst, reg_src
ADD = 20

从regdst的值中减去regsrc的值,并将结果存储在reg_dst中。

SUB reg_dst, reg_src
SUB = 21

将reg_src的值推送到堆栈上。

PUSH reg_src
PUSH = 30

弹出堆栈中的最后一个值,并将其加载到寄存器reg_dst中。

POP reg_dst
POP = 31

跳过执行以寻址addr。就像一个后人!

JP addr
JP = 40

只有当REG 1

JL reg_1, reg_2, addr
JL = 41

将调用后的指令地址推到堆栈上,然后跳到Addaddr。

CALL addr
CALL = 42

从堆栈中弹出的最后一个数字,假设是一个地址并跳转到该地址。

RET
RET = 50

在屏幕上打印寄存器reg中包含的值。

PRINT reg
PRINT = 60

阻止我们的VM。一旦遇到暂停,虚拟CPU就不会执行指令。

HALT
HALT = 255

模拟CPU
随着幻想CPU规格,我们现在可以向前迈进,以模仿CPU的软件-在JavaScript。

如前所述,在实际机器上,程序存储在内存中。在我们的模拟器中,将使用一个简单的数组结构来模拟内存。事实上,我们只会在内存中放置一个程序。

let program = [11,0,10,42,6,255,30,0,11,0,0,11,1,1,11,3,1,60,1,10,2,0,20,
    2,1,60,2,10,0,1,10,1,2,11,2,1,20,3,2,31,2,30,2,41,3,2,19,31,0,50];

我们的虚拟CPU需要一个接一个地从这个数组中获取指令并执行它们。CPU将通过使用名为“PC”的专用寄存器(程序计数器)来跟踪需要获取的指令。

Fact: PC register is a real register on many physical CPU architectures.

CPU模拟器的核心是一个大的“开关”语句,它将根据规格处理每条指令。

let pc = 0;
let halted = false;    

function run()
{
    while(!halted)
    {
        runone();
    }
}


function runone()
{
    if (halted)
        return;

    let instr = program[pc];

    switch(instr)
    {
        // handle each instruction according to specs
        // also advance pc to prepare for the next fetch
        // ...
    }
}

就这样!这是我们光荣的CPU模拟器的结构。处理指令也是一项非常简单的任务。只需仔细阅读和执行指示的规格。

switch(instr)
{
    // movr rdst, rsrc
    case 10:
        pc++;
        var rdst = program[pc++];
        var rsrc = program[pc++];
        regs[rdst] = regs[rsrc];
        break;

    // movv rdst, val
    case 11:
        pc++;
        var rdst = program[pc++];
        var val = program[pc++];
        regs[rdst] = val;
        break;

    ...

}

慢慢来,阅读规范,看看是否可以完成这个虚拟CPU实现。当你看到执行程序的结果时,你就会知道你做得很好。

加载程序
正如您在上面的操场上看到的,我们的模拟器实现包括一个简单的虚拟机,它最初从一个字节数组加载程序,然后要求幻想CPU执行它。

vm.load([11,0,10,42,6,255,30,0,11,0,0,11,1,1,11,3,1,60,1,10,2,0,20,
   2,1,60,2,10,0,1,10,1,2,11,2,1,20,3,2,31,2,30,2,41,3,2,19,31,0,50]);

如果您的唯一目的是加载我们提供的程序,那么这样的加载是可以的。但是,如果您想用机器语言设计自己的程序并执行它们,这可能不是一种非常直观或有效的方法。

让我向您介绍一种小型技术,您可以使用一个人友好的CPU指令助记符来加载程序。它只需要定义几个常量:

const MOVR = 10;
const MOVV = 11;
const ADD  = 20;
const SUB  = 21;
const PUSH = 30;
const POP  = 31;
const JP   = 40;
const JL   = 41;
const CALL = 42;
const RET  = 50;
const PRINT= 60;
const HALT = 255;

const R0 = 0;
const R1 = 1;
const R2 = 2;
const R3 = 3;

vm.load([
    MOVV, R0, 10,
    CALL, 6,    
    HALT,

    // PrintFibo: (addr = 6)
    PUSH, R0,

    MOVV, R0, 0,
    MOVV, R1, 1,
    MOVV, R3, 1,
    PRINT, R1,

    // continue: (addr = 19)
    MOVR, R2, R0,
    ADD, R2, R1,
    PRINT, R2,

    MOVR, R0, R1,
    MOVR, R1, R2,

    MOVV, R2, 1,
    ADD, R3, R2,

    POP, R2,
    PUSH, R2,

    JL, R3, R2, 19,

    POP, R0,

    RET
]);

使用这个技巧就像在JavaScript中用汇编语言编程一样。

汇编程序

在使用机器语言程序时,最重要的工具可能是汇编程序。

该程序允许用户以文本文件的形式编写程序,使用汇编语言更容易,然后汇编程序通过将汇编源代码转换为CPU理解的二进制数据来完成繁重的工作。

我们将尝试构建一个简化的汇编程序来完成基本的工作。它的工作方式如下:

let code = `
// Loads value 10 in R0 
// and calls Fibonacci routine

MOVV R0, 10
CALL 6
HALT
...
`;

let bytes = asm.assemble(code);

反汇编程序

在使用机器语言时可以构建的另一个有用工具是反汇编程序。

反汇编程序以二进制格式的程序作为输入,并以人类可读的方式以汇编语言输出其列表。

在以下程序上运行时:

let src = asm.disassemble(
    [11,0,10,42,6,255,30,0,11,0,0,11,1,1,11,3,1,60,1,10,2,0,20,
    2,1,60,2,10,0,1,10,1,2,11,2,1,20,3,2,31,2,30,2,41,3,2,19,31,0,50]
);

...我们的反汇编程序应该输出以下内容:

0   11 0 10         MOVV R0, 10
3   42 6            CALL 6
5   255             HALT
6   30 0            PUSH R0
8   11 0 0          MOVV R0, 0
11  11 1 1          MOVV R1, 1
14  11 3 1          MOVV R3, 1
17  60 6            PRINT R1
19  10 2 0          MOVR R2, R0
22  20 2 1          ADD R2, R1
25  60 6            PRINT R2
27  10 0 1          MOVR R0, R1
30  10 1 2          MOVR R1, R2
33  11 2 1          MOVV R2, 1
36  20 3 2          ADD R3, R2
39  31 2 2          POP R2
41  30 2            PUSH R2
43  41 3 2 19       JL R3, R2, 19
47  31 0 2          POP R0
49  50              RET

该清单包含内存地址和二进制指令,以及程序中每条指令的相关助记符。

关于Fibonacci程序

你可能想知道我们是如何想出程序来打印斐波纳契数字的?答案很简单。我们首先用JavaScript编写算法,然后逐步将其转换为汇编程序:


image.png

当然,一旦你在汇编中获得更多的写作经验,你就可以在一步之内直接完成这个任务!这只需要练习!

结语
希望你喜欢这篇文章!即使在您能够模拟一个完整的系统之前,还有很多工作要做,我希望本文能够为您提供一个基本的概述,说明如何才能模拟系统的核心--CPU。

想系统学习前端web的朋友,欢迎私信奕辰获取免费学习教程。

1.评论文章,没字数限制,一个字都行! 2.关注奕辰,成为的粉丝! 3.私信奕辰:“web”,“1”获取前端学习面试资料。

大家一起学习(群主会不定时更新学习资料,以及面试题文档)

或者可以添加我的个人微信号:wdnmd__xxx

小助理微信:lyf___1201

你可能感兴趣的:(使用JavaScript实现幻想CPU的模拟器)