如何用 Lua 实现一个微型虚拟机?有点意思

本文介绍了如何使用 Lua 实现一个微型虚拟机,包括指令集设计、虚拟机工作原理、项目文件结构和可视化。文章详细讲解了指令集的定义、虚拟机的执行流程,以及如何利用 Love2D 进行虚拟机内部状态的可视化。通过这个项目,读者可以了解虚拟机的基础概念和实现方法。
摘要由CSDN通过智能技术生成

目录

  1. 介绍
  2. 机器指令模拟
  3. 最终核心代码
  4. 虚拟机内部状态可视化
  5. 完整项目代码
  6. 后续计划
  7. 参考

介绍

在网上看到一篇文章 使用 C 语言实现一个虚拟机, 这里是他的代码 Github示例代码, 觉得挺有意思, 作者用很少的一些代码实现了一个可运行的虚拟机, 所以打算尝试用 Lua 实现同样指令集的虚拟机, 同时也仿照此文写一篇文章, 本文中大量参考引用了这位作者的文章和代码, 在此表示感谢.

准备工作:

  • 一个 Lua 环境
  • 文本编辑器
  • 基础编程知识

为什么要写这个虚拟机?

原因是: 很有趣, 想象一下, 做一个非常小, 但是却具备基本功能的虚拟机是多么有趣啊!

指令集

谈到虚拟机就不可避免要提到指令集, 为简单起见, 我们这里使用跟上述那篇文章一样的指令集, 硬件假设也一样:

  • 寄存器: 本虚拟机有那么几个寄存器: A,B,C,D,E,F, 这些也一样设定为通用寄存器, 可以用来存储任何东西.
  • 程序: 本虚拟机使用的程序将会是一个只读指令序列.
  • 堆栈: 本虚拟机是一个基于堆栈的虚拟机, 我们可以对这个堆栈进行压入/弹出值的操作.

这样基于堆栈的虚拟机的实现要比基于寄存器的虚拟机的实现简单得多.

示例指令集如下:

PSH 5       ; pushes 5 to the stack
PSH 10      ; pushes 10 to the stack
ADD         ; pops two values on top of the stack, adds them pushes to stack
POP         ; pops the value on the stack, will also print it for debugging
SET A 0     ; sets register A to 0
HLT         ; stop the program

注意,POP 指令将会弹出堆栈最顶层的内容, 然后把堆栈指针, 这里为了方便观察, 我们会设置一条打印命令,这样我们就能够看到 ADD 指令工作了。我还加入了一个 SET 指令,主要是让你理解寄存器是可以访问和写入的。你也可以自己实现像 MOV A B(将A的值移动到B)这样的指令。HTL 指令是为了告诉我们程序已经运行结束。

说明: 原文的 C语言版 在对堆栈的处理上不太准确, 没有把 stack 的栈顶元素 "弹出", 在 POP 和 ADD 后, stack 中依然保留着应该弹出的数据,,

虚拟机工作原理

这里也是本文的核心内容, 实际上虚拟机很简单, 遵循这样的模式:

  • 读取: 首先,我们从指令集合或代码中读取下一条指令
  • 解码: 然后将指令解码
  • 执行: 执行解码后的指令

为聚焦于真正的核心, 我们现在简化一下这个处理步骤, 暂时忽略虚拟机的编码部分, 因为比较典型的虚拟机会把一条指令(包括操作码和操作数)打包成一个数字, 然后再解码这个数字, 因此, 典型的虚拟机是可以读入真实的机器码并执行的.

项目文件结构

正式开始编程之前, 我们需要先设置好我们的项目. 我是在 OSX 上写这个虚拟机的, 因为 Lua 的跨平台特性, 所以你也可以在 Windows 或 Linux 上无障碍地运行这个虚拟机.

首先, 我们需要一个 Lua 运行环境(我使用Lua5.3.2), 可以从官网下载对应于你的操作系统的版本. 其次我们要新建一个项目文件夹, 因为我打算最终把这个项目分享到 github 上, 所以用这个目录 ~/GitHub/miniVM, 如下:

Air:GitHub admin$ cd ~/GitHub/miniVM/
Air:miniVM admin$

如上,我们先 cd 进入 ~/GitHub/miniVM,或者任何你想放置的位置,然后新建一个 lua 文件 miniVM.lua。 因为现在项目很简单, 所以暂时只有这一个代码文件。

运行也很简单, 我们的虚拟机程序是 miniVM.lua, 只需要执行:

lua miniVM.lua

机器指令集

现在开始为虚拟机准备要执行的代码了. 首先, 我们需要定义虚拟机使用的机器指令集.

指令集数据结构设计

我们需要用一种数据结构来模拟虚拟机中的指令集.

C语言版

在 C语言版 中, 作者用枚举类型来定义机器指令集, 因为机器指令基本上都是一些从 0 到 n 的数字, 我们就像在编辑一个汇编文件, 使用类似 PSH 之类的助记符, 再翻译成对应的机器指令.

假设助记符 PSH 对应的机器指令是 0, 也就是把 PSH, 5 翻译为 0, 5, 但是这样我们读起来会比较费劲, 因为在 C 中, 以枚举形式写的代码更具可读性, 所以 C语言版 作者选择了使用枚举来设计机器指令集, 如下:

typedef enum {
   PSH,
   ADD,
   POP,
   SET,
   HLT
} InstructionSet;

Lua版的其他方案

看看我们的 Lua 版本如何选择数据结构, 众所周知 Lua 只有一种基本数据结构: table, 因此我们如果想使用枚举这种数据结构. 就需要写出 Lua 版的枚举来, 在网络上搜到这两篇文档:

第一篇是直接用 Lua 使用 C 定义的枚举, 代码比较多, 就不在这里列了, 不符合我们这个项目对于简单性的要求.

第二篇是用Luatable 模拟实现了一个枚举, 代码比较短, 列在下面.

function CreateEnumTable(tbl, index)   
    local enumtbl = {}   
    local enumindex = index or 0   
    for i, v in ipairs(tbl) do   
        enumtbl[v] = enumindex + i   
    end   
    return enumtbl   
end  

local BonusStatusType = CreateEnumTable({"NOT_COMPLETE", "COMPLETE", "HAS_TAKE"},-1)  

不过这种实现对我们来说也不太适合, 一方面写起来比较繁琐, 另一方面代码也不太易读, 所以需要设计自己的枚举类型.

最终使用的Lua版

现在的方案是直接选择用一个 table 来表示, 如下:

InstructionSet = {"PSH","ADD","POP","SET","HLT"}

这样的实现目前看来最简单, 可读性也很不错, 不过缺乏扩展性, 我们暂时就用这种方案.

测试程序数据结构设计

现在需要一段用来测试的程序代码了, 假设是这样一段程序: 把 5 和 6 相加, 把结果打印出来.

在 C语言版 中, 作者使用了一个整型数组来表示该段测试程序, , 如下:

const int program[] = {
    PSH, 5,
    P
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值