下载本文的PDF版本 PDF

没有源代码?没问题!
PETER PHILLIPS 和 GEORGE PHILLIPS,DIGITAL ECLIPSE

如果你必须移植一个程序,但你只有二进制文件怎么办?

典型的软件开发涉及两个过程之一:创建新的软件以满足特定需求,或修改(维护)旧软件以修复问题或满足新需求。这些转换发生在源代码级别。但是,如果问题不是维护旧软件,而是需要创建原始软件的功能副本呢?如果源代码不再可用怎么办?

当试图在现代设备上重现旧街机游戏的原始玩法时,就会出现这个确切的问题。游戏玩法是如此出名,以至于任何低于原始水平的东西都是不可接受的。通常源代码是可用的,但它可能是不完整的,并且可能不包括添加到后期生产模型的所有补丁。此外,提供原始硬件的副本也太昂贵了。

提供视频游戏(和旧家用电脑)的忠实模拟是我们在此过程中的主要经验。但是,相同的技术可以应用于其他领域。老化的硬件和软件可以被具有完全兼容程序的新硬件所取代。

弥合差距

一般问题可以表达为“弥合语义差距”。你必须创建一个程序,将原始程序的含义精确地映射到宿主系统上。这主要意味着某种类型的目标处理器指令集解释器,但还必须处理 I/O(输入/输出)设备。这种解释器被称为模拟器。(请参阅本文第 54 页的“TRS-80:一个简单的模拟器”。)如果程序被自动转换为不同的语言,则称为翻译。

为什么我们要在如此低的级别上解决这个问题?主要是为了尽可能实现对原始的高度保真度。模拟是关于映射语义。硬件的语义通常通过电路图或芯片规格文档得到很好的记录。软件内部的层通常按照更宽松的标准设计,因此规格文档(如果存在)不太可能完全描述其行为。事实上,软件本身是可用的最权威的描述。诚然,芯片规格并非总是完整或准确,但芯片会被重复使用,随着时间的推移,偏差会变得广为人知。

目标系统和宿主系统之间的语义差距不仅仅是一个抽象概念。它可以量化如下:G = 模拟一条目标指令所需的宿主指令数。给定 G 以及对宿主系统和目标系统相对速度的一些了解,你可以快速决定模拟是否可行。这里的问题是 G 的值是实际模拟器的函数。经验法则是 G 至少为 10,但实际上,10 是非常相似的系统的下限。虽然时间通常是最重要的考虑因素,但在存储空间方面也存在语义差距的类似概念。

图 1

模拟和翻译都从相同的输入(ROM 数据和硬件文档)开始,并产生相同的结果:原始程序在 PC 上运行的副本。区别在于 ROM 数据的处理方式。对于模拟器,它只是程序的参数。对于翻译后的版本,ROM 被转换并编译到可执行文件中。

语义空间差距是宿主程序大小与目标程序大小的比率。对于模拟器,宿主程序大小分为两部分:目标程序的宿主表示和模拟器代码及相关表。除非宿主和目标截然不同,否则对存储表示进行重大更改几乎没有任何好处。因此,如果忽略模拟器代码,则语义空间差距通常正好为一。

模拟需要付出代价

直观地说,G 的值实际上取决于目标机器和宿主机器的差异程度。(请参阅本文第 54 页的“TRS-80:一个简单的模拟器”。)大多数现代机器在基本架构上非常接近,都使用 2 的补码算术和 8 位倍数的寄存器大小。但是,模拟器需要几个额外的步骤来解释一条目标指令

  1. 解码指令并分派到相应的代码。
  2. 执行内存加载和存储——并处理由此产生的 I/O 操作。
  3. 执行核心操作(例如,将两个数字相加)。
  4. 更新任何标志或其他副作用。通常这包括更新虚拟时间。

这些步骤加起来可能会有很多指令。解码步骤通常是时间和空间之间的权衡:更大的分派表占用更多内存,但可以实现更快的分派。加载和存储会带来问题,因为目标硬件可以根据寻址线信号选择访问哪些内存。在普通程序中,你无法区分内存访问,并且需要代码将地址映射到适当的数组中。

模拟器创建了真实机器的虚拟版本,可以使用你选择的任何编程语言编写。但是,如果你试图最小化语义差距,你将需要选择一种语言,使你尽可能接近底层的宿主硬件。大多数模拟器是用 C 语言编写的,因为 C 语言的语义与现代硬件相似。少量汇编语言可以用来进一步缩小差距。

