-
用VMware Workstation Pro搭建虚拟机为什么没有声音
2019-07-15 10:51:57这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、...这里写自定义目录标题
欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的
代码片
.// An highlighted block var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
项目 Value 电脑 $1600 手机 $12 导管 $1 设定内容居中、居左、居右
使用
:---------:
居中
使用:----------
居左
使用----------:
居右第一列 第二列 第三列 第一列文本居中 第二列文本居右 第三列文本居左 SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE ASCII HTML Single backticks 'Isn't this fun?'
‘Isn’t this fun?’ Quotes "Isn't this fun?"
“Isn’t this fun?” Dashes -- is en-dash, --- is em-dash
– is en-dash, — is em-dash 创建一个自定义列表
- Markdown
- Text-to-HTML conversion tool
- Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 是通过欧拉积分
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图::
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎
-
用C语言来实现一个简单的虚拟机
2020-12-31 17:04:38文本编辑器——我建议使用基于IDE的文本编辑器,我使用 Emacs; 基础编程知识——最基本的变量,流程控制,函数,数据结构等; Make 脚本——能使程序更快一点。 为什么要写个虚拟机? 有以下原因: 想深入了解... -
如何用 Lua 实现一个微型虚拟机?有点意思
2020-12-23 17:13:35目录 介绍 机器指令模拟 最终核心代码 虚拟机内部状态可视化 完整项目代码 后续计划 参考 介绍 ...在网上看到一篇文章使用 C 语言实现一个... 文本编辑器 基础编程知识 为什么要写这个虚拟机? 原因是: 很...目录
- 介绍
- 机器指令模拟
- 最终核心代码
- 虚拟机内部状态可视化
- 完整项目代码
- 后续计划
- 参考
介绍
在网上看到一篇文章 使用 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
定义的枚举, 代码比较多, 就不在这里列了, 不符合我们这个项目对于简单性的要求.第二篇是用
Lua
的table
模拟实现了一个枚举, 代码比较短, 列在下面.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, PSH, 6, ADD, POP, HLT };
注意:
PSH
是前面C语言版
定义的枚举值, 是一个整数0
, 其他类似.我们的
Lua
版暂时使用最简单的结构:表, 如下:program = { "PSH", "5", "PSH", "6", "ADD", "POP", "HLT" }
这段代码具体来说, 就是把
5
和6
分别先后压入堆栈, 调用ADD
指令, 它会将栈顶的两个值弹出, 相加后再把结果压回栈顶, 然后我们用POP
指令把这个结果弹出, 最后HLT
终止程序.很好, 我们有了一个完整的测试程序. 现在, 我们描述了虚拟机的
读取, 解码, 求值
的详细过程. 但是实际上我们并没有做任何解码操作, 因为我们这里提供的就是原始的机器指令. 也就是说, 我们后续只需要关注读取
和求值
两个操作. 我们将其简化为fetch
和eval
两个函数.从测试程序中取得当前指令
因为我们的
Lua
版把测试程序存为一个字符串表program
的形式, 因此可以很简单地取得任意一条指令.虚拟机有一个用来定位当前指令的地址计数器, 一般被称为
指令指针
或程序计数器
, 它指向即将执行的指令, 通常被命名为IP
或PC
. 在我们的Lua
版中, 因为表的索引以1
开始, 所以这样定义:-- 指令指针初值设为第一条 IP = 1
那么结合我们的
program
表, 很容易理解program[IP]
的含义: 它以IP
作为表的索引值, 去取program
表中的第1
条记录, 完整代码如下:IP = 1 instr = program[IP];
如果我们打印
instr
的值, 会返回字符串PSH
, 这里我们可以写一个取指函数fetch
, 如下:function fetch() return program[IP] end
该函数会返回当前被调用的指令, 那么我们想要取得下一条指令该如何呢? 很简单, 只要把指令指针
IP
加1
即可:x = fetch() -- 取得指令 PSH IP = IP + 1 -- 指令指针加 1 y = fetch() -- 取得操作数 5
我们知道, 虚拟机是会自动执行的, 比如指令指针会在每执行一条指令时自动加
1
指向下一条指令, 那么我们如何让这个虚拟机自动运行起来呢? 因为一个程序直到它执行到HLT
指令时才会停止, 所以我们可以用一个无限循环来模拟虚拟机, 这个无限循环以遇到HLT
指令作为终止条件, 代码如下:running = true -- 设置指令指针指向第一条指令 IP = 1 while running do local x = fetch() if x == "HLT" then running = false end IP = IP + 1 end
说明: 代码中的
local
表示x
是一个局部变量, 其他不带local
的都是全局变量一个虚拟机最基本的核心就是上面这段代码了, 它揭示了最本质的东西, 我们可以把上面这段代码看做一个虚拟机的原型代码, 更复杂的虚拟机都可以在这个原型上扩展.
不过上面这段代码什么具体工作也没做, 它只是顺序取得程序中的每条指令, 检查它们是不是停机指令
HLT
, 如果是就跳出循环, 如果不是就继续检查下一条, 相当于只执行了HLT
.执行每一条指令
但是我们希望虚拟机还能够执行其他指令, 那么就需要我们对每一条指令分别进行处理了, 这里最适合的语法结构就是
C语言
的switch-case
了, 让switch
中的每一个case
都对应一条我们定义在指令集InstructionSet
中的机器指令, 在C语言版
中是这样的:void eval(int instr) { switch (instr) { case HLT: running = false; break; } }
不过
Lua
没有switch-case
这种语法, 我们就用if-then-elseif
的结构来写一个指令执行函数, 也就是一个求值函数eval
, 处理HLT
指令的代码如下:function eval(instr) if instr == "HLT" then running = false end end
我们可以这样调用
eval
函数:running = true IP = 1 while running do eval(fetch()) IP = IP + 1 end
增加对其他指令处理的
eval
:function eval(instr) if instr == "HLT" then running = false elseif instr == "PSH" then -- 这里处理 PSH 指令, 具体处理后面添加 elseif instr == "POP" then -- 这里处理 POP 指令, 具体处理后面添加 elseif instr == "ADD" then -- 这里处理 ADD 指令, 具体处理后面添加 end end
栈的数据结构设计
因为我们的这款虚拟机是基于栈的, 一切的数据都要从存储器搬运到栈中来操作, 所以我们在为其他指令增加具体的处理代码之前, 需要先准备一个栈.
注意: 我们这里要使用一种最简单的栈结构:数组
在
C语言版
中使用了一个固定长度为256
的数组, 同时需要一个栈指针SP
, 它其实就是数组的索引, 用来指向栈中的元素, 如下:int sp = -1; int stack[256];
我们的
Lua
版也准备用一个最简单的表来表示栈, 如下:SP = 0 stack = {}
注意: 我们知道
C
的数组是从0
开始的, 而Lua
的数组是从1
开始的, 所以我们的代码中以1
作为数组的开始, 那么SP
的初值就要设置为0
.12月30号我们会做一个skynet的训练营直播,感兴趣的朋友可以进群973961276了解详情跟大家一起交流学习哦!并且群里还整理超多的视频资料和面经分享
各种指令执行时栈状态变化的分析
下面是一个形象化的栈, 最左边是栈底, 最右边是栈顶:
[] // empty PSH 5 // put 5 on **top** of the stack [5] PSH 6 [5, 6] POP [5] POP [] // empty PSH 6 [6] PSH 5 [6, 5]
先手动分析一下我们的测试程序代码执行时栈的变化情况, 先列出测试程序:
PSH, 5, PSH, 6, ADD, POP, HLT
先执行
PSH, 5,
也就是把5
压入栈中, 栈的情况如下:[5]
再执行
PSH, 6,
也就是把6
压入栈中, 栈的情况如下:[5,6]
再执行
ADD
, 因为它需要2
个参数, 所以它会主动从栈中弹出最上面的2
个值, 把它们相加后再压入栈中, 相当于执行2
个POP
, 再执行一个PSH
, 栈的情况如下:[5, 6] // pop the top value, store it in a variable called a a = pop; // a contains 6 [5] // stack contents // pop the top value, store it in a variable called b b = pop; // b contains 5 [] // stack contents // now we add b and a. Note we do it backwards, in addition // this doesn't matter, but in other potential instructions // for instance divide 5 / 6 is not the same as 6 / 5 result = b + a; push result // push the result to the stack [11] // stack contents
上面这段描述很重要, 理解了这个你才清楚如何用代码来模拟栈的操作.
上述没有提到栈指针
SP
的变化, 实际上它默认指向栈顶元素, 也就是上述栈中最右边那个元素的索引, 我们看到, 最右边的元素的索引是一直变化的.空的栈指针在
C语言版
的虚拟机中被设置为-1
.如果我们在栈中压入
3
个值, 那么栈的情况如下:SP指向这里(SP = 3) | V [1, 5, 9] 1 2 3 <- 数组下标
现在我们先从栈上弹出
POP
出一个值, 我们如果只修改栈指针SP
, 让其减1
, 如下:SP指向这里(SP = 2) | V [1, 5, 9] 1 2 <- 数组下标
注意: 我们不能指定弹出栈中的某个元素, 只能弹出位于栈顶的元素
因为我们是最简版的山寨栈, 所以执行弹出指令时只修改栈指针的话, 栈中的那个应该被弹出的
9
实际上还在数组里, 所以我们在模拟POP
指令时需要手动把弹出的栈顶元素从栈中删除, 这样做的好处在后面可视化时就清楚了.各指令的处理逻辑
经过上面的详细分析, 我们应该对执行
PSH
和POP
指令时栈的变化(特别是栈指针和栈数组)比较清楚了, 那么先写一下压栈指令PSH 5
的处理逻辑, 当我们打算把一个值压入栈中时, 先调整栈顶指针的值, 让其加1
, 再设置当前SP
处栈的值stack[SP]
, 注意这里的执行顺序:SP = -1; stack = {}; SP = SP + 1 stack[SP] = 5
在
C语言版
中写成这样的:void eval(int instr) { switch (instr) { case HLT: { running = false; break; } case PSH: { sp++; stack[sp] = program[++ip]; break; } } }
C语言版
作者用了不少sp++
,stack[sp] = program[++ip]
之类的写法, 但是我觉得这里这么用会降低易读性, 因为读者不太容易看出执行顺序, 不如拆开来写成sp = sp + 1
跟ip = ip + 1
, 这样看起来更清楚.所以在我们
Lua
版的eval
函数中, 可以这样写PSH
指令的处理逻辑:function eval(instr) if instr == "HLT" then running = false elseif instr == "PSH" then -- 这里处理 PSH 指令, 具体处理如下 SP = SP + 1 -- 指令指针跳到下一个, 取得 PSH 的操作数 IP = IP + 1 stack[SP] = program[IP] elseif instr == "POP" then -- 这里处理 POP 指令, 具体处理后面添加 elseif instr == "ADD" then -- 这里处理 ADD 指令, 具体处理后面添加 end end
分析一下我们的代码, 其实很简单, 就是发现当指令是
PSH
后, 首先栈顶指针SP
加1
, 接着指令指针加1
, 取得PSH
指令后面紧跟着的操作数, 然后把栈数组的第一个元素stack[SP]
赋值为测试程序数组中的操作数program[IP]
.接着是
POP
指令的处理逻辑, 它要把栈顶指针减1
, 同时最好从栈数组中删除掉弹出栈的元素:elseif instr == "POP" then -- 这里处理 POP 指令, 具体处理如下 local val_popped = stack[SP] SP = SP - 1 elseif ...
ADD指令的处理逻辑
最后是稍微复杂一些的
ADD
指令的处理逻辑, 因为它既有压栈操作, 又有出栈操作, 如下:elseif instr == "ADD" then -- 这里处理 ADD 指令, 具体处理如下 -- 先从栈中弹出一个值 local a = stack[SP] stack[SP] = 0 SP = SP - 1 -- 再从栈中弹出一个值 local b = stack[SP] stack[SP] = 0 SP = SP - 1 -- 把两个值相加 local result = a + b -- 把相加结果压入栈中 SP = SP + 1 stack[SP] = result end
最终代码
很好, 现在我们
Lua
版的虚拟机完成了, 完整代码如下:-- 项目名称: miniVM -- 项目描述: 用 Lua 实现的一个基于栈的微型虚拟机 -- 项目地址: https://github.com/FreeBlues/miniVM -- 项目作者: FreeBlues -- 指令集 InstructionSet = {"PSH","ADD","POP","SET","HLT"} Register = {A, B, C, D, E, F,NUM_OF_REGISTERS} -- 测试程序代码 program = {"PSH", "5", "PSH", "6", "ADD", "POP", "HLT"} -- 指令指针, 栈顶指针, 栈数组 IP = 1 SP = 0 stack = {} -- 取指令函数 function fetch() return program[IP] end -- 求值函数 function eval(instr) if instr == "HLT" then running = false elseif instr == "PSH" then -- 这里处理 PSH 指令, 具体处理如下 SP = SP + 1 -- 指令指针跳到下一个, 取得 PSH 的操作数 IP = IP + 1 stack[SP] = program[IP] elseif instr == "POP" then -- 这里处理 POP 指令, 具体处理如下 local val_popped = stack[SP] SP = SP - 1 elseif instr == "ADD" then -- 这里处理 ADD 指令, 具体处理如下 -- 先从栈中弹出一个值 local a = stack[SP] stack[SP] = 0 SP = SP - 1 -- 再从栈中弹出一个值 local b = stack[SP] stack[SP] = 0 SP = SP - 1 -- 把两个值相加 local result = a + b -- 把相加结果压入栈中 SP = SP + 1 stack[SP] = result -- 为方便查看测试程序运行结果, 这里增加一条打印语句 print(stack[SP]) end end -- 虚拟机主函数 function main() running = true while running do eval(fetch()) IP = IP + 1 end end -- 启动虚拟机 main()
执行结果如下:
Air:miniVM admin$ lua miniVM.lua 11.0 Air:miniVM admin$
本项目代码可以到群973961276里 下载.
虚拟机内部状态可视化
应该说目前为止我们的虚拟机已经完美地实现了, 不过美中不足的是它的一切动作都被隐藏起来, 我们只能看到最终运行结果, 当然了我们也可以增加打印命令来显示各条指令执行时的情况, 但是这里我们打算把虚拟机运行时内部状态的变化用图形的方式绘制出来, 而不仅仅是简单的
print
文本字符.框架选择:Love2D
这里我们选择使用
Love2D
来绘图, 原因有这么几个:- 简单好用:结构很简单, 框架很好用
- 跨平台:同时支持
Windows, Mac OS X, Linux, Android 和 iOS
- 免费开源:直接下载了就能用
Love2D的简单介绍
用
Love2D
写程序非常简单方便, 首先新建一个目录love
(目录名可以随便起), 接着在该目录下新建一个文件main.lua
(该文件必须使用这个名字), 然后在main.lua
中编写游戏逻辑即可, 可以试试这段代码:function love.draw() love.graphics.print("Hello World", 400, 300) end
执行命令是用
love
调用目录, 它会自动加载目录内的main.lua
文件, 命令如下:love ./love
它会新建一个窗口, 然后打印
Hello World
.把项目修改为 Love2D 的形式
其实很简单, 就是在项目文件目录下新建个目录
miniVM
, 然后拷贝miniVM.lua
代码文件到这个新目录中, 并将新目录中的代码文件名修改为main.lua
.Air:miniVM admin$ cp ./miniVM.lua ./miniVM/main.lua Air:miniVM admin$ tree . ├── README.md ├── miniVM │ └── main.lua └── miniVM.lua 1 directory, 3 files Air:miniVM admin$
按照
Love2D
的代码框架要求修改整合代码, 在main.lua
中增加一个加载函数love.load
, 把所有只执行一次的代码放进去, 再增加一个刷新函数love.update
, 把所有需要重复执行的代码放进去, 最后增加一个love.draw
函数, 把所有用于绘图的代码放进去, 修改后的main.lua
如下:function love.load() -- 指令集 InstructionSet = {"PSH","ADD","POP","SET","HLT"} Register = {A, B, C, D, E, F,NUM_OF_REGISTERS} -- 测试程序代码 program = {"PSH", "5", "PSH", "6", "ADD", "POP", "HLT"} -- 指令指针, 栈顶指针, 栈数组 IP = 1 SP = 0 stack = {} running = true end function love.update(dt) -- 虚拟机主体 if running then eval(fetch()) IP = IP + 1 end end function love.draw() love.graphics.print("Welcome to our miniVM!", 400, 300) end -- 取指令函数 function fetch() return program[IP] end -- 求值函数 function eval(instr) if instr == "HLT" then running = false elseif instr == "PSH" then -- 这里处理 PSH 指令, 具体处理如下 SP = SP + 1 -- 指令指针跳到下一个, 取得 PSH 的操作数 IP = IP + 1 stack[SP] = program[IP] elseif instr == "POP" then -- 这里处理 POP 指令, 具体处理如下 local val_popped = stack[SP] SP = SP - 1 elseif instr == "ADD" then -- 这里处理 ADD 指令, 具体处理如下 -- 先从栈中弹出一个值 local a = stack[SP] stack[SP] = 0 SP = SP - 1 -- 再从栈中弹出一个值 local b = stack[SP] stack[SP] = 0 SP = SP - 1 -- 把两个值相加 local result = a + b -- 把相加结果压入栈中 SP = SP + 1 stack[SP] = result -- 为方便查看测试程序运行结果, 这里增加一条打印语句 print(stack[SP]) end end
代码整合完毕, 检查无误后用
Love2D
加载, 如下:Air:miniVM admin$ pwd /Users/admin/GitHub/miniVM Air:miniVM admin$ love ./miniVM 11 Air:miniVM admin$
我们会看到弹出一个窗口用于绘制图形, 同时命令行也会返回执行结果.
编写绘制函数
目前我们的虚拟机有一个用来模拟存储器保存测试程序指令的
program
表, 还有一个用来模拟栈的stack
表, 另外有两个指针, 一个是指示当前指令位置的指令指针IP
, 另一个是指示当前栈顶位置的栈顶指针SP
, 所以, 我们只需要绘制出这4
个元素在虚拟机运行时的状态变化即可.绘制 program 表和指令指针 IP
首先绘制作为存储器使用的
program
表, 我们准备遵循约定俗成的习惯, 用两个连在一起的矩形方框来表示它的基本存储单元, 左边的矩形表示地址, 右边的矩形表示在改地址存放的值, 这里我们会用到Love2D
中这三个基本绘图函数:- love.graphics.setColor(0, 100, 100)
- love.graphics.rectangle("fill", x, y, w, h)
- love.graphics.print("Welcome to our miniVM!", 400, 300)
我们一步步来, 先绘制右侧矩形和指令, 代码如下:
-- 绘制存储器中指令代码的变化 function drawMemory() local x,y = 500, 300 local w,h = 60, 20 for k,v in ipairs(program) do -- 绘制矩形 love.graphics.setColor(0, 255, 50) love.graphics.rectangle("fill", x, y-(k-1)*h, w, h) -- 绘制要执行的指令代码 love.graphics.setColor(200, 100, 100) love.graphics.print(v, x+15,y-(k-1)*h+5) end end function love.draw() -- love.graphics.print("Welcome to our miniVM!", 400, 300) -- 绘制存储器中指令代码的变化 drawMemory() end
显示效果如下:
接着我们把左侧的地址矩形和地址值, 还有指令指针也绘制出来, 代码如下:
-- 绘制存储器中指令代码的变化 function drawMemory() local x,y = 500, 300 local w,h = 60, 20 for k,v in ipairs(program) do -- 绘制存储器右侧矩形 love.graphics.setColor(0, 255, 50) love.graphics.rectangle("line", x, y+(k-1)*h, w, h) -- 绘制存储器中要执行的指令代码 love.graphics.setColor(200, 100, 100) love.graphics.print(v, x+15,y+(k-1)*h+5) -- 绘制存储器左侧矩形 love.graphics.setColor(0, 255, 50) love.graphics.rectangle("line", x-w/3-10,y+(k-1)*h,w/3+10, h) -- 绘制表示存储器地址的数字序号 love.graphics.setColor(200, 100, 100) love.graphics.print(k,x-w/2-10+10,y+(k-1)*h+5) -- 绘制指令指针 IP love.graphics.setColor(255, 10, 10) love.graphics.print("IP".."["..IP.."] ->",x-w-10+10-120,y+(IP-1)*h) end end
显示效果如下:
绘制 stack 表和栈顶指针 SP
接下来就是绘制用来模拟栈的
stack
表和栈顶指针SP
了, 跟上面类似, 代码如下:-- 绘制栈的变化 function drawStack() local x,y = 200, 300 local w,h = 60, 20 for k,v in ipairs(stack) do -- 显示栈右侧矩形 love.graphics.setColor(0, 255, 50) love.graphics.rectangle("line", x, y+(k-1)*h, w, h) -- 绘制被压入栈内的值 love.graphics.setColor(200, 100, 100) love.graphics.print(v, x+10,y+(k-1)*h) -- 绘制栈左侧矩形 love.graphics.setColor(0, 255, 50) love.graphics.rectangle("line", x-w-20,y+(k-1)*h,w+20, h) -- 绘制表示栈地址的数字序号 love.graphics.setColor(200, 100, 100) love.graphics.print(k,x-w-20+10,y+(k-1)*h) -- 绘制栈顶指针 SP love.graphics.setColor(255, 10, 10) love.graphics.print("SP".."["..SP.."] ->",x-w-10+10-100,y+(SP-1)*h) end end function love.draw() -- love.graphics.print("Welcome to our miniVM!", 400, 300) -- 绘制存储器中指令代码的变化 drawMemory() drawStack() end
显示效果如下:
很不错的结果, 终于能看到虚拟机这个黑盒子里面的内容了, 不过一下子就执行过去了, 还是有些遗憾, 那么就给它增加一项单步调试的功能好了!
说明: 因为
Love2D
的坐标轴方向是左手系,也就是说Y
轴的正向向下, 所以我们调整了一下program
和stack
的地址顺序, 小序号在上, 大序号在下.增加单步调试功能
其实很简单, 我们只需要在虚拟机的主体执行流程中增加一个判断逻辑, 每执行一条指令后都等待用户的输入, 这里我们设计简单一些, 就是每执行完一条指令, 虚拟机就自动暂停, 如果用户用键盘输入
s
键, 则继续执行下一条指令.需要用到这个键盘函数:
- love.keyreleased(key)
代码如下:
function love.load() ... step = false end function love.keyreleased(key) if key == "s" then step = true end end function love.update(dt) -- 虚拟机主体 if running then if step then step = false eval(fetch()) IP = IP + 1 end end end
运行中可以通过按下
s
键来单步执行每一条指令, 可以看看效果:到现在为止, 我们的可视化部分完成了, 而且也可以通过用户的键盘输入来单步执行指令, 可以说用
Lua
实现微型虚拟机的基本篇顺利完成. 接下来的扩展篇我们打算在这个简单虚拟机的基础上增加一些指令, 实现一个稍微复杂一些的虚拟机, 同时我们可能会修改一些数据结构, 比如我们的指令集的表示方式, 为后面更有挑战性的目标提供一些方便.完整项目代码
完整项目代码保存在群973961276里, 欢迎自由下载.
项目文件清单如下:
Air:miniVM admin$ tree . ├── README.md ├── miniVM │ └── main.lua ├── miniVM.lua └── pic ├── p01.png ├── p02.png ├── p03.png ├── p04.png ├── p05.png ├── p06.png ├── p07.png ├── p08.png └── p09.png 2 directories, 12 files Air:miniVM admin$
后续计划
因为这种方式很好玩, 所以我们打算后续在这个基础上实现一个
Intel 8086
的虚拟机, 包括完整的指令集, 最终目标是可以在我们的虚拟机上执行DOS
时代的x86
汇编程序代码.参考
-
JAVA虚拟机(JVM)和垃圾收集机制是什么
2016-06-27 19:41:11一、Java虚拟机(JVM)是什么 JAVA可以完成“一次编译,到处运行”,就是写好源代码*.java,编译成.class...源程序用文本编辑器就可以编写,这个无所谓,然后编译器会将其编译成字节码,即是class文件。对于用户而一、Java虚拟机(JVM)是什么
JAVA可以完成“一次编译,到处运行”,就是写好源代码*.java,编译成.class(字节码)文件以后,可以在MAC、Windows、Linux上运行,而不用再去重新编译,这也是其跨平台的体现之处。一个JAVA源代码的运行过程粗略可以表示如下图:
源程序用文本编辑器就可以编写,这个无所谓,然后编译器会将其编译成字节码,即是class文件。对于用户而言,可以拿着这个class文件在其他平台上去运行,但是我们知道,不同的操作系统所能识别的可执行文件是不一样的,比如Windows可执行文件.exe,Linux下又是另外的类型的可执行文件(Linux下可执行文件是没有扩展名的,是依照文件本身的内容来定的,在shell中执行路径就行,不展开讲了),而同一个.class文件竟然可以不作任何更改就直接在不同操作平台上直接运行(当然前提是安装了JDK或者JRE),这是如何实现的呢?
我们知道,对于机器(CPU)来说,不论你采用什么语言,编写了什么东西,要想运行,最终都得翻译成010101,而操作系统是负责把操作系统所认识的可执行文件翻译成010101交给机器去运行。而Java虚拟机(JVM)完成的功能就是将.class文件翻译(解释)成操作系统能认识的格式。也就是说,在.class文件和操作系统之间,还有一层,这一层就是JVM,比如图中的类装载器、字节码检验器、解释器等等都是在JVM里面,JVM负责把.class文件解释成操作系统认识的文件,然后再执行程序。见下图:
也就是说可以理解为,编写好的.class文件是运行在虚拟机JVM上面的,而不同操作系统上的JVM是有所不同的,这个Sun公司已经做好了,对于编写 Java程序不用去管,只需要编写好程序,编译成.class文件就行了,不用考虑这个.class是要运行在什么操作平台上的,这也就是跨平台的地方,就是在操作系统和程序之间多隔了一层JVM,JVM屏蔽了底层运行平台的差别。所以可以说JAVA虚拟机就是去完成跨平台的。
而Java语言也是一种解释型语言,而不是编译语言,虽然在源代码到.class之间由编译,但是在JVM当中,程序运行的时候,是JVM一行一行的将.class解释成操作系统认识的文件再运行的。
二、Java垃圾收集机制
编程中操作内存是很关键的,在C/C++语言当中,划出的内存,使用结束以后都需要释放掉,说直白一点,内存就是一亩地,划出一小块地方来种玉米,玉米收割以后,得把地还回去,让别人来种地。但是在编C++的时候,还地的时候很关键,就是内存什么时候用完了,不再需要了,这个时机不好判断,不能还没用完就释放,当然也不能释放两次,这个对编程功底和指针的使用要比较熟练。
在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。什么时候去收这个垃圾,不需要程
序员来做,有了Java垃圾回收机制,不再需要的对象占用的内存空进会自动被回收。Java语言消除了程序员回收无用内
存空间的责任;它提供一种系统级线程跟踪存储空间的分配情况。在JVM空闲时,检查并释放那些可被释放的空间。
当然,C++的空闲内存回收时机是一空闲下来基本上马上就会被回收(依赖程序员水平),但是Java垃圾回收机制还没
有那么智能,它并不是在一个对象成为无用对象以后立马就去回收它所占用的空间,而是隔一定时间,统一检查空闲内
存,并且回收释放。
总之Java垃圾回收是减轻程序员负担,提升编程效率的一种方式。
三、个别概念区分
SDK和JDK的区别:SDK是Software Development Kit,软件开发工具包,是一个广义的概念,任何编程工具几乎都可以看成SDK。而JDK是Java Development Kit,是Sun公司针对Java编程的产品,范围仅限于Java。也就是说,JDK是SDK的一种。
开发需要JDK,用户运行只需要JRE,JDK包含了JRE,JRE当中又包含了JVM。见下图:
可以看到JDK当中很大一部分都包含的是运行环境JRE。
-
Linux常用命令——迭代版(二)
2020-11-10 09:31:02答:vi是一个linux系统内建的文本编辑器。 vim具有程序编辑的能力,可以看作是vi的增强版本,可以主动的以字体的颜色辨别语法的正确性,方便程序设计,代码补全,编译及错误等方便编程的功能特别丰富,在程序员界被...这篇文章主要写一下两部分的内容:
1)vi/vim编辑器
2)文件备份与压缩相关命令一、vi/vim编辑器
1、什么是vi/vim编辑器?
答:vi是一个linux系统内建的文本编辑器。
vim具有程序编辑的能力,可以看作是vi的增强版本,可以主动的以字体的颜色辨别语法的正确性,方便程序设计,代码补全,编译及错误等方便编程的功能特别丰富,在程序员界被广泛使用。2、vi/vim编辑器有什么用?该怎么去用?
答:vi/vim编辑器可以使用户在终端界面输入对应的命令就能完成编辑文件,写程序,修改日志文件等等操作。
只需要在终端敲入 vim 命令就可以了。具体介绍及注意事项见后续介绍。3、vi/vim相关的命令
vi:文本编辑器命令基本语法:vi 相关目录下的文件(若有则打开,没有则创建)
vim:文本编辑命令
基本语法:vim 相关目录下的文件(若有则打开,没有则创建)
应用实例:
使用vim编辑器写一个Hello.java程序(延续到下面的问题)
4、vi 和 vim 的三种常见模式
① 正常模式:用 vim 命令打开的文件就处在正常模式下。在这个模式下用户可以使用快捷键来操作文件。
如图:
② 插入模式/编辑模式:i、a、o、r(它们的大写也行)中的任意一个键即可进入插入模式,在这个模式下可以输入内容。
如图:
③ 命令行模式:写完程序后按 Esc 键即可进入到命令行模式,在这个模式下,可以进行完成读取、保存、替换、退格、离开vim、显示行号等操作。
如图:
注意:“:”(冒号)是命令开始的标志,“w"是保存命令,”q"是退出命令。
5、vi/vim常用快捷键(正常模式下)
① 拷贝当前行:yy,拷贝当前行向下的5行,5yy,并粘贴(p)。
② 删除当前行:dd,删除当前行向下的3行,3dd。
③ 光标位置跳转:最末行——G,回到首行——gg。
6、vi/vim常用命令(命令行模式下)
① 在文件中查找某个单词:“/关键字” 回车,输入 n 就是查找下一个。
② 设置(取消)文件行号:“:set nu” 和 “:set nonu"。
7、vi/vim 快捷键键盘一览图
二、文件备份与压缩相关命令
1、 zip/unzip:压缩/解压命令(压缩成 *.zip格式)
基本语法:zip [选项] XXX.zip 需要压缩的内容 unzip [选项] XXX.zip zip常用选项: -r :递归压缩,即压缩目录 unzip常用选项: -d :解压目录
应用实例:
案例1:将 /home 下的所有文件压缩成mypackage.zip
案例2:将mypackage.zip 解压到 /opt/tmp 目录下
2、gzip/gunzip:压缩/解压指令基本语法: gzip 文件(压缩文件,将文件压缩为*.gz) gunzip 文件.gz (解压缩文件命令)
应用实例:
案例1:gzip 压缩,将 /home 下的 hello.txt 文件进行压缩
案例2:gunzip 解压缩,将 /home 下的 hello.txt.gz 文件进行解压缩
细节说明
当我们使用 gzip 对文件进行压缩后,不会保留原来的文件。
3、bzip2/bunzip2:文件压缩/解压缩命令
基本语法: bzip2 [选项] 要压缩的文件(压缩后文件格式为:*.bz2) bunzip2 [选项] *.bunzip2文件
应用实例:
使用 bzip2 命令压缩文件 a.c、b.c、c.c
4、tar:命令用来压缩(解压),备份文件基本语法: tar [选项] 要操作的文件或目录 常用选项: -f 需要操作的文件名称(必须要使用,放最后) -c 建立新的备份文件 -v 显示指令的执行过程 -x 从备份文件中还原文件 -z 通过gzip指令处理备份文件 -t 列出备份文件的内容 -C 需要(解)压缩到的目录,常用于解压缩
应用实例:
案例1:压缩文件 非打包
案例2:列出压缩文件的内容
案例3:解压文件
-
恢复WINDOWS快速启动栏中的 “显示桌面”
2008-10-30 17:18:00然后就自己用IPMESSAGE发送给虚拟机,IPMESSAGE可以看见这个文件的扩展名,“.scf”,传送完成后,突发奇想,想看看能否用文本编辑器打开这个文件,看看里面是什么, 里面内容如下: [Shell] Command=2 IconFile=... -
solaris10联网问题
2010-05-04 12:04:00为了比较一下linux与unix到底有什么不同,所以在虚拟机里安装了它。不过,不能联网真是一件很丢人的事。开始干活:首先激活虚拟网卡lo0#ifconfig lo0 plumb 我...在/etc目录下配置以下四个文件,没有就用“文本编辑器 -
NotePad++与MinGw的第一次HelloWorld——C语言环境配置说明
2015-06-18 00:56:00然后,我就想,我现在还没写什么太复杂的程序,就搞个最基础配置应该死不了吧……然后,我就调查了一下用文本编辑器进行编译运行的方法。本着开源第一的目的,我调查了一下GNU在windows环境下的使用,人家推荐的是... -
NotePad++与MinGw的第一次HelloWorld
2015-06-18 00:39:00然后,我就想,我现在还没写什么太复杂的程序,就搞个最基础配置应该死不了吧……然后,我就调查了一下用文本编辑器进行编译运行的方法。本着开源第一的目的,我调查了一下GNU在windows环境下的使用,人家推荐的是... -
疯狂JAVA讲义
2014-10-17 13:35:01学生提问:为什么要用this来调用另一个重载的构造器?我把另一个构造器里的代码复制、粘贴到这个构造器里不就可以了吗? 143 5.6 类的继承 144 5.6.1 继承的特点 144 5.6.2 重写父类的方法 145 5.6.3 父类实例的... -
Tcl_TK编程权威指南pdf
2011-03-25 09:30:55同学们在努力编制一个新式的内核程序,而John编写了一个新的编辑器和终端仿真程序。他使用Tcl作为这两种工具的命令语言,这样用户就可以定义菜单或者对那些程序进行定制。那时还处在使用X10的时代,他计划编写一个... -
循序渐进Linux基础知识、服务器搭建、系统管理、性能调优、集群应用
2014-01-17 14:06:124.7 文本编辑工具vi 122 4.8 小结与练习 125 第5章 Linux下软件包的安装与管理 127 5.1 源码安装方式 128 5.1.1 下载解压源码 128 5.1.2 分析安装平台环境 128 5.1.3 编译安装软件 129 5.1.4 源码安装Apache ... -
加密和解密 第二版 段钢 PDF
2010-01-17 21:02:1910.9.3 资源编辑工具 10.10 TLS初始化 10.11 调试目录 10.12 延迟装入数据 10.13 程序异常数据 10.14 .NET头部 10.15 PE分析工具编写 10.15.1 文件格式检查 10.15.2 FileHeader和OptionalHeader内容的读取 10.15.3 ... -
JAVA上百实例源码以及开源项目源代码
2016-09-17 21:58:33在有状态SessionBean中,用累加器,以对话状态存储起来,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证Bean正常被激活和钝化,EJB对象是用完毕,从内存中清除…… Java Socket 聊天... -
JAVA上百实例源码以及开源项目
2016-01-03 17:37:40在有状态SessionBean中,用累加器,以对话状态存储起来,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证Bean正常被激活和钝化,EJB对象是用完毕,从内存中清除…… Java Socket 聊天... -
python入门到高级全栈工程师培训 第3期 附课件代码
2018-06-07 09:38:4204 vim编辑器 05 系统启动流程 06 grub加密 07 bios加密 08 top命令 09 free命令 10 进程管理 第6章 01 上节课复习 02 磁盘分区 03 文件系统与挂载 04 挂载信息讲解 05 磁盘用满的两种情况 06 软连接和硬链接 07 ... -
java开源包1
2013-06-28 09:14:34Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
java开源包12
2013-06-28 10:14:45Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
Java资源包01
2016-08-31 09:16:25Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
java开源包101
2016-07-13 10:11:08Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
java开源包11
2013-06-28 10:10:38Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
java开源包6
2013-06-28 09:48:32Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
java开源包10
2013-06-28 10:06:40Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
java开源包8
2013-06-28 09:55:26Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
java开源包9
2013-06-28 09:58:55Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
java开源包7
2013-06-28 09:52:16Spring4GWT ...JARP是为petri 网提供的一个Java编辑器,基于ARP分析器。可以将网络图导出为 GIF, JPEG, PNG, PPM, ARP and PNML (XML based)文件格式。使用了优秀的JHotDraw 5.2 框架。 activemq... -
专业文本编辑工具,支持 Markdown $44.99 # ★★★ iExplorer 管理iOS设备 $34.99 # ★★★ Promotee Apps官网素材,设备更新还算即时 $4.99 # ★★★ Overflow 归类摆放 App Icon $14.95 # ★★★ ...