模拟器如何工作?当我看到 NES / SNES 或 C64 仿真器时,我感到非常惊讶。
您是否必须通过解释特定的组装说明来模拟这些机器的处理器?还有什么呢?它们通常是如何设计的?
您可以为有兴趣编写模拟器(尤其是游戏系统)的人提供任何建议吗?
仿真是一个多方面的领域。这里是基本思想和功能组件。我将把它分成几部分,然后通过编辑填写细节。我将要描述的许多内容都需要了解处理器的内部工作原理 - 组装知识是必需的。如果我在某些方面不太清楚,请提出问题,以便我可以继续改进此答案。
通过处理处理器和各个组件的行为来进行仿真。您构建系统的每个单独部分,然后像连接硬件中的电线一样连接各个部分。
有三种处理处理器仿真的方法:
使用所有这些路径,您将具有相同的总体目标:执行一段代码来修改处理器状态并与 “硬件” 进行交互。处理器状态是给定处理器目标的处理器寄存器,中断处理程序等的集合。对于 6502,您将有许多 8 位整数表示寄存器: A
, X
, Y
, P
和S
;您还将拥有一个 16 位PC
寄存器。
通过解释,您可以从IP
(指令指针,也称为PC
,程序计数器)开始,然后从内存中读取指令。您的代码将解析此指令,并使用此信息来更改处理器指定的处理器状态。解释的核心问题是它的速度很慢。每次处理给定指令时,都必须对其进行解码并执行必要的操作。
通过动态重新编译,您可以像解释一样迭代代码,但是不仅可以执行操作码,还可以构建操作列表。到达分支指令后,您可以将此操作列表编译为主机平台的机器代码,然后缓存此编译后的代码并执行它。然后,当您再次命中给定的指令组时,只需执行高速缓存中的代码即可。 (顺便说一句,大多数人实际上并没有列出指令,而是将它们即时编译为机器代码,这使优化变得更加困难,但这超出了此答案的范围,除非有足够的人对此感兴趣)
使用静态重新编译,您可以执行与动态重新编译相同的操作,但是要遵循分支。您最终构建了代表程序中所有代码的代码块,然后可以在没有更多干扰的情况下执行代码。如果不是以下问题,这将是一个很好的机制:
这些结合在一起使静态重新编译在 99%的情况下完全不可行。有关更多信息,Michael Steil 对静态重新编译进行了一些出色的研究,这是我见过的最好的。
处理器仿真的另一面是与硬件交互的方式。这实际上有两个方面:
某些平台 - 尤其是 NES,SNES 等较旧的控制台 - 要求您的模拟器具有严格的时间安排以完全兼容。使用 NES,您将拥有 PPU(像素处理单元),它要求 CPU 在精确的时刻将像素放入其内存中。如果使用解释,则可以轻松地计算周期并模拟正确的时间;通过动态 / 静态重新编译,事情变得很复杂。
中断是 CPU 与硬件通信的主要机制。通常,您的硬件组件会告诉 CPU 它关心的是什么中断。这非常简单 - 当您的代码引发给定的中断时,您查看中断处理程序表并调用适当的回调。
模拟给定的硬件设备有两个方面:
以硬盘驱动器为例。通过创建后备存储,读取 / 写入 / 格式化例程等来模拟该功能。这部分通常非常简单。
设备的实际接口要复杂一些。这通常是内存映射寄存器(例如,设备监视用于执行信令更改的内存部分)和中断的某种组合。对于硬盘驱动器,可能会有一个内存映射区域,您可以在其中放置读取命令,写入等,然后将这些数据读回。
我会更详细地介绍,但是您可以使用一百万种方法。如果您在此处有任何特定问题,请随时提出,我将添加信息。
我想,我已经给了一个很好的介绍在这里,但有一吨的其他领域。我很乐意为您解答任何问题;由于巨大的复杂性,我在大多数情况下一直很模糊。
自从提交此答案以来已经过去了一年多,并且得到了所有人的关注,我认为现在是时候更新一些东西了。
也许目前最令人兴奋的事情是libcpu ,它是由前面提到的 Michael Steil 启动的。它是一个旨在支持大量 CPU 内核的库,这些内核使用 LLVM 进行重新编译(静态和动态!)。它具有巨大的潜力,我认为它将为仿真做出巨大贡献。
我还引起了emu-docs 的注意,它包含一个很大的系统文档库,该库对于仿真非常有用。我没有花很多时间在这里,但是看起来他们有很多很棒的资源。
我很高兴这篇文章对您有所帮助,并且希望我能在年底或明年年初完成有关该主题的书。
一个名叫 Victor Moya del Barrio 的人就这个话题写了论文。 152 页上的很多很好的信息。您可以在此处下载 PDF。
如果您不想在scribd上注册, 则可以在 Google 上搜索 PDF 标题“仿真编程技术研究” 。 PDF 有两个不同的来源。
仿真看似令人生畏,但实际上比仿真要容易得多。
任何处理器通常都具有写得很好的规范,用于描述状态,交互等。
如果您根本不关心性能,则可以使用非常优雅的面向对象程序轻松地模拟大多数较旧的处理器。例如,一个 X86 处理器需要某种东西来维护寄存器的状态(简单),某种东西来维护内存的状态(简单),并且需要接受每个传入命令并将其应用于计算机的当前状态。如果您确实想要准确性,那么您还可以模拟内存转换,缓存等,但这是可行的。
实际上,许多微芯片和 CPU 制造商会先在芯片的仿真器上测试程序,然后在芯片本身上测试程序,这有助于他们找出芯片规格或芯片在硬件中的实际实现方式是否存在问题。例如,可以编写可能导致死锁的芯片规格,并且当硬件中出现最后期限时,重要的是要查看是否可以在规格中复制该规格,因为这表明比芯片实现中的问题更大。
当然,用于视频游戏的模拟器通常关心性能,因此它们不使用幼稚的实现,并且还包括与主机系统的 OS 接口的代码,例如使用绘图和声音。
考虑到旧视频游戏(NES / SNES 等)的性能非常慢,在现代系统上进行仿真非常容易。实际上,考虑到当这些系统流行时,免费访问每个卡带将是梦想成真的,您可以下载一套既有的 SNES 游戏,也可以下载任何一款 Atari 2600 游戏,这真是令人惊讶。
我知道这个问题有点老了,但是我想在讨论中添加一些内容。这里的大多数答案都围绕着仿真器,这些仿真器解释了它们所仿真系统的机器指令。
但是,有一个非常著名的例外,称为 “UltraHLE”( WIKIpedia 文章 )。 UltraHLE 是有史以来最著名的模拟器之一,它在人们普遍认为 Nintendo 64 游戏(在家用计算机上具有不错的性能)的情况下,对它进行了仿真。实际上,在创建 UltraHLE 时,任天堂仍在为 Nintendo 64 制作新游戏!
第一次,我在印刷杂志上看到有关模拟器的文章,以前,我只是在网上看到它们的讨论。
UltraHLE 的概念是通过模拟 C 库调用而不是机器级调用来使不可能成为可能。
值得一看的是 Imran Nazar 尝试用 JavaScript 编写Gameboy模拟器。
创建了我自己的 80 年代 BBC 微型计算机的仿真器(在 Google 中输入 VBeeb),有很多事情要知道。
实际上,您通常是在为仿真的速度和保真度而写。这是因为目标系统上的软件将(可能)比源系统上的原始硬件运行得更慢。这可能会限制编程语言,编译器,目标系统等的选择。
除此之外,您还必须限制要准备模拟的内容,例如,不必模拟微处理器中晶体管的电压状态,而是可能必须模拟微处理器寄存器组的状态。
一般来说,仿真的细节级别越小,您对原始系统的保真度就越高。
最后,旧系统的信息可能不完整或不存在。因此,掌握原始设备非常重要,或者至少要与别人写的另一个好的模拟器分开!
是的,您必须 “手动” 解释整个二进制机器代码混乱。不仅如此,大多数时候您还必须模拟一些目标计算机上没有等效硬件的奇特硬件。
一种简单的方法是一对一解释指令。效果很好,但是很慢。一种更快的方法是重新编译 - 将源机器代码转换为目标机器代码。这更加复杂,因为大多数指令不会一对一映射。取而代之的是,您将不得不进行复杂的解决方案,其中涉及其他代码。但是最终它要快得多。大多数现代仿真器都这样做。
开发仿真器时,您正在解释系统正在使用的处理器组件(Z80、8080,PS CPU 等)。
您还需要模拟系统具有的所有外围设备(视频输出,控制器)。
您应该开始为 simpe 系统编写模拟器,例如老式的Game Boy (使用 Z80 处理器,我不会误会)或 C64。
创建仿真器非常困难,因为您需要模拟许多 hack(如异常效果),时序问题等。
有关此示例,请参见http://queue.acm.org/detail.cfm?id=1755886 。
这也将向您展示为什么您需要 “GHz” 仿真 1MHz 的 CPU。
另请访问 Darek Mihocka 的Emulators.com ,以获取有关 JIT 指令级优化的出色建议,以及有关构建高效仿真器的许多其他优点。