例如,大多数核心操作在 C 语言中都很容易编写代码。但并非完全如此:大多数微处理器都有某种旋转指令,在 C 语言中,只能表示为两次移位和一个或运算。因此,差距立即增加了三倍。(除非你的 C 编译器异常聪明。)在这种情况下,一小段内联汇编语言可以产生很大的不同。

当使用 C 语言时,如果目标系统在算术运算结果上设置标志(例如,进位或溢出),差距将会扩大。如果你的模拟器是用汇编语言编写的,你也许可以使用宿主系统的标志寄存器。如果不能,你将需要添加更多 C 代码。

而这仅仅是针对处理器而言。我们还没有考虑处理系统中其他设备的工作。例如,视频游戏硬件可能支持可移动图形,称为精灵,而你的宿主系统不支持。

将二进制代码翻译成源代码

如果差异特别大,或者宿主和目标之间的速度差异不够大,那么纯模拟器将不可行——至少不是实时运行的模拟器。最终产品可能需要进行一些修改,例如图形缩放,以匹配宿主的显示。将目标程序翻译成源代码将比模拟版本更快,并且更易于修改。由于翻译是自动化的,与移植不同,翻译后的版本将与模拟版本一样忠实于原始版本。

虽然翻译目标代码比编写模拟器更困难,但在处理预先已知的一组程序时,翻译目标代码并不十分困难。可以分析每个程序以生成完整的代码路径集。代码翻译器实际上是模拟器的反射版本。它不是执行机器指令,而是输出执行指令的代码。

生成所有可能的代码路径集似乎令人望而生畏,甚至是不可能的。幸运的是,可以建立一个简单的基线:假设程序中的每个字节都是指令的开始,然后只翻译所有字节,将它们连接在一起,就可以建立完全覆盖——当然,除非程序生成程序代码。虽然这在实践中并不常见,尤其是在旧游戏中,但只有少量代码会被程序员分析和移植。

暴力破解方法使翻译后的代码大小大于严格意义上的必要大小。代码扩展主要归因于将程序数据翻译成宿主代码。其余未使用的宿主代码将来自与实际执行路径重叠的指令。多余的翻译代码本身不是问题,因为它永远不会被执行。

然而,额外的宿主代码内存大小可能在游戏中成为一个问题,因为相当大的 ROM 空间将用于图形。由于翻译后的代码将非常模糊,因此任何必要的更改也可能成为问题。这可以通过更微妙的方法来解决,该方法涉及跟踪来自所有已知入口点的所有可能的执行路径。只有实际需要的代码才会被翻译,并且没有数据会被翻译成代码。然而,这种方法会产生其他困难。

自动跟踪会遗漏一些代码,因为跳转表和其他构造对于跟踪子例程来说很困难。虽然可能不需要复杂的数据流分析来定位跳转表的基地址,但表的实际大小不太可能在代码中表达出来。也就是说,表中的索引通常不会进行边界检查,因为程序隐式地确保该值永远不会超出范围。可以使用许多启发式方法来确定表的范围,但仍然需要程序员检查大量情况。这种工作使得翻译器比模拟器更昂贵。

目标翻译语言

对于目标程序要翻译成的语言,有两种明显的选择:汇编语言或高级语言,如 C 或 Java。以汇编语言输出可以缩小语义差距,并且通常每条目标指令输入会输出两条或三条宿主指令。但是,这样的翻译器更难编写,并且在宿主机器之间的可移植性显然更有限。

以高级语言(我们现在只假设是 C 语言)输出不会运行得那么快,但会为我们提供一些技术优势。编译器优化将减少生成优化的翻译代码的需求。现在,使用程序流跟踪来消除从未执行的代码路径的额外工作将真正开始获得回报。任何翻译都通过消除指令提取和解码机制来获得相对于模拟的 speed advantage。基于程序流的翻译代码将保留原始程序的基本块。现在,编译器有机会省略冗余代码。例如,目标程序可能必须使用两条加法指令来求和三个数字。但是编译器可以识别出这两条指令可以用一条 3 操作数加法指令代替。此外,进位标志计算可以从第一个加法中完全删除,因为它会为第二个加法重新计算。

这并不是说编译器是某种万能药。数据流分析仍然很容易超过编译器。但是,它可以节省一些工作,并且可能足以胜任这项工作。

