模拟器是如何工作的,它们是如何编写的?

966

模拟器是如何工作的?当我看到NES / SNES或C64模拟器时,我感到惊讶。

http://www.tommowalker.co.uk/snemzelda.png

您是否需要通过解释其特定的汇编指令来模拟这些机器的处理器?还有哪些因素需要考虑?它们通常是如何设计的?

针对有兴趣编写模拟器(尤其是游戏系统)的人,您能给出一些建议吗?


15
你需要找到的最重要的东西是该系统的“程序员手册”,因为它详细说明了硬件供应商与程序员之间的“契约”,并隐藏了不相关且可能会改变的细节。你的成功机会取决于该系统的流行程度。 - Uri
155
好的游戏选择。 - Cristián Romo
2
是的,我相信是这样的:http://zh.wikipedia.org/wiki/%E9%98%BF%E5%82%B3%E4%BC%A0%E8%AF%B4%EF%BC%9A%E6%9E%97%E5%85%8B%E7%9A%84%E5%A5%87%E8%BF%B9%EF%BC%88%E6%B8%B8%E6%88%8F%EF%BC%89 - mmcdole
16
任何人想知道模拟和仿真的区别,请查看以下链接:https://dev59.com/8nI-5IYBdhLWcg3w-9sK。 - Lazer
8
自从我第一次玩那个游戏以来,我一直想知道为什么海拉鲁充满了“8球”大石头 :-) - Vivian River
16个回答

1121
模拟是一个多方面的领域。以下是基本想法和功能组件。我会将其分解成若干部分,并通过编辑填充细节。我所描述的许多内容需要了解处理器内部运作原理,需要汇编语言知识。如果我在某些事情上有点含糊,请提问,以便我能够继续改进这个答案。
基本想法: 模拟通过处理处理器和各个组件的行为来工作。您构建系统的每个单独的部分,然后像硬件中的电线一样连接这些部分。
处理器模拟: 处理器模拟有三种处理方式: - 解释执行 - 动态重新编译 - 静态重新编译
使用所有这些方法,您都有相同的总体目标:执行一段代码以修改处理器状态并与“硬件”交互。处理器状态是给定处理器目标的处理器寄存器、中断处理程序等的集合。对于6502处理器,您将拥有表示寄存器的若干个8位整数:A、X、Y、P和S;还将拥有一个16位PC寄存器。
使用解释执行,您从IP(指令指针,也称为PC)开始,并从内存中读取指令。您的代码解析此指令并使用此信息来按照处理器指定的方式更改处理器状态。解释执行的核心问题是它非常缓慢;每次处理给定指令时,您都必须对其进行解码并执行所需操作。
使用动态重新编译,您遍历代码,就像解释执行一样,但不仅执行操作码,还构建操作列表。一旦到达分支指令,您将此操作列表编译为主机平台的机器代码,然后缓存此已编译的代码并执行它。然后,当您再次遇到给定指令组时,您只需执行来自缓存的代码。 (顺便说一句,大多数人实际上不会制作指令列表,而是在运行时将其编译为机器代码 - 这使得优化更加困难,但这超出了本答案的范围,除非有足够多的人感兴趣)
使用静态重新编译,您与动态重新编译中的操作相同,但要跟踪分支。最终,您将构建代表程序中所有代码的代码块,该代码块可以无需进一步干预即可执行。如果没有以下问题,这将是一种很好的机制:
  • 没有被编写在程序中的代码(例如压缩、加密、运行时生成/修改等)将不会被重新编译,因此也无法运行。
  • 已经证明,在给定二进制文件中找到所有代码等效于停机问题

这些组合使得99%的情况下静态重新编译完全不可行。有关更多信息,请参见Michael Steil对静态重新编译的研究-这是我看过的最好的。

处理器仿真的另一面是您与硬件交互的方式。 这确实有两个方面:

  • 处理器时间
  • 中断处理

处理器时间:

