主要参考和借鉴 silan-liu 微微笑的蜗牛 的 《听说你想写个虚拟机》系列
最近《中文编程语言——青语言》发布了,我表示即震惊又惭愧,几年前就开始准备也想自己写个中文编程,奈何每次自己都想的大而全导致遗落在某个角落。
在看了青语言的源码后,我感觉我又可以了。
为了避免自己好高骛远,还是从最底层的来,不要想着一下就搞定,可以多借鉴和学习已有的模式,从而真正积累起来。
最起码我还在路上。
虚拟机
虚拟机就像名字说的那种,它就是一个虚拟机,像VMware一样的虚拟机,但是,这个系列写下来,顶多写一个类似C#运行时(CLR) 或者 java的虚拟机(JVM)。
当然,肯定比CLR小很多就是了,主要还是偏向虚拟机 VMware的概念,偏向于用软件来模拟硬件的CPU,寄存器,堆栈等设备信息,来实现指令的执行,这样的虚拟机。
还是比较基础的,对计算机编译基础想有简单动手能力的,可以跟我一起,我动手能力有点差,正好适合我。
依稀记得在大学时期用汇编写51的时钟案例了,可惜太早了,都忘求了,现在,慢慢捡起来。
同时,也能让你理解一门语言是如何实现跨平台的(实际上就是每个平台运行了相应的虚拟机 [ 运行时/解释器 ] )
今天的任务
主要是通过最少的指令来实现一个加法,并发结果输出到控制台上。
大概有以下几个指令
指令集
/// <summary>
/// 指令集
/// </summary>
public enum InstructionSet
{
/// <summary>
/// PUSH 5;
/// 将数据放入栈中
/// </summary>
PUSH,
/// <summary>
/// 加法
/// 取出栈中的两个操作数,执行加法操作,然后,放入栈中
/// </summary>
ADD,
/// <summary>
/// 取栈顶数据
/// </summary>
POP,
/// <summary>
/// 虚拟机停止运行
/// </summary>
HALT
}
指令集
这里有一个重点,我提一下那就是指令集是CPU带的,比如ARM指令集,Intel 指令集,以及RISC(精简指令集)。
指令集对应的其实就是汇编集,比如 ARM汇编,指令(机器码)与汇编(指令码)是一一对应的。
指令就是CPU对外提供的它能支持的各种操作,比如,加减乘除,跳转,判断等,而CPU与CPU之间是有差异的,它们对外提供的指令集不同。但是,大部分是相似的。
所以,真正执行指令的是CPU,而映射到虚拟机这个概念,执行的就是虚拟机本身。
目前提供了4条最简单的指令。
栈结构
栈结构很容易理解,相对于队列来讲。队列是先进先出。
从图上可以看到,从左侧进入,右侧出去,里面是有序的,从右到左排列,就像去超市结账排队一样。
栈是后进先出。
从口里进去,然后,从口里出去,就像堆了一堆砖头,你总得从最上边把砖头拿走(最下边的不好抽出来)。
理解了堆栈的结构,就有助于理解,接下来的操作了。
逻辑
PUSH,就是堆栈的进操作。堆栈指针要在执行后+1;(累加),它有一个操作数,比如:PUSH 5 ; 就是把5压到堆栈上。
POP,就是堆栈的出操作。堆栈指针要在执行后-1;(累减),它只能获取一个数据,比如:POP ; 就弹出栈顶的数据。
ADD,就是对加数与被加数进行加操作,它只能从栈里获取两个数据,并把结果PUSH到堆栈上。
HALT ,没有操作数,就是任务执行完毕的意思。
寄存器相关
/// <summary>
/// 指令指针寄存器
/// </summary>
public int IP { get; private set; } = 0;
/// <summary>
/// 栈指针寄存器
/// </summary>
public int StackPointer { get; private set; } = -1;
/// <summary>
/// 栈
/// </summary>
public int[] Stack { get; private set; } = new int[256];
主要是IP,指令指针寄存器和栈指针寄存器以及我们所定义的栈。
控制指令指针IP,就可以让CPU(虚拟机)从所设定的位置,继续往下执行我们给它的指令集合(程序)。
而,栈指针寄存器(sp)是控制和表现Stack栈结构的辅助索引。
虚拟机相关
public class VM
{
public bool Runing { get; private set; }
/// <summary>
/// 指令指针寄存器
/// </summary>
public int IP { get; private set; } = 0;
/// <summary>
/// 栈指针寄存器
/// </summary>
public int StackPointer { get; private set; } = -1;
/// <summary>
/// 栈
/// </summary>
public int[] Stack { get; private set; } = new int[256];
public void Run(int[] Instructions)
{
Start();
while (Runing)
{
int instruction = Instructions[IP];
Eval((InstructionSet)instruction, Instructions);
IP++;
}
Console.WriteLine("VM 虚拟机 退出运行!");
}
private void Eval(InstructionSet instruction, int[] Instructions)
{
switch (instruction)
{
case InstructionSet.HALT:
Runing = false;
Console.WriteLine("虚拟机停止");
break;
case InstructionSet.PUSH:
StackPointer++;
++IP;
Stack[StackPointer] = Instructions[IP];
break;
case InstructionSet.POP:
int popValue = Stack[StackPointer];
StackPointer--;
Console.WriteLine($"poped {popValue}");
break;
case InstructionSet.ADD:
//从栈中取出两个操作数,相加,然后,保存在栈中
int a = Stack[StackPointer];
StackPointer--;
int b = Stack[StackPointer];
StackPointer--;
int sum = a + b;
StackPointer++;
Stack[StackPointer] = sum;
break;
}
}
public void Start()
{
Runing = true;
IP = 0;
Stack = new int[256];
StackPointer = -1;
}
}
整体逻辑就是,先初始化,start,恢复初始环境,相当于重启,然后,根据程序,依次往下执行,直到遇到停止的指令,才会退出执行。
主函数
static void Main(string[] args)
{
VM VM = new VM();
var program = new List<int>()
{
(int)InstructionSet.PUSH,5,
(int)InstructionSet.PUSH,6,
(int)InstructionSet.ADD,
(int)InstructionSet.POP,
(int)InstructionSet.HALT
};
VM.Run(program.ToArray());
Console.ReadLine();
}
因为我的操作码是枚举来的,所以,在C#里需要转码一下,当然,后期的操作码和操作数应该能合成一条指令,类似字节码。
program就是定义的程序(汇编),压入了5,压入了6,执行加法,然后,弹出结果,停止虚拟机的执行。
结果如下
弹出了结果,11,结果正确。
然后,依次各个退出,顺利完成极简版的虚拟机。
敬请期待下篇《跟我一起写个虚拟机 .Net 7(二)》
代码地址
https://github.com/kesshei/VirtualMachineDemo.git
https://gitee.com/kesshei/VirtualMachineDemo.git
致谢
我主要是看 silan-liu 微微笑的蜗牛 的 《听说你想写个虚拟机》系列。
作者写的真不错,有喜欢的,可以去支持一下。
阅
一键三连呦!,感谢大佬的支持,您的支持就是我的动力!