65.9K
CodeProject 正在变化。 阅读更多。
Home

如何创建您自己的虚拟机 - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (16投票s)

2010年2月28日

CPOL

6分钟阅读

viewsIcon

58569

downloadIcon

2176

本文将引导您逐步创建自己的虚拟机。

引言

不久前,我提交了关于如何创建自己的虚拟机的两部分教程中的第一部分。在第一部分中,我们为我们虚构的 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与我联系!

© . All rights reserved.