某些平台-尤其是旧的游戏机(如NES、SNES等)-要求您的仿真器具有严格的时间才能完全兼容。 对于NES,您拥有PPU(像素处理单元),它要求CPU在精确定时刻将像素放入其内存中。 如果使用解释,您可以轻松计算周期并模拟适当的时间; 对于动态/静态重新编译,事情变得复杂得多。

中断处理:

中断是CPU与硬件通信的主要机制。 通常,您的硬件组件会告诉CPU它关心哪些中断。 这很简单-当您的代码引发特定中断时,您查看中断处理程序表并调用适当的回调。

硬件仿真:

模拟给定硬件设备有两个方面:

  • 模拟设备的功能
  • 模拟实际设备接口

以硬盘为例。 创建支持存储,读取/写入/格式化例程等来模拟功能。 这部分通常非常简单。

设备的实际接口稍微复杂一些。 这通常是内存映射寄存器的组合(例如,设备监视以进行信令的某些内存部分)和中断。 对于硬盘,您可以拥有一个内存映射区域,在其中放置读取命令,写入命令等,然后读取此数据。

我可以详细说明,但是您可以采用无数种方法。 如果您在这方面有任何具体问题,请随时询问,我将添加信息。

资源:

我认为我已经提供了一个相当不错的介绍,但还有许多其他领域。 我很乐意回答任何问题; 由于极其复杂,我在大多数内容中都非常模糊。

必要的维基百科链接:

一般的模拟资源:

  • Zophar -- 这是我开始研究模拟器的地方,我首先下载了模拟器,最后在那里寻找到了庞大的文档资料。这是你可能拥有的最佳资源。
  • NGEmu -- 没有太多的直接资源,但他们的论坛无与伦比。
  • RomHacking.net -- 文档部分包含有关流行游戏机的机器体系结构资源。

可参考的模拟器项目:

  • IronBabel -- 这是一个基于 .NET 的模拟器平台,用 Nemerle 编写,在运行时会将代码重新编译为 C#。免责声明:这是我的项目,所以请原谅我无耻的宣传。
  • BSnes -- 一个很棒的 SNES 模拟器,目标是完美的周期准确性。
  • MAME -- the 街机模拟器。非常好的参考资料。
  • 6502asm.com -- 这是一个带有很酷的小型论坛的 JavaScript 6502 模拟器。
  • dynarec'd 6502asm -- 这是我在一两天内完成的一个小技巧。我采用了 6502asm.com 上现有的模拟器,并将其更改为动态重编译代码以实现大幅提高速度的目的。

处理器重编译参考:

  • Michael Steil(上面引用过)进行的静态重编译研究最终发表在这篇论文中,你可以在这里找到源代码等。

补充:

这个答案提交已经过去一年多了,由于它引起了很多关注,我觉得是时候更新一些内容了。

目前模拟技术中最令人兴奋的事情可能是libcpu,由前述的Michael Steil发起。这是一个旨在支持大量CPU核心的库,使用LLVM进行重新编译(静态和动态!)。它有巨大的潜力,我认为它将对模拟产生巨大的影响。

我也注意到了emu-docs,这里有很多系统文档资源仓库,对于模拟非常有用。虽然我没花太多时间在那里,但看起来他们有很多优秀的资源。

我很高兴这篇帖子能有所帮助,希望我可以振作起来,年底或明年初完成我的这个主题的书籍。