还应该指出的是,相信代码可以被翻译成类似于普通 C 语言(或 Java 或任何其他语言)的东西,这完全是愚蠢的。充其量,输出看起来会像用 C 语言编写的汇编语言代码,具有奇怪的语法,使其非常非常糟糕。但是,结构改进的神话非常强大;警惕其迷人的诱惑。虽然翻译后的代码在功能上是等效的,但它将是不可维护的。

虽然翻译后的代码不会特别令人愉快,但可以像处理任何源代码一样处理它。特别是,可以添加新代码以弥补翻译失败或进行任何必要的代码更改。

最重要的是,性能分析技术和工具的整个频谱都可以应用于提高执行速度。所有常用的经验都适用,并且不需要在此处回顾。但是,减少翻译循环的强度将是许多改进中的一个共同主题。

例如,考虑一个复制内存的循环。在一般情况下,复制的每个字节都需要从目标空间到宿主空间解码源地址和目标地址。

在大多数情况下,源和目标将是宿主空间中的连续内存块。与其翻译循环内的地址,不如在循环外翻译地址——并且循环本身变成使用本机指针的宿主内存复制。对于那些访问内存映射硬件寄存器的循环,差异甚至会更大,因为从内存写入到设备动作的转换通常更加昂贵。

当然,模拟和翻译之间没有硬性划分,因此一个非常有效的策略是让翻译代码的早期版本在到达那些未翻译的区域时恢复为模拟。这种混合方法可能是最终结果,翻译仅在必要时应用于程序中特别密集的部分。然而,缺点是模拟器需要原始目标程序,从而增加了额外的内存成本。但是,在模拟器和翻译代码之间切换与快速上下文切换的成本差不多。

展望二进制翻译的未来

二进制翻译的未来会怎样?需要翻译的二进制文件的创建正在放缓。即使在游戏行业,编码基线也已在很大程度上转移到 C 和 C++。此类游戏可以以传统方式移植。请注意,源代码确实会丢失——尽管该语言可能是直接可用的——但支持库、中间件和工具的源代码可能从未可用过。在这里,我们可以看到翻译成为移植技术套件的一部分,或者成为辅助我们重建原始代码的取证工具。

模拟本身仍将是将软件引入新平台的相对廉价的方式,尤其是在游戏行业。目标平台日益增长的复杂性将使模拟器更难编写,并且很可能更难快速运行。计算机体系结构的进步是模拟的双刃剑。例如,多媒体指令集扩展通常在跨越目标和宿主图形系统之间的语义差距方面非常有用,但几年后,模拟器程序员将面临模拟这些复杂指令的问题。尽管添加了这些 2D 和 3D 指令扩展,但指令添加将直接有益于模拟器编写者的希望渺茫。单个指令和总线解码中固有的微并行性将继续限制可以减少开销的程度。

然而,一些希望确实寄托于模拟器在主流中的日益普及,例如 Sun 的 Java 虚拟机和 Microsoft 的 .NET VM。存在一些市场压力,要求本机处理器更快地运行这些 VM,因此其他模拟器和翻译器的编写者将从中受益。但不要屏住呼吸太久;显然,增加对虚拟机的支持并不是芯片制造商的主要兴趣,他们的不同指令集会引发开发人员的忠诚度。

更激进的架构,例如 MIT 的 Oxygen 处理器,或(已披露的)索尼 PlayStation 3 架构,为更快地执行模拟器和翻译代码提供了机会。目标平台的各种并行子系统,例如地址解码、指令分派、指令执行等,比单个单片处理器更好地映射到一组小型处理器。该过程可以跨处理器流水线化,就像真实硬件完成任务的方式一样,而不是停止并等待数据或地址转换。

现场可编程门阵列 (FPGA) 技术为快速模拟器带来了最大的希望。可重构硬件显然可用于尝试模拟现有的目标硬件平台。然而,仍有待观察的是,FPGA 功能是否会迁移到主流处理器中。即使是构建专用指令的有限能力也可能有所帮助。分派和数据转换是模拟器和翻译器中常见的,但代价高昂的元素,它们通常在门和线路级别非常简单。

TRS-80:一个简单的模拟器

系统在复杂性方面差异很大,但 TRS-80 可以说明模拟器编写的基本任务。

