6

背景信息:最终,我想写一个真机的模拟器,比如原来的任天堂或Gameboy。然而,我决定我需要从一个简单得多的地方开始。我的计算机科学顾问/教授向我提供了一个非常简单的假想处理器的规格,他首先创建了该处理器以进行模拟。有一个寄存器(累加器)和 16 个操作码。每条指令由 16 位组成,其中前 4 位包含操作码,其余为操作数。指令以二进制格式的字符串形式给出,例如“0101 0101 0000 1111”。

我的问题:在 C++ 中,解析处理指令的最佳方法是什么?请记住我的最终目标。以下是我考虑过的几点:

  1. 我不能只是在阅读指令时处理和执行指令,因为代码是自我修改的:一条指令可以改变后面的指令。我能看到解决此问题的唯一方法是存储所有更改并为每条指令检查是否需要应用更改。这可能会导致与每条指令的执行进行大量比较,这是不好的。所以,我想我必须以另一种格式重新编译指令。

  2. 尽管我可以将操作码解析为字符串并对其进行处理,但在某些情况下,必须将整个指令视为一个数字。例如,递增操作码甚至可以修改指令的操作码部分。

  3. 如果我要将指令转换为整数,我不确定如何仅解析 int 的操作码或操作数部分。即使我将每条指令重新编译为三部分,整个指令为 int,操作码为 int,操作数为 int,这仍然无法解决问题,因为我可能必须递增整个指令然后解析受影响的操作码或操作数。此外,我是否必须编写一个函数来执行此转换,或者是否有一些 C++ 库具有将“二进制格式”的字符串转换为整数的函数(如 Java 中的 Integer.parseInt(str1, 2))?

  4. 另外,我希望能够执行移位等操作。我不确定如何实现,但这可能会影响我实现此重新编译的方式。

感谢您提供的任何帮助或建议!

4

4 回答 4

5

将原始代码解析为整数数组。该数组是您计算机的内存。

使用按位运算来提取各种字段。例如,这个:

unsigned int x = 0xfeed;
unsigned int opcode = (x >> 12) & 0xf;

将从0xf存储在unsigned int. 然后您可以使用 egswitch()来检查操作码并采取适当的措施:

enum { ADD = 0 };

unsigned int execute(int *memory, unsigned int pc)
{
  const unsigned int opcode = (memory[pc++] >> 12) & 0xf;

  switch(opcode)
  {
  case OP_ADD:
    /* Do whatever the ADD instruction's definition mandates. */
    return pc;
  default:
    fprintf(stderr, "** Non-implemented opcode %x found in location %x\n", opcode, pc - 1);
  }
  return pc;
}

修改内存只是写入整数数组的一种情况,如果需要,可能还使用一些按位数学。

于 2010-03-02T14:24:28.307 回答
1

我认为最好的方法是读取指令,将它们转换为无符号整数,并将它们存储到内存中,然后从内存中执行它们。

  1. 一旦你解析了指令并将它们存储到内存中,自我修改比存储每条指令的更改列表要容易得多。您可以只更改该位置的内存(假设您不需要知道旧指令是什么)。

  2. 由于您将指令转换为整数,因此这个问题没有实际意义。

  3. 要解析操作码和操作数部分,您需要使用位移位和掩码。例如,要获取操作码,您需要屏蔽高 4 位并向下移动 12 位 ( instruction >> 12)。您也可以使用掩码来获取操作数。

  4. 你的意思是你的机器有移位指令?这不应该影响您存储操作数的方式。当您开始执行其中一条指令时,您可以只使用 C++ 位移运算符<<>>.

于 2010-03-02T14:30:32.860 回答
0

以防万一,这是我用 C++ 编写的最后一个 CPU 模拟器。实际上,它是我用 C++ 编写的唯一模拟器。

该规范的语言有点特殊,但它是一个非常受人尊敬的简单 VM 描述,可能与您教授的 VM 非常相似:

http://www.boundvariable.org/um-spec.txt

这是我的(有点过度设计的)代码,它应该会给你一些想法。例如,它在 um.cpp 的 Giant Switch 语句中展示了如何实现数学运算符:

http://www.eschatonic.org/misc/um.zip

您也许可以找到其他实现与网络搜索进行比较,因为有很多人参加了比赛(我不是其中之一:我后来做了很多)。尽管我猜 C++ 中的数量不多。

如果我是你,我只会将指令存储为字符串,如果这是你的虚拟机规范定义对它们的操作的方式。然后在每次要执行它们时根据需要将它们转换为整数。它会很慢,但那又怎样?您的虚拟机并不是您将要用来运行时间要求严格的程序的真正虚拟机,而且狗慢速解释器仍然说明了您在此阶段需要了解的重要点。

尽管虚拟机实际上用整数定义了所有内容,但字符串可能只是用来描述程序加载到机器中时的情况。在这种情况下,请在开始时将程序转换为整数。如果 VM 将程序和数据存储在一起,并且对两者执行相同的操作,那么这就是要走的路。

在它们之间进行选择的方法是查看用于修改程序的操作码。新指令是作为整数还是作为字符串提供给它的?不管是哪一种,最简单的开始可能就是以这种格式存储程序。一旦它工作,你可以随时更改。

在上述 UM 的情况下,机器被定义为具有 32 位空间的“盘片”。显然,这些可以在 C++ 中表示为 32 位整数,这就是我的实现所做的。

于 2010-03-02T15:44:18.393 回答
0

我为自定义加密处理器创建了一个模拟器。我通过创建基类树来利用 C++ 的多态性:

struct Instruction  // Contains common methods & data to all instructions.
{
    virtual void execute(void) = 0;
    virtual size_t get_instruction_size(void) const = 0;
    virtual unsigned int get_opcode(void) const = 0;
    virtual const std::string& get_instruction_name(void) = 0;
};

class Math_Instruction
:  public Instruction
{
  // Operations common to all math instructions;
};

class Branch_Instruction
:  public Instruction
{
  // Operations common to all branch instructions;
};

class Add_Instruction
:  public Math_Instruction
{
};

我也有几个工厂。至少有两个有用:

  1. 工厂从文本创建指令。
  2. 工厂从操作码创建指令

指令类应该具有从输入源(例如std::istream)或文本(std::string)加载其数据的方法。还应支持输出 的推论方法(例如指令名称和操作码)。

我让应用程序从输入文件创建对象,并将它们放入Instruction. executor方法将运行数组中每条指令的“execute()”方法。此操作向下渗透到执行详细执行的指令叶对象。

还有其他可能需要仿真的全局对象。在我的例子中,一些包括数据总线、寄存器、ALU 和内存位置。

在编写代码之前,请花更多时间设计和思考项目。我发现这是一个很大的挑战,尤其是实现一个single-step功能强大的调试器和 GUI。

祝你好运!

于 2010-03-02T18:07:35.863 回答