如何创建您自己的虚拟机 - 第二部分
本文将引导您逐步创建自己的虚拟机。
引言
不久前,我提交了关于如何创建自己的虚拟机的两部分教程中的第一部分。在第一部分中,我们为我们虚构的 B32 机器开发了一个功能齐全的汇编器。该汇编器将“B32 汇编语言”文件转换为可在我们的 B32 虚拟机上运行的 B32 字节码。在本教程的第二部分中,我将向您展示如何创建实际运行由我们的汇编器生成的 B32 字节码程序的虚拟机。
在阅读本节之前,请务必阅读我的教程的第一部分,该部分可以在此处找到。此外,我提供了一个本教程的 PDF 版本供下载,其中包含更详细的内容。您可以随时下载。
B32 虚拟机
我们需要创建的第一个东西是“虚拟屏幕”。几乎所有运行在任何微处理器或 CPU 上的机器都有某种输出。为了遵循 KISS(保持简单愚蠢)原则,我们将为 B32 机器创建的虚拟显示器将是一个标准的文本彩色显示器,能够显示 80 列宽 x 25 行高。虽然它可以轻松扩展以实现简单的图形,例如线条和圆圈,但为了保持简单,我决定在本教程中省略图形。
回想一下,在第一部分中,我们的虚拟机将拥有 64KB 的内存访问权限,并且可以使用所有 64KB,但 $A000 到 $AFA0 之间的内存块除外。这 4KB 的内存区域将是我们的视频内存。对于那些曾在 DOS 时代编程过的人来说,您将毫不费力地理解我们的显示器是如何工作的。每当一个字节存储在该内存块的偶数部分时,它将代表要显示的字符。例如,如果我在 $A000 中存储值 65,那么它将在屏幕的左上角显示字母“A”。该内存块的奇数部分将决定前景色和背景色。例如,如果我在 $A001 中存储值 7,那么屏幕左上角的“A”将显示为黑色背景上的灰色文本(就像命令窗口一样)。有关前景色和背景色如何工作的更多信息,请参阅我的 PDF 或此网站。
我们的 B32 屏幕将是一个 Windows Forms 用户控件。屏幕本身存储在一个名为 `m_ScreenMemory` 的变量中,它是一个 4000 字节的数组。它有一个用于获取或设置我们的虚拟屏幕起始地址(默认为 0xA000)的属性。此属性会设置一个名为 `m_ScreenMemoryLocation` 的变量。有三个公共方法,称为 `Poke`、`Peek` 和 `Reset`。 `Poke()` 将一个值存储在虚拟屏幕数组中。 `Peek()` 将检索存储在给定地址的值。最后,`Reset()` 将重置屏幕,基本上是清除它。这三个方法的代码如下:
public void Reset()
{
for (int i = 0; i < 4000; i += 2)
{
m_ScreenMemory[i] = 32;
m_ScreenMemory[i + 1] = 7;
}
Refresh();
}
public void Poke(ushort Address, byte Value)
{
ushort MemLoc;
try
{
MemLoc = (ushort)(Address - m_ScreenMemoryLocation);
}
catch (Exception)
{
return;
}
if (MemLoc < 0 || MemLoc > 3999)
return;
m_ScreenMemory[MemLoc] = Value;
Refresh();
}
public byte Peek(ushort Address)
{
ushort MemLoc;
try
{
MemLoc = (ushort)(Address - m_ScreenMemoryLocation);
}
catch (Exception)
{
return (byte)0;
}
if (MemLoc < 0 || MemLoc > 3999)
return (byte)0;
return m_ScreenMemory[MemLoc];
}
这段代码非常直接。除了这些方法之外,我还重写了 `Paint` 方法。 `Paint` 方法是所有工作的执行者。基本上,它根据我的属性字节确定要使用的前景色和背景色画笔,然后使用 `DrawString()` 将一个字符一个字符地输出到内部位图。最后,这个位图会立即传输到屏幕,以避免闪烁。
我们的主程序包含三个部分。顶部是菜单栏,用于加载 B32 程序、暂停和重新启动程序以及退出。底部将用作状态栏。它将显示所有五个寄存器的值、比较标志和我们的指令指针。最后,中间的部分是我们的 B32 虚拟屏幕。
虚拟机组装的详细信息可以在 PDF 中找到。但我会简要总结一下它的工作原理。基本上,在运行时创建一个 64KB 的数组。该数组代表我们虚拟机的全部内存。每当打开 B32 字节码程序时,它会将该程序存储在数组中,地址由 origin 指定(参见教程的第一部分)。然后创建一个线程,该线程在适当的执行地址执行我们的程序。
该线程与主 B32 程序并发执行。该线程只是一个解释字节码并执行相应功能的循环。每当数据存储在我们的 64KB 数组中时,它也会传递给我们为 B32 虚拟屏幕创建的 `Poke()` 函数。这是通过一个名为 `ThreadPoke()` 的函数完成的,该函数以线程安全的方式更新我们的 B32 屏幕。如果存储的数据超出了我们的视频内存范围,那么我们的 B32 虚拟屏幕控件将基本忽略 poke。此循环将继续,直到达到程序末尾。
此外,我还创建了挂起和恢复线程的功能。这是通过使用 .NET 的 `ManualResetEvent()` 来实现的,该事件会实际挂起线程。此外,还可以控制执行速度。默认情况下,程序会实时运行,没有延迟。但是,您可以减慢执行速度。这是通过在每次操作码执行之间添加 `Thread.Sleep()` 命令来实现的。
B32 与真实虚拟机有什么区别?
这里提供的虚拟机与实际虚拟机之间至少存在两个主要区别。第一个区别:时序至关重要。如果您要创建一种旨在根据真实机器执行代码的虚拟机,那么时序是一件大事。每个 CPU 指令都需要一定的执行时间。通常,这是一个非常短的时间;但是,必须准确测量;否则,在您的虚拟机上执行的程序将运行得太快或太慢。我使用 `Thread.Sleep()` 来模拟 B32 中的延迟;然而,在真实的虚拟机中,您将需要使用更好的、更高分辨率的计时器,例如 DirectX 提供的计时器。
第二个区别是,为了模拟现代机器,程序员使用一种称为动态重编译的方法。在 B32 中,我使用服务器 IF 语句来解释 B32 程序中的操作码。大多数虚拟机不会使用此方法。相反,它们使用动态重编译,即获取虚拟字节码并将其转换为与主机计算机兼容的机器代码。例如,如果 B32 使用动态编译,那么在打开文件后,B32 将程序转换为 Intel x86 机器代码。然后 B32 原生地执行该机器代码。显然,这是一个高级主题,远远超出了本教程的范围。但是,如果我将来创建第三部分,我几乎肯定会更深入地探讨这个主题。
结束
希望大家喜欢本教程。如果您需要任何帮助或有任何反馈,请随时通过icemanind@yahoo.com与我联系!