关于实现简单CPU仿真器的问题

时间:2010-03-02 14:21:20

标签: c++ binary emulation machine-code

背景信息:最终,我想写一个真机的模拟器,如原版Nintendo或Gameboy。但是,我决定我需要在更远,更简单的地方开始。我的计算机科学顾问/教授为我提供了一个非常简单的假想处理器的规范,他先创建了这个处理器。有一个寄存器(累加器)和16个操作码。每条指令由16位组成,前4位包含操作码,其余为操作数。指令以二进制格式给出,例如“0101 0101 0000 1111”。

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

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

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

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

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

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

4 个答案:

答案 0 :(得分:5)

将原始代码解析为整数数组。这个数组是你计算机的内存。

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

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

将从存储在0xf中的16位值中提取最顶端的四位(此处为unsigned int)。然后你可以使用例如switch()检查操作码并采取适当的措施:

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;
}

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

答案 1 :(得分:1)

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

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

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

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

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

答案 2 :(得分:0)

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

规范的语言略显特殊,但它是一个完全可敬,简单的虚拟机描述,可能与你教授的虚拟机很相似:

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

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

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

你可以找到其他与网络搜索进行比较的实现,因为很多人参加了比赛(我不是其中之一:我之后做了很多)。虽然在C ++中我猜的并不多。

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

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

在它们之间进行选择的方法是查看用于修改程序的操作码。新指令是作为整数还是作为字符串提供给它的?无论哪个,最简单的事情可能是以该格式存储程序。一旦工作,您可以随时更改。

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

答案 3 :(得分: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. 工厂创建指令 操作码
  3. 指令类应该有从输入源(例如std::istream)或文本(std::string)加载数据的方法。还应支持推论输出方法(例如指令名和操作码)。

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

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

    在编写代码之前,请花更多时间设计和思考项目。我发现这是一个非常大的挑战,特别是实现了一个支持single-step的调试器和GUI。

    祝你好运!

相关问题