37
这个回答已经开始变得史诗般了。如果你可以在最后向我指出任何相关资源,那将不胜感激。我正在考虑模拟SNES或NES系统,并把它作为我的学期项目。 - mmcdole
8
好的,我会为您准备一个不错的资源列表。如果您有任何具体要求,我会尽力满足。 - Serafina Brocious
3
@thenonhacker,我资源部分提到的IronBabel项目是我的。(这是个厚颜无耻的广告 ;)) - Serafina Brocious
1
请提供参考文献,证明了在给定的二进制代码中找到所有代码与停机问题是等价的。或者应该是“已经证明在任何给定的二进制代码中找到所有代码与停机问题是等价的”?而且无法访问Steil的论文 :-( - squelart
4
你提到正在写一本书,能否向我们介绍一下最新进展?我会很感兴趣阅读它。 - alex
显示剩余9条评论

126

43

模拟可能看起来令人生畏,但实际上比模拟要容易得多。

任何处理器通常都有一个写得很好的规范,描述了状态、交互等等。

如果你完全不关心性能,那么你可以使用非常优雅的面向对象程序轻松地模拟大多数旧处理器。例如,X86处理器需要维护寄存器状态(容易)、维护内存状态(容易),以及将每个传入的命令应用于当前机器状态的一些东西。如果你真的想更精确,你还可以模拟内存转换、缓存等等,但这是可行的。

事实上,许多微芯片和CPU制造商会针对芯片的仿真器和芯片本身进行测试,这有助于找出芯片规格或硬件实现中的问题。例如,编写一个芯片规格可能会导致死锁,在硬件中发生截止时间时,重要的是要看是否可以在规格中重现它,因为这表示比芯片实现中的某些问题更大的问题。

当然,视频游戏的仿真器通常关注性能,所以他们不使用幼稚的实现,并且他们还包括与主机系统的操作系统交互的代码,例如使用绘图和声音。

考虑到旧视频游戏(NES/SNES等)的非常缓慢的性能,现代系统上的仿真相当容易。事实上,即使是下载每个SNES游戏或任何Atari 2600游戏的一组,也是很神奇的,考虑到这些系统流行时,免费获得每个游戏卡带将是一个梦想成真。


1
仿真和模拟有什么区别? - Wei Hu
2
@Wei:一般来说,模拟器应该在“外部”表现得像它所模拟的系统,但这并不意味着它必须以类似的方式实现。模拟器是以模拟的系统为基础实现的,因此行为类似于被模拟的系统。 - Uri
当你看到“模拟器”时,认为它类似于真实情况,而“仿真器”则是在模拟真实情况。 - mP.
@WeiHu,请查看https://dev59.com/8nI-5IYBdhLWcg3w-9sK。 - Pacerier

29

我知道这个问题有点老,但我想为这个讨论添加一些内容。这里大多数答案都集中在模拟器解释它们模拟的系统的机器指令方面。

然而,有一个非常著名的例外叫作 "UltraHLE" (WIKIpedia文章)。UltraHLE 是有史以来最著名的模拟器之一,在很多人认为不可能的情况下,在家用电脑上模拟商业任天堂64游戏时表现良好。事实上,当 UltraHLE 被创建时,任天堂仍在为 Nintendo 64 制作新游戏!

第一次,我看到了关于模拟器的文章出现在印刷杂志上,而以前我只在网上看到过它们被讨论。

UltraHLE 的概念是通过模拟 C 库调用而不是机器级别的调用来实现不可能的任务。


22

有值得一看的内容是Imran Nazar在JavaScript中尝试编写Gameboy模拟器。


1
我们如何获取Gameboy游戏的原始操作码指令? - Pacerier
有许多设备在“灰色市场”上出售。你在发达国家的任何主要商店都找不到它们。这些设备能够将游戏卡带中的指令复制到通常称为“ROM”的文件中。谷歌搜索“Gameboy Roms”,但要注意不要点击不安全的链接和攻击网站! - Vivian River

18

我自己创建了80年代BBC微型计算机的仿真器(在Google中搜索VBeeb),这里有几个需要知道的事情。

  • 你并不是完全模拟真实设备,那将是复制品。相反,你正在模拟状态。一个好例子是计算器,真实设备有按钮、屏幕、外壳等等。但是为了模拟一个计算器,你只需要模拟按钮是否按下,LCD 的哪些段亮起等等。基本上,一组数字代表了计算器中可能发生变化的所有组合。
  • 你只需要让仿真器的界面看起来和行为像真实设备即可,这个仿真就越接近真实系统。在幕后发生的事情可以任何你喜欢的事情。但是,为了轻松编写仿真器,需要在真实系统(如芯片、显示器、键盘、电路板)与抽象计算机代码之间进行心理映射。
  • 为了模拟计算机系统,最好将其拆分为较小的块,并逐个模拟这些块。然后将所有块串联起来形成一个成品。就像一组带有输入和输出的黑匣子,非常适合面向对象编程。你还可以进一步细分这些块以使生活更轻松。

实际上,你通常会考虑模拟的速度和精度。这是因为目标系统上的软件可能比源系统上的原始硬件运行得更慢。这可能会限制编程语言、编译器和目标系统的选择等问题。
此外,你必须确定你准备模拟什么,例如并非需要模拟微处理器中晶体管的电压状态,但很可能需要模拟微处理器的寄存器集合的状态。
一般来说,模拟的细节越小,对原系统的仿真就越精确。
最后,旧系统的信息可能不完整或根本不存在。因此,获取原始设备至关重要,或者至少解开其他人编写的良好仿真器!


17

是的,您必须手动解释整个二进制机器代码混乱的内容。而且大多数时候,您还需要模拟一些在目标机器上没有相应硬件的奇特硬件。

简单的方法是逐个解释指令。这种方法效果很好,但速度很慢。一种更快的方法是重新编译 - 将源机器代码转换为目标机器代码。这更加复杂,因为大多数指令不会一一映射。相反,您将不得不制定涉及额外代码的精心解决方案。但最终它的速度要快得多。大多数现代仿真器都是这样做的。


1
最糟糕的事情是缺少文档。当你发现GameBoy Color中修改的Z80核心具有未记录的标志操作,并且正在测试的游戏使用它时,你真正开始失去信心。 - Callum Rogers
1
个人讨厌的事情:它是机器代码(单数),而不是机器代码(复数);就像是Morse Code而不是Morse Codes - Lawrence Dol
1
@Vilx:实际上不是这样的——术语“机器码”指的是CPU指令集,它从软件诞生时就开始使用,并且不是复数形式。它指的是“指令集”,是一个单数形式,而不是复数形式“指令”。与程序代码、莫尔斯电码等类似。复数形式的使用是由于误用而渐渐流行起来的,通常是非英语母语的人在说话时如此。 - Lawrence Dol
1
@软件猴子 - 但是我不能使用“代码”这个词来指代集合中的单个项目吗?例如:“... --- ... - 这三个莫尔斯码代表了三个字母S、O、S。”因为 ... 是代表字母“S”的一种编码。不行吗? - Vilx-
1
不,代码是不可数名词,它没有类似水或沙子的复数形式。 - Ivan
显示剩余4条评论

15

当您开发一个模拟器时,您需要解释系统正在运行的处理器汇编语言(Z80、8080、PS CPU等)。

您还需要模拟系统拥有的所有外围设备(视频输出,控制器)。

您应该首先为像经典的Game Boy(使用Z80处理器,我没有搞错吧)或C64这样的简单系统编写模拟器。


9
C64 是一个“简单”的系统吗?虽然 6510 相对来说比较简单(一旦你掌握了未列出的操作码),但声音芯片(SID)和视频芯片(VIC)却远非简单。为了达到任何像样的兼容性水平,你需要模拟它们 - 包括硬件漏洞在内。 - moobaa

10

模拟器的创建非常困难,因为需要模拟许多黑科技(即不寻常的效果)、时序问题等。

关于这一点,请参见http://queue.acm.org/detail.cfm?id=1755886

这也将向您展示为什么模拟1MHz CPU需要多GHz的CPU。


9

同时,您还可以查看Darek Mihocka的Emulators.com网站,了解有关JIT指令级优化的建议以及构建高效仿真器的其他好东西。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接