X86/ARM 模拟器





5.00/5 (32投票s)
使用 C++ 和汇编语言为 .NET 环境编写的 X86/ARM 模拟器。
引言
快速搜索发现,万维网上有大量被称为模拟器、虚拟机和解释器的材料和工具。这些从高度复杂的到简单的学术练习不等。如果您缩小搜索范围并寻找 X86 汇编模拟器,您会发现商业 DOS 模拟器、Basic 语言工具、Java 工具(如 Jasmin)以及 CodeProject 的一篇题为“ASM.net X86”的文章等。另一大类相关工具是独立的 X86 调试器和反汇编器,它们既可以用于逐步执行代码片段,也可以用于静态或动态地检查生成的机器代码。本文提供了一些不同的东西。一个基于 Visual Studio Express 的解决方案,使用 C++ 和汇编语言编写,允许用户像使用文本编辑器一样轻松地逐行编写和执行 X86、X86/64 和 ARM 汇编代码。由于代码只是被模拟,错误会在程序内捕获并报告,而不会产生任何更广泛的影响。
更新
第一个版本仅为 X86 代码提供部分实现。现在可以通过下面的 Dropbox 链接获得一组更新的解决方案文件,该文件实现了一些 ARM 代码,特别是指令的数据处理子集。ARM 的操作数语法要求寄存器使用 r1-r15,数值立即数以 #(十进制)或 #&(十六进制)开头。条件码默认为 AL(始终),标志设置默认为未设置(空白)。
示例
标签 助记符 Cc 设置标志 Rd Rn Rm/imm 移位
label1: add al s r1 r2 #456
label2: and eq r0 r1 r4
label3: sub al s r4 r2 r1 lsl#4
adc al r3 r2 r4 ror#2
label4: eor al s r0 r1 #&7FF
背景
本文档不面向专家读者或专业应用程序,而是面向那些希望在 .NET 环境中发展 C++ 和汇编知识的人,或者喜欢为了一些狂热乐趣而拥有此类工具的想法的人。为了方便起见,我选择使用并提供基于 Visual Studio Express 2012 在 Windows 7 64 位系统下运行的材料(可执行文件为 X86 32 位)。我最初的目标是学习如何使用 Visual C++ 编写多格式应用程序,学习 X86 汇编,以及如何将这两者结合起来生成单个可执行文件。在此过程中,我尝试了内联汇编、单独的汇编模块、ARM 汇编和 X64 位汇编。
目前我没有展示一个完成的项目,而是一个正在进行的工作,其他人可能会尝试或甚至开发它。在 Windows 7 下,基本框架可以可靠地运行,X86 虚拟机对于核心指令集(在“帮助”菜单中列出)是完全功能的。ARM 和 X64 部分目前仅具说明性,X86 浮点尚未实现。我广泛使用了 Windows Forms,编写了类和线程,并尝试了一些 C 标准库。我研究了诸如 Atomic Class 之类的新材料,但由于混合使用托管代码和本机代码而遇到的问题阻止了我快速采用。我甚至尝试链接 FORTRAN 模块作为解释 X86 浮点指令的基础,并使用二进制文件进行数据交换,但 FORTRAN 的有效使用需要通过 .net 环境进行紧密集成。虽然存在此类工具和编译器(如 Silverfrost FTN95 等),但我无法获得完整的商业产品和相关的开发环境来试用这些想法。
如果您的兴趣是标准的面向对象开发,或者您是一名专业的 C++ 或汇编程序员,请绕道。我自 20 世纪 70 年代末以来就不是职业程序员了,当时我使用的是 ICL System 4 汇编和 Fortran IV。我在 ICT 领域度过了漫长的一生,但您很快就不得不放弃动手技术。C++ 是我的完美载体,它可以以多种形式使用和滥用。我已退休,可以玩耍,但我的真正目标是持续学习。
我从这个项目的动手编码中学到了很多。专业的 Windows 程序员在理解 Windows 引用类与类之间的区别,或者 Windows Forms 头文件在 Visual C++ 中如何工作的细节,或者汇编程序和多个 C++ 窗体之间需要静态、全局数据共享所引起的问题时,可能不会遇到困难。我避免了严格的父子窗体层次结构、专业解析器和编译器编写的精妙之处,以及严格的面向对象开发。我甚至使用了一些 GOTO。我的 FORTRAN 根源像野草一样生长。如果我现在开始,我会为头文件和类使用更具层次化的方法,并采用继承和条件编译来避免重复定义的问题并简化代码重用。
想法
我最初使用 MASM32 和类似工具学习 X86 汇编的尝试受到以下因素的阻碍:对低级 Windows 环境缺乏理解,对使用 Windows 控制台进行输入/输出的限制,以及汇编错误(至少在 Windows 7 之前)可能导致系统崩溃而不是仅仅测试程序崩溃的事实。作为一项学习练习,我决定编写一个 X86 汇编语言解释器,该解释器可以作为标准的 Windows Form 应用程序执行。C++ 具有从面向对象使用到低级代码的所有能力,似乎是这个项目的完美载体。关键要求是能够混合使用托管代码和本机代码,并使用汇编。我尝试了 Java,研究了诸如 Jasmin 等项目,但我认为 Visual Studio 环境比使用 Eclipse 或 Netbeans 来开发 GUI 应用程序(结合 Windows 和汇编语言以及 Java)更具挑战性。我明白我可以将我的目标设定为纯粹的 Java X86 汇编语言解释器,但这并没有让我兴奋。通常在软件开发中,缺乏一个稳定的规范来生产一个完成的“产品”是一个挑战,甚至是失败的原因。然而,我的第一个目标是提供一个软件框架,可以作为(我)学习的“测试平台”,不仅是 C++,也是被模拟的目标系统。这个概念类似于企业分析中经常使用的原型,用于在实际开发工作开始之前评估竞争技术解决方案的优点。
先决条件
我目前正在使用 Visual Studio 2012 Express 开发此项目,直接从该环境中运行可执行文件。如果您安装了最新版本的 Visual Studio 2012 X86 运行时(可从 Microsoft 免费下载),该可执行文件也可以直接运行。我的 PC 环境是 Windows 7 64 位,但我也在 Windows 8 下成功运行了此项目(使用 VS2012 Express for Desktop)。
已知问题
在 VS2012 下执行时,选择“生成”,然后在生成完成后选择“启动但不调试”。如果选择“启动调试”,应用程序并不总是可靠运行。非常偶尔地,并且似乎是随机地,在执行过程中我遇到了通用的 GDI+ 错误,但如果发生错误,可以“继续”执行或重新启动应用程序。这显然不是 GDI 对象溢出问题,但也很难诊断出实际原因。
限制
X86 和 X64 机器采用扁平内存模型运行,32 位和 64 位操作完全独立。包含段寄存器但初始化为零。绝对地址仅从偏移量计算,不参考段寄存器。对于 32 位模拟,数据、代码和堆栈基地址分别预配置为 0x00、0x0600 和 0x0F00,内存顶部设置为 0x01000。内存中的值以小端格式存储。在 X86 模式下,只允许使用 32 位指令,但可以使用为移位指令等定义的 16 位寄存器。允许的指令和操作数类型可以在每个机器的“帮助”菜单中找到。中断、调用约定和调用堆栈未实现。
X64 实现正在开发中,以允许同时使用 64 位和 32 位寄存器。段寄存器初始化为零。数据、堆栈和代码内存分别从地址 0x00、0x01000 和 0x02000 开始。
模拟器应用程序尚未包含系统事件(如 DEP、地址错误、系统堆错误等)的异常处理。
可视化指南
下面是每个模拟模式的用户界面示意图。下载附带的图片文件,全屏查看以检查细节。
安装
通过下面的 Dropbox 链接提供的文件包括:一组应用程序在每种模拟模式下运行的屏幕快照(.jpg 文件)、一个压缩的 Windows 32 位可执行文件(.zip)以及 Visual Studio 解决方案文件夹(已压缩和未压缩)。下载文件并解压 x64interprettest.zip 以提取可执行文件。安装 Microsoft 网站上提供的 Visual Studio 2012 C++ X86 运行时,导航到包含解压的可执行文件的文件夹,然后双击 x64interprettest.exe 文件。根据您的 PC 环境和用户凭据,您可能需要右键单击并以管理员身份运行 .exe 文件,以及/或覆盖实时防病毒警告。在 Windows 8 下,如果您看到一个提示应用程序未授权的框,请点击 更多信息,然后选择 仍要运行。可执行文件的 MD5 校验和是
(using WinMD5Free) Not yet available for this update
用于生成此可执行文件的 Visual Studio 2012 解决方案可从 DropBox 下载,链接如下:
https://www.dropbox.com/sh/i0ozt6expnepg37/iutkJytnH4
下载后解压 X86Emulator.zip 文件。这将构建文件夹 ...\X86Emulator\interprettest,包含所需的子文件夹和文件。使用 Visual Studio Express 2012 for Desktop 打开 ...\interprettest 文件夹内的 x64interprettest.sln 文件。在 生成 之前,您需要确认配置和项目属性设置正确。首先,确保屏幕顶部功能区菜单中的配置设置为 Release 和 Win32。我的测试表明,只要配置设置为 Release/Win32,项目属性就会保存在解决方案文件中。但是,要确认这一点或解决任何 生成 “配置”错误(见下文),请右键单击“解决方案资源管理器”顶部的 x64interprettest,然后选择 属性。使用选项卡式页面检查并根据需要调整以下内容(确保在每个页面上单击 应用)
Config. Properties->General->set Common.Lang.R.Time = Common.Lang.R.Time (/clr)
Config. Properties->Debugging->set Debugger Type->Mixed
Config. Properties->C/C++->General->set Common.Lang.R.Time = Common.Lang.R.Time (/clr)
Config. Properties->C/C++->Preprocessor->edit Preproc.Defns->remove any _Debug or NDEBUG
Config. Properties->Linker->Advanced->set Image has safe exception handlers->NO
Config. Properties->Resources->Gen.->Preproc.Defns->edit to remove any _Debug or NDEBUG
同样在解决方案资源管理器中,找到源文件 assm1.asm, 右键单击并选择属性,然后按如下方式编辑:
Config. Properties->General->Excluded from Build->set NO
Config. Properties->General->Item Type->set Custom Build Tool
Config. Prop.->Custom Build Tool->Command Line->edit to->ml -c -Zi -Sa assm1.asm assm1.obj
Config. Properties->Custom Build Tool->Outputs->edit to read->assm1.obj
单击应用。对于上面的最后一项,确保没有路径,以便 assm1.obj 写入默认文件夹。
在设置了上述属性和配置后,选择 生成,并在完成后选择 调试\启动但不调试。如果后者选项变灰,请使用选项选择“专家模式”。注意,如果生成失败,因为选择了 PURE,安全事件处理程序设置为 Yes,或者找不到 assm1.obj 文件,请重新检查以上内容。
用途
初始屏幕提供多种模拟模式供选择。选择 X86。加载后,光标将定位在代码输入行上。例如,制表符移至 助记符 字段并输入 add,然后制表符移至第一个和第二个操作数字段并输入 eax 和 45。单击代码输入线上方的 翻译代码,然后单击 执行 按钮。之后可以逐行以相同方式输入更多代码。数据可以直接使用 数据输入 菜单输入到寄存器、堆栈和内存中。例如,要更改 eax,请单击显示 eax 值的框,输入十进制数或以 0x 开头的十六进制数,然后选择 文件/寄存器/EAX。您输入的应确认为您输入的 eax 值。如果未确认,则会在“诊断”框中显示相应的错误消息。要在内存中输入值,您必须填写与显示相关内存段的框相邻的地址和值框,然后从 数据输入 菜单中选择所需的选项。堆栈也类似,填写堆栈显示上方的字段,然后使用 数据输入 菜单,如前所述。有关更多说明和操作数类型,请查阅 帮助/顶部菜单。输入一段代码后,可以通过将 eip 重置为 0 或 0x600 并单击 执行代码块 按钮来执行整个代码块。更改 eip 的方式与更改 eax 类似。将 eip 框设置为所需值,然后单击菜单行上的 重置/更改 eip 按钮。
IN 和 OUT 指令可以用作非常简单的 Windows 控制台流来输入或输出数值。注意,如果在运行过程中出现调试断言错误,尤其是在定义常量时,请确保生成配置设置为 Release & Win32 并且所有项目属性均按上述设置。
变量、标签和常量
模拟器可以接受数据和标签定义。对于标签,使用代码行中的标签字段输入标签名称,后跟 : 或 ::(定义近标签和远标签),然后是所需的汇编代码。像以前一样单击 翻译代码,然后单击 执行。对于数据,在 .Data 输入行上输入变量或常量定义,然后单击 翻译数据,然后单击 执行。例如:
Label1: add eax 234
Label2: equ near (':' optional with near & far)
var1 dword 45
const1 equ 336
代码
模拟器是一个 C++ Windows Form 应用程序,包含多个链接的窗体,这些窗体又利用外部函数。后者是一些混合体:C++、使用内联汇编的非托管 C++,以及一个仅 X86 汇编模块。由于应用程序的最初目的是作为学习辅助工具,它已发展成为一个用于尝试编码想法的“测试平台”。它不是面条式代码,但也不是命名或结构最佳实践的典范。该软件包含大量函数重复,以试用不同的方法,以及一些早期版本的冗余代码(大部分已注释掉)。全局变量被广泛使用,以简化窗体和线程之间的数据共享。尽管如此,X86 模拟器仍然具有直接的架构。
Translate 函数解析输入的代码行,检查操作数对于指令类型是否有效,并将输入行转换为令牌流,然后将其存储在内存中的一系列结构化记录中。当用户选择 Execute 函数时,代表指令的令牌值用作参数来选择所需的执行例程。Execute 函数既充当解释器,生成指令执行的结果,也发出机器代码表示。代码、数据和堆栈内存以及寄存器都会被更新。实现了一系列辅助函数来加快使用速度,例如允许直接将数据输入寄存器和内存,以及重置虚拟机的各种方面。输入多行代码后,用户可以通过将 eip 重置为 0 或 0x600 并单击 Execute Code Block 函数来执行整个“程序”,该函数使用从先前解析的指令构建的结构化记录。与逐行执行不同,代码块执行不是顺序的,而是遵循代码中的分支、循环和跳转。
X64 和 ARM 模拟
X86 64 位模拟目前仅是一个框架。您可以以类似于 X86 32 位模拟的方式输入代码或数据定义,但界面已简化,所有内容都在一行中输入,并提供一个单一的 Assemble 按钮来翻译和执行输入的代码。如果您下载并检查屏幕截图的细节,您可以看到到目前为止实现的少量指令。菜单条上的大多数按钮也是可操作的,例如 Reset stack。此模拟正在使用离散的类进行开发,用于令牌解析、虚拟机数据处理、解释执行和机器代码生成。最后,已实现 ARM 模拟的第一步,其构思围绕着 ARM 32 位指令集的编码和解码。在完成的版本中,用户将能够输入汇编助记符进行翻译和执行,或者输入一系列 32 位二进制值作为机器指令流。此框架尚不可操作。单击 Translate 和 Execute 按钮仅用于演示对几个硬编码测试值的解码。最后一个组件是在等待获取 Raspberry PI 时添加的。随着后者教育用途的普及,可能会有更多人对简单的 ARM 模拟感兴趣,而不仅仅是商业工具(由开发嵌入式系统和移动设备应用程序的人使用)。
结论
这个持续的项目有非常个人的目标,但我希望它能吸引有类似兴趣的人。