「玩一玩」自制虚拟机,第一部分:设计和汇编器

Conmajia 2012-09-14 02:34:38
加精
这是一个长篇的开始,或者说一个大坑。
© Conmajia 2012, icemanind 2012
本文根据icemanind的《How to Create Your Own Virtual Machine》系列文章编译,并进行了大量改造(已征得作者同意)。
原文发表于codeproject.com网站,链接地址如下:
第一部分 · 第二部分
源代码:点击下载
英文教程:点击下载

序言
By Conmajia
各位,你正在阅读的这个系列的文章将从零开始,带你一步一步设计并实现一个类似于VMWare的——当然只有粗陋的简易CPU和界面——完整可运行的虚拟机(Virtual Machine)。我们将要使用C#语言,基于Microsoft .NET Framework 2.0运行库来完成整个虚拟机的制作(出于兼容性考虑,也是为了将主要精力集中在设计上)。因此,你需要具备最基本的.NET程序开发知识。也就是说,至少你应该会使用Visual Studio 2005(或者更高版本),并且能成功运行自己的「Hello World」程序。
在开始设计前,让我们先来了解一下虚拟机的相关知识。
虚拟机是一种模拟硬件环境的中间件(Middleware),是一种高度隔离的软件容器,它可以运行自己的操作系统和应用程序,就好像它是一台物理计算机一样。虚拟机的行为完全类似于一台物理计算机,它包含自己的虚拟(即基于软件实现的)CPU,有些甚至扩展了RAM、硬盘和网络接口卡(NIC)等虚拟硬件。
操作系统无法分辨虚拟机与物理机之间的差异,应用程序和网络中的其他计算机也无法分辨。即使是虚拟机本身也认为自己是一台「真正的」计算机。不过,虚拟机完全由虚拟机软件组成,不含任何硬件组件。因此,虚拟机具备物理硬件所没有的很多独特优势。

虚拟机的优势

一般而言,虚拟机具备以下四个关键特征:
•兼容性:虚拟机与所有标准的 x86 计算机都兼容
•隔离:虚拟机相互隔离,就像在物理上是分开的一样
•封装:虚拟机将整个计算环境封装起来
•独立于硬件:虚拟机独立于底层硬件运行

好了,下面就开始设计我们自己的虚拟机。

设计虚拟机
我们要为这个虚拟机绘制一个蓝图。我们给虚拟机起名为:B32,代号SunnyApril(SA for short)。为了简化设计,SA被设计成一个16位的机器(这意味着她的CPU位宽是16-bit的)。这样一来,SA能够支持的地址空间就是0000H-FFFFH。现在我们为SA加入5个寄存器(Register)。寄存器是计算机硬件的一个重要概念和组件。寄存器是具有有限存贮容量(通常是1、2字节)的高速存储部件,用来暂存指令、数据或者地址。几乎所有的CPU和虚拟机中都包含有内建的寄存器。简单来说,寄存器就是「CPU内部的内存」。
为了简单,我们只设计了5个寄存器,分别是A、B、D、X和Y。A、B寄存器是8位寄存器,可以保存0-FFH的无符号数或是80H-7FH的有符号数。X、Y和D寄存器都是16位的,可以保存0-FFFFH的无符号数或是8000H-7FFFH的有符号数。同样是为了设计简便,目前我们只考虑无符号数的情况,有符号数将在后面研究浮点数的时候一起进行。
D寄存器是一个特殊的16位寄存器。它的值是由A、B寄存器的值合并而成,A保存了D的高8位值,B保存了低8位值。例如A寄存器值为3CH,B寄存器值为10H,则D寄存器值为3C10H。反之,如果修改D寄存器值为07C0H,则A寄存器值变为07H,B寄存器值变为C0H。
下面的图形象地说明了各寄存器的规格和之间的关系。

为了让我们的虚拟机能在第一时间「反馈」运行结果,我们从64KB的内存空间中留出4000字节的空间(A000H-AFA0H)作「显示器」缓存。我们模仿DOS下的汇编语言,用其中2000字节用于保存显示字符(这样可以得到80x25的字符屏幕),2000字节用于保存每个字符的样式。每个样式字节低3位分别表示前景色的红、绿、蓝颜色值,第4位表示明暗度,5-7位同样,用于表示背景颜色。样式字节的最高位本来是表示是否闪烁字符,但在我们的设计中不需要这个功能,所以直接忽略。
接下来的工作就是设计能让虚拟机运行起来的指令集(即字节码)了。指令集和我们自制的「汇编语言」一起设计,简便起见,先设计4个指令,如图所示。