Radio Shack TRS-80 微型计算机是我们编写的第一个模拟器。一个典型的系统包含 ROM 中的 Basic、16K RAM、Z-80 处理器、1K 内存映射显示和内存映射键盘。真正的系统有更多部件,但这已经是一个很好的开始。

内存映射
$0000–$2FFF Basic ROM
$3800–$38FF 键盘矩阵
$3C00–$3FFF 视频内存
$4000–$7FFF RAM

实现内存
一个 32,768 个无符号字符的数组就可以了。找一台旧的 TRS-80 获取 Basic-ROM 的副本,并使用它来初始化元素 0 到 12,287。

视频显示
视频内存组织为 64 列 16 行文本。支持字符图形。

实现:将每个视频字节映射到 ASCII 字符将是一个好的初步近似。

键盘
键盘如何映射到 $3800 有点复杂(见表 1)。
代码很简单:在宿主按键弹起和按下事件时,设置/重置内存数组中的相应位。你可以推迟这项工作,因为你不需要可操作的键盘来启动系统。”

实现 Z-80
这是最苛刻的任务。有 255 个操作码。其中,有四个前缀字节表示扩展操作码:$CB、$DD、$ED、$FD。大约有 630 个不同的前缀。前缀有很多内部模式可以利用来减少要编写的代码量。

编写代码,将键盘和图形放入一个漂亮的 GUI 包装器中,将 Z-80 PC 从 $0000 启动,你很快就会拥有一个模拟器。

表 1
地址 位 0 位 1 位 2 位 3 位 4 位 5 位 6 位 7
$3801 @ A B C D E F G
$3802 H I J K L M N O
$3804 P Q R S T U V W
$3808 X Y Z - - - - -
$3810 0 1 2 3 4 5 6 7
$3820 8 9 * + < = > ?
$3840 回车 清除 中断 向上 向下 向左 向右 空格
$3880 Shift              

梦幻之星 III

如表 1 所示,原始游戏在世嘉 Genesis(旧家用游戏机)上运行,并将在 Game Boy Advance(掌上系统)上模拟。

CPU 的性能非常接近,可以直接排除直接模拟。更大的卡带尺寸意味着我们可以容纳翻译引起的一些代码扩展。处理器不同的大小端会使翻译更加困难。

表 1 目标系统和宿主系统概述
Genesis Game Boy
CPU 16 位 8MHz 68000 32 位 16MHz ARM7
RAM 64 千字节 256 千字节
视频内存 64 千字节 96 千字节
视频 I/O DMA DMA 和直接访问
视频类型 瓦片和精灵 瓦片和精灵
字序 大端 小端
卡带 1 兆字节 8 兆字节
声音
FM 合成 PCM 采样

视频系统相似,但不相同。瓦片大小相同(8x8 像素),但在像素顺序上可以找到差异。更大的障碍是屏幕尺寸,基于电视的 Genesis 为 320x228,而 Game Boy 的尺寸为 240x160。由于过扫描,一些电视空间未使用。差异足够大,艺术家必须缩放许多图片。

声音系统呈现出最大的差距。FM 合成使用相对较少的参数来创建各种各样的声音效果,这些效果必须在宿主系统上用预先计算的采样来代替。幸运的是,我们有足够的额外卡带空间来容纳必要的采样。

在这种情况下,我们可以访问相对准确的源代码。编写了一个工具来比较已发布 ROM 的反汇编代码和源代码,以识别出一些差异并将其反向移植。从源代码进行翻译使得处理缩放后的图形变得更容易一些。翻译器(Perl 脚本)将汇编程序源代码转换为 C 宏,这些宏扩展为每个 68000 指令的实现。

最后,进行了广泛的测试,以确保 PSIII 的完成版本与原始版本完全相同。

GEORGE PHILLIPS 获得了不列颠哥伦比亚大学的硕士学位。他目前在 Digital Eclipse (http://www.digitaleclipse.com) 担任原创和模拟视频游戏的程序员。他还曾担任 Unix 系统管理员和商业网络爬虫的开发人员。

PETER PHILLIPS 获得了不列颠哥伦比亚大学的计算机科学学士学位。他在软件开发和系统管理方面拥有 15 年的经验,目前在 Digital Eclipse 担任开发人员。他正在从事 3D PC 游戏和模拟项目。

acmqueue

最初发表于 Queue 第 1 卷,第 6 期
数字图书馆 中评论本文








© 保留所有权利。

© . All rights reserved.