球速体育新闻

News Center

当前位置: 首页 > 球速体育新闻 > 行业新闻

Welcome-球速体育【转】如何编写模拟器

更新时间:2026-05-14点击次数:

  我 在写这篇文章之前收到很多人的邮件,他们希望编写一个模拟器却不知从何下手。文章中提到的任何观点和建议都来自我个人,切勿将其当成绝对真理。我的文章主 要讨论“解释型”模拟器,而不是“编译型”模拟器,这是因为我对重编译技术没有太多经验。我在文章中列出一两个链接,读者可以查找这些技术的相关信息。

  如果你觉得这篇文章还有不足或是有所指正,请你通过邮件发给我。我不回复那些来争吵的,问无聊问题的,还有来要ROM映像的邮件。我在这篇文章的资源列表中丢失了几个重要的FTP和址,如果你知道任何有价值的网址也可以告诉我。另外FAQ中如果有问题也可以给我发邮件。

  你决定编写一个软件模拟器了吗?很好,这篇文章也许可以给你一些帮助,它包含编写模拟器时常见的技术问题,提供了模拟器内部的“设计蓝图”,一定程度上可供你借鉴。

  基本上只要是微处理器内部的任何部件都可以模拟。当然只有那些运行或多或少程序的设备才有模拟的需要,包括:

  有必要知道你可以模拟任何一种计算机系统,不论它有多复杂(例如Commordore的Amiga计算机),只不过这种模拟的性能可能非常低。

  模拟试图去模拟一个设备的内部设计,而“simulation”则模拟设备的功能。例如,一个程序能模拟Pacman(即“吃豆先生”,译者注)街机的硬件,并能运行Pacman的ROM,就可以称之为模拟器。而为你机器所编写的Pacman游戏,只是使用了和真实街机相同的图像,就称之为simulator。

  尽管这个问题处于一种“灰色”地带,似乎只要模拟器包含的信息没有非法手段得到,对专有硬件的模拟应该是合法的。但要注意的是,如果发布受版权保护的系统ROM(如BIOS等)就是非法行为。

  模拟器将所模拟的代码按字节从存储器中读入,译码并执行对所模拟的寄存器、存储器和I/O的操作。这种模拟器的大致算法如下:

  这种模拟器的优点在于:便于调试、移植和同步(可以方便地计算出时钟周期,并将模拟操作绑定到时钟周期上)。

  而明显的缺点就是性能低,解释过程占用了大量的CPU时间,要获得比较良好的速度就需要在相对较快的机器上运行。

  这种技术是将所模拟的程序翻译为本机的汇编代码,生成一个可执行文件,可以在本机运行而无需任何工具。虽然静态重编译看上去不错,但并不总是可以实现的。例如,无法对自修改代码进行静态重编译, 因为除非去运行这些代码,否则无法知道它们最终变成什么形态。为了避免这种情况,可以将静态重编译器与解释器、动态重编译器结合使用。

  要编写一个模拟器,必须掌握计算机编程和数字电路的一般知识,另外有汇编编程经验会更加方便。

  5 编写一个内置调试器非常有用,可以中止模拟并观察程序执行的状态。另外需要一个所模拟系统的反汇编器,如果没有可以自己写一个。

  掌握使用的编程语言对编写一个模拟器是相当重要的,工程越复杂,就要使代码更加优化,从而获得更快的速度。计算机模拟器绝不是用来学习编程语言的工程例子。

  这是一个讨论计算机模拟的新闻组,许多模拟器作者都会阅读,虽然里面也有一定程度的水分。在发言之前可以先阅读里面的FAQ。

  和comp.emulators.misc相似,只不过主要讨论电子游戏机模拟器。同样在发言之前阅读FAQ。

  comp.sys.*的下级栏目包含各种不同的计算机,从中可以获得大量有用的技术信息。可以按如下格式输入:

  首先,假如你要模拟标准的Z80或6502CPU,你可以使用我编写的一个CPU模拟器,尽管它只适用于某些特定环境。

  对于那些想编写自己的CPU模拟器内核或是对模拟器工作原理感兴趣的人,我提供了一个C编写的典型CPU模拟器框架。在实际应用中,你可能需要增减部分内容。

  首先,为CPU周期计数器(Counter)和程序计数器(PC)赋一个初值:

  周期计数器用来存放离下一次可能中断发生所剩的CPU时钟周期,注意的是当它越界时,中断不一定必然发生。周期计数器有多种用途,比如用作同步时钟,或是更新屏幕的扫描线。另外,PC存放模拟器下一次读取的操作码在存储器的地址。

  CPUIsRunning是布尔变量,这样有一个好处,就是可以通过设置CPUIsRunning=0随时退出循环。但是,每一轮循环时对这个变量的检查也会消耗大量CPU时间,所以还是尽可能避免采用这种方法。当然,不要这样来实现循环:

  因为这样的话,某些编译器会生成判断“1”是“真”还是“假”的代码,你当然不愿意编译器在每一轮循环中做这些无用功。

  虽然这是读取所模拟存储器最简单的方法,但并不总是可行的,文章之后会再增加更多的通用方法。

  数组Cycles包含执行每条指令所需的CPU周期数。要注意某些指令(如条件转移或子程序调用)根据操作数的不同,所需的周期数也不同,可以在以后的代码中进行调整。

  Switch结构通常被误认为缺乏效率,因为会被编译一系列的if()…else if()…语句。这种情况在少量的case分支时确实存在,但是大量的分支结构(100-200甚至更多)会被编译生成跳转表,这样是非常高效的。

  译码有另外两种方法:一种是生成一个函数表,并调用对应的函数,由于这种方法采用间接调用函数,效率比使用Switch要低;另一种方法是建立一个标号表,使用goto语句来实现,这种方法比用Switch要快一些,但它只适用于支持“precomputed labels”的编译器,其它编译器则不允许创建地址标号表。

  成功译码并执行之后,要检查是否有中断发生,同时也执行一些需要系统时钟同步的任务。

  要注意这里并不是简单地采用Counter=InterruptPeriod来赋值,而是采用Counter+=InterruptPeriod:这样做可以使周期计数更加精确,因为周期计数器有可能会出现负值的情况。

  由于每轮循环都检查是否退出开销太大,所以只在周期计数器越界时检查:这样当ExitRequired=1时模拟始终会退出,而且不会消耗太多CPU时间。

  要访问所模拟的存储器,最简单的办法就是将其视为一个字节数组,访问的方法很简单:

  地址空间被分成若干个可切换的页面(或者块),当地址空间比较小(64KB),这种存储方法用来扩展存储空间。

  同一块存储空间可以由若干个不同地址来访问。如在地址$4000写入的数据可能会同时出现在地址$6000和$8000中。ROM也可以利用不完全译码映射到镜像存储空间中。

  某些存储在卡带上的软件(如MSX游戏)会试图向自身的ROM写入数据,如果写入成功机器就无法工作,这就经常需要进行复写保护。为了在模拟器上模拟这些软件,需要禁止向ROM中写入数据。

  系统中会有一些I/O设备映射到存储空间,对这些存储空间的访问会产生“特殊效应”,所以需要进行跟踪。

  模拟过程需要频繁地调用ReadMemory()和WriteMemory(),所以在模拟框架中经常大量使用这两个函数,这就要求必须采用尽可能高效的方法去实现它们。下面是这两个函数实现页面地址空间访问的例子:

  注意inline(内联)关键字,它会让编译器将函数嵌入到代码中,而不是去调用它。如果你的编译器不支持inline或_inline,那就试着将函数改为静态函数:一些编译器(如WatcomC)会采用内联的方式对较短的静态函数进行优化。

  正如之前所讲的,很多计算机都有镜像RAM,在某处写入的值会出现在其它位置。虽然这种情况可以在ReadMemory()中处理,但并不可取,因为ReadMemory()被调用的次数比WriteMemory()要多得多,在WriteMemory()中实现存储镜像要更有效率。

  为了模拟这些任务,要将其绑定适当的CPU周期数。例如,假设CPU以2.5MHz运行,显示采用50Hz的刷新频率(PAL视频标准),则每隔

  现在我们假设整个屏幕(包括垂直空白)有256条扫描线条会显示在屏幕上(另外44条成为垂直空白),这样每隔

  对每个任务所需的CPU周期数进行仔细的计算,然后取它们的最大公约数作为中断周期,并绑定到所有任务中(周期计数器越界时并不一定执行这些任务)。

  首先,在编译器中选择正确的优化选项可以让代码提高更多额外的性能。根据我的经验,下列几种选项的组合可以获得最快的执行速度:

  在优化时选择“循环展开”选项有时很管用,这个选项会试着将比较短的循环展开为顺序代码。但根据我的经验,这个选项并不会提高多少性能,选择该选项反而会在某些十分特殊的情况下破坏你的代码。

  优化C代码本身要比选择编译器选项稍微复杂,而且通常依赖于编译代码所用的CPU。下面几条准则一般适用于所有CPU,但是不要把它们当成绝对的真理,因为使用环境可能不同。

  在一款优秀的profiling工具下运行你的程序(我立刻想到GPROF),会出现很多让你意想不到的有意思的东西。你可能会发现一些看似无关紧要的代码会比其它代码更加频繁地被调用,从而导致程序整体运行速度变慢。要提高程序性能,可以对这部分代码进行优化或者直接用汇编重写。

  避免使用任何结构体,这样你不得不用C++编译器而不是C编译器来编译你的程序:C++编译器会生成更多不必要的代码。

  尽量使用CPU所支持的基准长度的整数类型,即用int来代替short或者long。这样编译器在生成最终代码时可以减少那部分用来转换不同长度数据类型的代码,也可以减少存储器访问的时间,因为某些CPU在读写那些长度可以与地址边界对齐的数据时速度最快。

  在每个程序块中尽可能地少用变量,将最常用的变量声明为寄存器(虽然大多数新编译器会自动将变量放到寄存器中)。相对于那些只有少量专用寄存器的CPU(如Intel 80x86),这样做对于那些有着大量通用寄存器的CPU(如PowerPC)是更有意义。

  如果你正好有一个只执行几次的小循环,那就手动将它展开为一段线性代码,这始终是一个好办法。参见上面有关自动循环展开的注意点。

  当需要乘以(或除以)2^n时始终用移位来代替(如J/128==J7),对于大多数CPU来说执行的速度更快。同样地,可以使用位运算AND来代替模运算(如J%128==J&0x7F)。

  通常根据数据在存储器中的如何存储,可以将CPU分为若干类。除非是某些极特殊的情况,大多数CPU都可以归为以下两种:

  编写模拟器时,要同时注意所模拟的CPU和宿主机CPU的大小端情况。比如说,想模拟一个小端CPU Z80,它将一个16位字的低字节存储在低地址。如果宿主机使用小端CPU(如Intel 80x86),那么一切都没有问题。如果宿主机是大端CPU(如PowerPC),那么将一个16位的Z80数据存储到存储器中就会出现问题。更糟糕的是,如果程序要同时在这两种架构上运行,你就需要对大小端情况进行识别。

  你会发现,一个字可以直接用W来访问。如果模拟过程需要访问其中单独一个字节,可以使用B.l和B.h以保证数据的正确顺序。

  如果程序要在不同的平台下编译,不管执行多么重要的程序,在这之前先要测试一下是否以正确的大小端方式进行编译。下面是一种测试方法:

  大多数计算机系统由若干个大芯片组成,每个芯片实现一部分系统功能。这样就有了CPU、视频控制器和声音发生器等。这些芯片大都有自己的存储器和连接的其它硬件。

  一个典型的模拟器要重现原有系统的设计,就要在单独的模块中实现各个子系统的功能。首先,可以方便调试,因为可以将bug定位到各个模块当中。其次,采用模块化架构可以让你在其它模拟器中重用某些模块。计算机硬件是高度标准化的,你可以在许多不同的计算机模型中找到相同的CPU和视频芯片。模拟出这个芯片一次,显然比在每个使用同种芯片的计算机中重复实现它要容易很多。

  • 电子邮箱: facai@126.com

  • 热线电话: 0755-89800918

  • 公司地址: 深圳市南山区粤海街道高新区社区深圳湾创新科技中心2栋A座22层

Copyright © 2012-202X 球速体育公司 版权所有 Powered by EyouCms
备案号:粤ICP备05004158号-1

SiteMap

网站二维码
关注

联系

0755-89800918

顶部