【问题标题】:Decoding and matching Chip 8 opcodes in C/C++在 C/C++ 中解码和匹配芯片 8 操作码
【发布时间】:2011-09-30 23:36:46
【问题描述】:

我正在编写一个 Chip 8 仿真器作为仿真的介绍,我有点迷路了。基本上,我已经读取了 Chip 8 ROM 并将其存储在内存中的 char 数组中。然后,按照指南,我使用以下代码在当前程序计数器(pc)处检索操作码:

// Fetch opcode
opcode = memory[pc] << 8 | memory[pc + 1];

芯片 8 个操作码每个是 2 个字节。这是来自指南的代码,我模糊地理解为向 memory[pc] 添加 8 个额外的位空间(使用

现在我已经隔离了操作码,但我真的不知道如何处理它。我正在使用this 操作码表,我基本上迷失了将我读取的十六进制操作码与该表中的操作码标识符相匹配的问题。另外,我意识到我正在阅读的许多操作码也包含操作数(我假设是后一个字节?),这可能使我的情况更加复杂。

帮忙?!

【问题讨论】:

  • 请注意,您说“一个字符数组”,您可能希望内存是一个“无符号字符数组”,因此无论编译器如何它都可以工作。我写了一个 Apple 2 模拟器,所以模拟了 6502 处理器,不过要容易得多。 sourceforge.net/projects/apple-emulator

标签: c++ c emulation decode opcode


【解决方案1】:

基本上——屏蔽掉操作码的可变部分,并寻找匹配项。然后使用可变部分。

例如 1NNN 是跳转。所以:

int a = opcode & 0xF000;
int b = opcode & 0x0FFF;
if(a == 0x1000)
   doJump(b);

那么游戏就是让代码变得更快、更小或更优雅,如果你愿意的话。好干净的乐趣!

【讨论】:

    【解决方案2】:

    您将不得不设置一堆不同的位掩码来从 16 位字中获取实际的操作码,并结合有限状态机来解释这些操作码,因为看起来存在一些复杂性操作码是如何编码的(例如,某些操作码具有寄存器标识符等,而其他操作码则相当简单,只有一个标识符)。

    你的有限状态机基本上可以做以下事情:

    1. 使用类似 `0xF000 的掩码获取操作码的第一个半字节。这将允许您“分类”操作码
    2. 根据第 1 步中的函数类别,应用更多掩码以从操作码中获取寄存器值,或者使用操作码对任何其他变量进行编码,从而缩小需要调用的实际函数,如以及它的论点。
    3. 获得操作码和变量信息后,查找具有适当处理程序的函数的固定长度表,以与操作码功能和随操作码一起出现的变量一致。虽然您可以在您的状态机中硬编码一旦您隔离了正确的功能后将与每个操作码一起使用的函数的名称,但您使用每个操作码的函数指针初始化的表是一种更灵活的方法将使您更轻松地修改代码功能(即,您可以轻松地在调试处理程序和“正常”处理程序之间进行交换等)。

    【讨论】:

      【解决方案3】:

      基本上,一旦您获得了需要对其进行解码的指令。例如来自您的操作码表:

      if ((inst&0xF000)==0x1000)
      {
        write_register(pc,(inst&0x0FFF)<<1);
      }
      

      并且猜测由于您要访问 rom 每条指令两个字节,地址可能是(16 位)字地址而不是字节地址,所以我将它向左移动了一个(您需要研究这些指令是如何编码的,操作码你提供的表格是不够的,不用做假设)。

      还有很多事情要做,我不知道我是否在我的 github 示例中写过任何关于它的内容。我建议您创建一个用于在地址处获取指令的 fetch 函数、一个读内存函数、一个写内存函数、一个读寄存器函数、一个写寄存器函数。我建议您的解码和执行功能一次只解码和执行一条指令。正常执行只是在循环中调用它,它提供了执行中断和类似操作的能力,而无需大量额外工作。它还模块化您的解决方案。通过创建 fetch() read_mem_byte() read_mem_word() 等函数。您可以将代码模块化(以稍微降低性能为代价),使调试变得更加容易,因为您可以在一个地方查看寄存器或内存访问并确定发生了什么或没有发生什么。

      根据您的问题,以及您在此过程中所处的位置,我认为在编写模拟器之前您需要做的第一件事就是编写反汇编程序。作为一个固定指令长度的指令集(16 位),这使得它变得更加容易。您可以从 rom 中的某个有趣点开始,或者如果您愿意,也可以从头开始,然后解码您看到的所有内容。例如:

      if ((inst&0xF000)==0x1000)
      {
        printf("jmp 0x%04X\n",(inst&0x0FFF)<<1);
      }
      

      只有 35 条指令不应该花一个下午,也许是整个星期六,这是您第一次解码指令(我假设这是基于您的问题)。反汇编程序成为您的模拟器的核心解码器。用仿真替换 printf()s,最好保留 printfs,只添加代码来模拟指令执行,这样您就可以跟踪执行。 (同样的交易有一个反汇编单个指令函数,为每条指令调用它,这将成为您的模拟器的基础)。

      您需要对获取代码行的作用非常模糊,为了完成这项任务,您必须对位操作有深入的了解。

      另外,我认为你提供的那行代码有问题,或者至少有风险。如果 memory[] 是一个字节数组,编译器可能会很好地使用字节大小的数学运算来执行左移,产生一个零,然后与第二个字节进行零或与第二个字节相交。

      基本上,编译器有权改变这个:

      opcode = memory[pc] << 8) | memory[pc + 1];
      

      进入这个:

      opcode = memory[pc + 1];
      

      这根本不适合你,一个非常快速的解决方法:

      opcode = memory[pc + 0];
      opcode <<= 8;
      opcode |= memory[pc + 1];
      

      将为您省去一些麻烦。最小的优化将使编译器免于为每个操作将中间结果存储到 ram 中,从而产生相同的(所需的)输出/性能。

      我在上面编写和提到的指令集模拟器不是为了提高性能,而是为了提高可读性、可见性,并希望具有教育意义。我会从类似的东西开始,然后如果对性能感兴趣,你将不得不重新编写它。这个chip8模拟器,一旦体验过,将是一个下午从头开始的任务,所以一旦你第一次完成这个任务,你可能会在一个周末重新编写它三四次,而不是一个巨大的任务(必须重新编写)。 (拇指调节器花了我一个周末,大部分时间。msp430 可能更像是一个晚上或两个晚上的工作。一劳永逸地正确设置溢出标志是最大的任务,后来才出现)。无论如何,重点是,看看诸如mame源之类的东西,即使不是全部,大多数指令集模拟器都是为执行速度而设计的,如果没有大量研究,许多指令集几乎无法阅读。通常是大量的表驱动,有时还有很多 C 编程技巧等。从可管理的东西开始,让它正常运行,然后担心改进它的速度、大小或可移植性等。这个chip8的东西看起来是基于图形的,所以你还必须在位图/屏幕/任何地方处理大量的线条绘制和其他位操作。或者你可以只调用 api 或操作系统函数。基本上这个chip8的东西不是你传统的带有寄存器和寻址模式和alu操作的清单的指令集。

      【讨论】:

      • 我赞扬您花时间写出这个出色且非常详细的回复。这对我很有帮助。非常感谢!反汇编程序的想法很棒,我肯定会接受它。我不知道为什么我以前没有想到。
      • ++ 建议先写一个反汇编程序。可视化,可视化,可视化。
      【解决方案4】:

      不同的 CPU 在内存中存储值的方式不同。 Big endian 机器以 FF,CC 的顺序在内存中存储像 $FFCC 这样的数字。 Little-endian 机器以相反的顺序 CC、FF 存储字节(即,“小端”在前)。

      CHIP-8 架构采用大端序,因此您将运行的代码具有以大端序编写的指令和数据。

      在您的语句“opcode = memory[pc]

      有一些资源可能会有所帮助:http://www.emulator101.com 提供了 CHIP-8 仿真器教程以及一些通用仿真器技术。这个也不错:http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-15
        相关资源
        最近更新 更多