仿真器是一项很酷的技术,它允许用户在另一个系统之上运行完全不同的系统。
仿真器有广泛的应用程序,例如在ARM设备上运行x86程序或在x86 Windows桌面上运行ARM Android应用程序,甚至在Raspberry PI上的仿真计算机/控制台上运行您最喜欢的复古游戏。
仿真整个系统时,仿真器软件需要处理该系统的所有硬件设备。这些可能不仅包括CPU,还包括视频系统、输入设备等。然而,仿真器的核心概念仍然是CPU的仿真。
在本文中,我们将探讨CPU是如何工作的,以及如何通过实现一台运行为幻想CPU编写的程序的简单机器来在软件中进行仿真。
众所周知,CPU是机器的核心,它们的主要工作是执行程序。程序只不过是计算机内存中的一系列指令。
此时,您可能会认为您的CPU了解JavaScript。虽然这是一个非常诱人的概念,但事实并非如此。如果出现新版本的JavaScript,请想象一下更换CPU!
实际情况是,CPU只能理解有限的指令集。这些是CPU工程团队在设计CPU时要理解的说明。
作为用户,您可以直接使用这些指令或用高级语言编码,然后使用编译器/系统将您的程序翻译成CPU能理解的指令集,从而创建程序。
无论用户如何创建程序,指令在内存中最终都是如下所示的一系列字节:
在这一点上,请再次查看上面的一系列数字。这是为我们幻想中的CPU编写的一个真正的程序。为了了解它的功能,我们需要在软件中实现一个虚拟CPU,然后我们将使用它来执行程序。
我们即将在软件中模拟的CPU是一个奇幻的CPU。它在现实世界中并不存在,但它非常接近于模拟真实CPU的工作方式。这台机器在一定程度上受到了这篇文章的启发。
CPU有4个通用数字寄存器:R0、R1、R2、R3(可以将这些寄存器视为可以存储数字的变量)。除了这些寄存器之外,CPU还可以操作堆栈,使其能够将值推入或弹出堆栈。
CPU通过指令操作。有些指令没有操作数,而另一些指令有几个操作数(将操作数视为参数)。
一系列指令组成了一个程序。程序中的指令编码如下:
每条指令都有一个与其相关联的唯一编号。为简单起见,指令代码、操作数和偶数地址都是正规数。因此,不需要字节或任何其他数据类型。一切都是数字!
因此,我们的节目是一系列的数字。每个数字占用单个内存单元。例如,具有3个操作数的指令将占用4个程序存储器单元(1个用于指令代码,3个用于操作数)。
现在让我们看看我们的CPU接受的指令集。虽然所有CPU(包括我们的幻想CPU)都执行二进制形式的指令,但是CPU工程团队通常将名称/助记符与CPU识别的指令关联起来。
使用助记符使人类编写程序的任务变得容易得多。如果一个人用指令助记法编写程序,就说他用汇编语言编写代码。只需一个简单的步骤就可以将这些用汇编语言编写的程序转换成二进制指令(例如,机器语言)。
我们梦幻CPU的说明和助记符非常简单直观。让我们花点时间来描述一下下面的所有内容。毕竟,如果我们想要将CPU模拟成软件,我们需要知道所有的CPU指令。
注意:在每条指令下面都指定了用于对该特定指令进行编码的编号。寄存器R0、R1、R2、R3也编码为数字0、1、2、3):
将regsrc中的值与regdst的值相加,并将结果存储在reg_dst中。
从regdst的值中减去regsrc的值,并将结果存储在reg_dst中。
仅当来自REG1<;REG2的值(如果REG1<;REG2,则JP Addr)跳转到地址Addr。
将CALL之后的指令地址压入堆栈,然后跳转到地址地址
从堆栈中弹出最后一个数字,假定是一个地址并跳转到该地址。
有了梦幻的CPU规格,我们现在可以用软件--JavaScript--来模拟CPU了。
如上所述,在真实机器上,程序存储在内存中。在我们的仿真器中,将使用一个简单的数组结构来仿真内存。事实上,我们只会在内存中放置一个程序。
我们的虚拟CPU将需要从该数组逐个获取指令并执行它们。CPU将使用名为“PC”(程序计数器)的专用寄存器跟踪需要获取的指令。
CPU仿真器的核心只是一个很大的“switch”语句,它将根据规范处理每条指令。
设PC=0;设HALTED=FALSE;函数RUN(){WHILE(!HALTED){runone();}}函数runone(){if(Halted)return;let instr=program[pc];switch(Instr){//根据规格处理每条指令//同时推进pc为下一次提取做准备//...}}。
就这样!。这就是我们出色的CPU仿真器的结构。处理指令也是一项非常简单的任务。只需仔细阅读和执行说明的规格即可。
Switch(Instr){//movr rdst,rsrc case 10:PC++;var rdst=PROGRAM[PC++];var rrc=PROGRAM[PC++];regs[rdst]=regs[rsrc];Break;//movv rdst,val case 11:pc++;var rdst=PROGRAM[PC++];var Val=PROGRAM[PC++];regs[rdc]
请慢慢来,阅读规范,并尝试查看是否可以完成此虚拟CPU实现。当您看到执行程序的结果时,您就会知道您做得很好。
正如您在上面的游乐场中看到的,我们的仿真器实现包含一个简单的虚拟机,该虚拟机最初从字节数组加载程序,然后要求幻想的CPU执行它。
如果您唯一的目的是加载我们提供的程序,这样加载是可以的。但是,如果您想用机器语言设计自己的程序并执行它们,这可能不是一种非常直观或高效的方式。
让我向您介绍一种小技术,您可以使用友好的CPU指令助记符来加载程序。只需定义几个常量:
常数MOVR=10;常数MOVV=11;常数ADD=20;常数SUB=21;常数推送=30;常数POP=31;常数JP=40;常数JL=41;常数呼叫=42;常数RET=50;常数打印=60;常数HALT=255;常数R0=0;常数R1=1;常数R2=2;常数R3=3;VM。加载([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、添加、R3、R2、POP、R2、PUSH、R2、JL、R3、R2、19、POP、R0、RET]);
至此,我们可以结束我们的文章了。不过,我们将利用这个机会快速实现一些在使用机器语言代码时常用的小工具。
在使用机器语言程序时,最重要的工具可能是汇编语言程序。
这个程序让用户把程序写成文本文件,用更容易使用的汇编语言编写,然后汇编器就会把汇编源代码转换成CPU能理解的二进制数据来完成繁重的工作。
我们将尝试构建一个执行基本工作的简化汇编程序。它的工作方式是这样的:
让code=`//在R0中加载值10//并调用Fibonacci routineMOVV R0,10CALL 6HALT...`;让Bytes=ASM。汇编(代码);
在使用机器语言时可以构建的另一个有用工具是反汇编程序。
反汇编程序接受二进制格式的程序作为输入,并以人类可读的方式输出它的清单-用汇编语言。
0 11 0 10 MOVV R0,103 42 6呼叫65 255 HALT6 30 0 PUSH R08 11 0 0 MOVV R0,011 11 11 1 MOVV R1,114 11 3 1 MOVV R3,117 60 6 Print R119 10 2 0 MOVR R2,R022 20 2 1 Add R2,R125 60 6 Print R227 10 0 1 MOVR R0,R130 10 1 2 MOVR R1,R233 11 21 1 MOVV R2,136 20 3 2 Add R3,R2。
该清单包含程序中每条指令的内存地址和二进制指令以及相关助记符。
你可能想知道我们是怎么想出打印斐波纳契数的汇编程序的?嗯,答案很简单。我们首先用JavaScript编写算法,然后逐步将其转换为汇编语言:
当然,一旦您获得了更多的汇编语言编写经验,您可以直接在一个步骤中完成这项任务!这只需要练习!
希望你喜欢这篇文章!即使在您能够仿真一个完整的系统之前还有很多工作要做,我希望本文为您提供了一个基本的概述,让您了解如何仿真系统的核心-CPU。
为了快速参考,我将本文中的所有程序放在一起,并在下面的游乐场中做了一些解释:
下一步,我邀请您为这个梦幻CPU创建额外的程序,如果需要,可以用新指令扩展CPU指令集来支持您的程序。
期待您的反馈和意见。如果您想了解有关此主题的更多信息,甚至可能想了解完整仿真器的实现,请告诉我。有关更多娱乐编码程序,请随时浏览codeguppy.com