以LDA指令(字节码01H)为例,该指令将操作数(#41H)存入A寄存器,即「Load A」。由于操作数寻址方式太多,这里简单地用「#」符号起头,表示「立即数」(模仿51单片机的汇编语言)。以「H」结尾的数字表示为16进制,类似的有「O」(八进制)、「B」(二进制)和「D」(十进制,可以省略)。
END指令(字节码04H)表示程序结束。同时它后面的「标签」表示程序的起始标签,用于标注程序运行的开始位置。标签是使用「:」半角冒号结尾的单独成行的字母开头的字符串,如START标签就这样书写:
START: 

接下来是设计编译后的字节码文件格式。大部分的二进制文件格式都是以一串「魔法数字」字符串开头的。例如,DOS/Windows文件用「MZ」开头,Java二进制文件用4字节的数字3405691582开始,用16进制表示就是「CAFEBABE」(咖啡宝贝)。我们的SunnyApril就使用「CONMAJIA」作为魔数。魔数之后是文件体偏移量,表示文件体(即程序字节码)在文件中的起始位置。接着是程序长度,即文件体长度。执行地址表示字节码执行起始地址,固定为0。(后续可能会改变)偏移段用于保存额外的数据或者中断向量表等,其长度为「偏移量-13」字节。文件头后就是文件体,保存了程序编译后的全部字节码。文件结构参见下图。


汇编器
现在我们可以开始动手设计汇编器了。这个汇编器将能够把我们写好的汇编源程序编译后写入到可以供虚拟机运行的二进制字节码文件中。汇编文件格式如下:
01.[标签:]  
02.<指令><空白><操作数>[空白]<换行>

其中,方括号[]中的内容是可选的。
注:以下内容和源代码经过较大幅度的改造和优化,和原文差异较大,注意区别。
这就是我们的汇编源程序:
01.START:  
02.LDA #65
03.LDX #A000H
04.STA X
05.END START

这个程序的功能就是简单地把字符'A'输出到屏幕的左上角。第一行代码定义了START标签。第二行将立即数65(即ASCII代码'A')存入A寄存器。第三行将立即数A000H(即显示缓存的起始地址,参见《设计》一节)存入X寄存器。第四行代码将A寄存器中的值(65)存入X寄存器中的数值(A000H)代表的内存地址。最后用END结束程序。
下面我们运行Visual Studio,新建一个「Windows窗口应用程序」项目,选择.NET Framework版本为2.0,仿照下面的截图设计窗体。

其中,textBox1.Readonly属性设置为true,numericUpDown1.Hexadecimal属性设置为true。
首先在窗体类中建立如下的变量。
Dictionary<string, UInt16> labelDict;  
UInt16 binaryLength;
UInt16 executionAddress;

定义一个寄存器枚举。

enum Registers
{
Unknown = 0,
A = 4,
B = 2,
D = 1,
X = 16,
Y = 8
}

在窗体的构造函数中初始化变量和控件。
public Form1()
{
InitializeComponent();
labelDict = new Dictionary<string, ushort>();
binaryLength = 0;
executionAddress = 0;
numericUpDown1.Value = 0x200;
}

button1的功能是打开「文件浏览」对话框选择需要汇编的源文件。双击button1,在生成的Click事件中输入以下代码:
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "SunnyApril Assembly Files(*.asm)|*.asm";
ofd.DefaultExt = "asm";
ofd.FileName = string.Empty;
if (ofd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
textBox1.Text = ofd.FileName;
else
textBox1.Clear();

button2功能是执行汇编,并生成二进制字节码文件,主要代码如下:
if (textBox1.Text == string.Empty)
return;

labelDict.Clear();
binaryLength = (UInt16)numericUpDown1.Value;

FileInfo fi = new FileInfo(textBox1.Text);

BinaryWriter output;
FileStream fs = new FileStream(
Path.Combine(
fi.DirectoryName,
fi.Name + ".sab"),
FileMode.Create
);
output = new BinaryWriter(fs);

// magic word
output.Write('C');
output.Write('O');
output.Write('N');
output.Write('M');
output.Write('A');
output.Write('J');
output.Write('I');
output.Write('A');

// org
output.Write((UInt16)numericUpDown1.Value);

// scan to ORG and start writing byte-code
output.Seek((int)numericUpDown1.Value, SeekOrigin.Begin);

// parse source code line-by-line
TextReader input = File.OpenText(textBox1.Text);
string line;
while ((line = input.ReadLine()) != null)
{
parse(line.ToUpper(), output);
dealedSize += line.Length;
}
input.Close();

// binary length & execution address (7 magic-word, 2 org before)
output.Seek(10, SeekOrigin.Begin);
output.Write(binaryLength);
output.Write(executionAddress);
output.Close();
fs.Close();

MessageBox.Show("Done!");

在这个方法中,通过一个while逐行解析源代码(原作者是全文解析),解析方法如下:
private void parse(string line, BinaryWriter output)
{
// eat white spaces and comments
line = cleanLine(line);
if (line.EndsWith(":"))
// label
labelDict.Add(line.TrimEnd(new char[] { ':' }), binaryLength);
else
{
// code
Match m = Regex.Match(line, @"(\w+)\s(.+)");
string opcode = m.Groups[1].Value;
string operand = m.Groups[2].Value;

switch (opcode)
{
case "LDA":
output.Write((byte)0x01);
output.Write(getByteValue(operand));
binaryLength += 2;
break;
case "LDX":
output.Write((byte)0x02);
output.Write(getWordValue(operand));
binaryLength += 3;
break;
case "STA":
output.Write((byte)0x03);
// NOTE: No error handling.
Registers r = (Registers)Enum.Parse(typeof(Registers), operand);
output.Write((byte)r);
binaryLength += 2;
break;
case "END":
output.Write((byte)0x04);
if (labelDict.ContainsKey(operand))
{
output.Write(labelDict[operand]);
binaryLength += 2;
}
binaryLength += 1;
break;
default:
break;
}
}
}

其中用到了读取字节(byte)操作数的内部方法,如下所示。稍作改进可以很方便地支持多种数制。读取字(Word)操作数的方法与此类似,不再另作说明。
private byte getByteValue(string operand)
{
byte ret = 0;
if (operand.StartsWith("#"))
{
operand = operand.Remove(0, 1);
char last = operand[operand.Length - 1];
if (char.IsLetter(last))
switch (last)
{
case 'H':
// hex
ret = Convert.ToByte(operand.Remove(operand.Length - 1, 1), 16);
break;
case 'O':
// oct
ret = Convert.ToByte(operand.Remove(operand.Length - 1, 1), 8);
break;
case 'B':
// bin
ret = Convert.ToByte(operand.Remove(operand.Length - 1, 1), 2);
break;
case 'D':
// dec
ret = Convert.ToByte(operand.Remove(operand.Length - 1, 1), 10);
break;
}
else
ret = byte.Parse(operand);
}

return ret;
}

运行汇编器,对前面保存的demo1.asm文件进行汇编,得到demo1.sab二进制字节码文件,该文件内容如下:

可以见到,汇编器忠实地完成了我们交代的任务,正确计算了文件大小,在0200H位置处开始,汇编出的字节码为「01 00 02 00 00 03 10 04 00 02」,下面我们对照源程序进行检验。
第一行为START标签,将地址0200H存入缓存(在文件中没有体现)。
第二行LDA指令,存入字节码01H,然后存入单字节操作数(A寄存器是8位寄存器)65,即41H。
第三行LDX指令,存入字节码02H,然后存入双字节操作数(X寄存器是16位寄存器)A000H,由于计算机采用小端模式(低位在前),所以在文件中是以「00 A0」的形式存储的。
第四行STA指令,存入字节码03H,然后存入Registers.X枚举值(16,即01H)。
第五行END指令,存入字节码04H,然后存入START标签地址0200H(2字节,仍以小端模式存储)。
根据以上分析,我们制作的汇编器完全符合设计。
下一步,我们将开始设计虚拟机,敬请期待。
欢迎各种建议意见。
(第一部分 完)
© Conmajia 2012, icemanind 2012
...全文
9506 107 打赏 收藏 转发到动态 举报
写回复
用AI写文章
107 条回复
切换为时间正序
请发表友善的回复…
发表回复
jixu0218 2012-10-02
  • 打赏
  • 举报
回复
挺有意思的!看了CodeProject上icemanind的PDF文档。icemanind实现了一个b32 assembler和b32 virtual machine,从最初的4条汇编指令增加到后来的几十条指令,当然虚拟机也相应地增加了对新增指令的支持。而且为了方便UI更新,还使用了多线程,一个用来跑虚拟机模拟执行,一个用于UI更新显示。

请问楼主后续会做什么样的修改和创新呢,非常期待呀!!!
牧风 2012-09-29
  • 打赏
  • 举报
回复
[Quote=引用 46 楼 的回复:]

虚拟机运行虚拟机操作系统和模拟器模拟运行的关系有点像操作系统调用程序和解释器解释脚本的关系。

只要稍微想象下也不难理解,模拟器根本不可能像虚拟机那样达到物理机80%的性能运行程序一样。

“新版”虚拟机要求硬件支持很大程度上是噱头,一个很可笑的事实是,在Win7 XP Mode推出一年后,因为厂家的抱怨,Microsoft做出妥协,允许让非硬件虚拟化支持的CPU也可以运行——事实上性能……
[/Quote]

虽然你看起来挺厉害,但是我想说一句:达到80%的性能是可行的,我们这已经在应用Xen+qemu的模式了,性能损耗20%左右。所有人都是用只有一个小盒子的(基于虚拟化的)云计算机。
“新版”虚拟机可不是噱头,远远强于之前vmware的那一套,性能有极大的超越,很快会成为未来的主流。
这一切都在进行中,KVM已经并入了linux主线,未来qemu的特性也将移植到KVM上,进入内核主线。
“虚拟机直接替换掉操作系统内核”这个概念倒是Xen的那一套,但是搞起来很费力。每个系统都要做适配。现在Xen已经快要过时了。基于KVM,修改硬件驱动就可以完美虚拟化。
oop_hpt 2012-09-29
  • 打赏
  • 举报
回复
速度第二期啊
yueguihuashu 2012-09-28
  • 打赏
  • 举报
回复
看起来很牛X的样子
GodDices 2012-09-27
  • 打赏
  • 举报
回复
哥决定先从图灵机开始玩
fly4free 2012-09-27
  • 打赏
  • 举报
回复
更加低级的就是 Simpletron .<- 这仅仅是一个课后习题……
GodDices 2012-09-27
  • 打赏
  • 举报
回复
擦,我的马克怎么没了。。。
yzx714 2012-09-25
  • 打赏
  • 举报
回复
楼主制作的不是“虚拟机”(VirtualBox这类),而是模拟器(Bochs这类)
algebraic 2012-09-24
  • 打赏
  • 举报
回复
可以用在其他操作系统下吗?
skyeg 2012-09-24
  • 打赏
  • 举报
回复
不知道在毫无实际效果的情况下能够做到哪一步
argenCHN 2012-09-24
  • 打赏
  • 举报
回复
lz厉害,支持
zhujiawei7 2012-09-24
  • 打赏
  • 举报
回复
这好像有点犀利,看不懂恶。。。
yojinlin 2012-09-24
  • 打赏
  • 举报
回复
感謝分享。
chen_198965 2012-09-24
  • 打赏
  • 举报
回复
俺也学习一下
qlz37238 2012-09-24
  • 打赏
  • 举报
回复
好强啊。。。LZ这个是一个完整的教程吗??
cde32 2012-09-23
  • 打赏
  • 举报
回复
牛人就是多啊!
yourmem 2012-09-23
  • 打赏
  • 举报
回复
原文:Have you ever wondered how the Microsoft .NET Framework or how a Java virtual machine works.
JAVA 虚拟机或者.net的虚拟机,这个和VMWARE的虚拟机不太一样。可以比对KVM和java VM的代码,完全不同的,虽然有些概念上是类似的。
rmy6688 2012-09-23
  • 打赏
  • 举报
回复
楼主太厉害了
xuexi005 2012-09-23
  • 打赏
  • 举报
回复
玩一玩,计算机太复杂了
buyaodaohao8448 2012-09-22
  • 打赏
  • 举报
回复
不错 很好 感谢呀!!
加载更多回复(87)

110,566

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