在C/ c++中解码和匹配Chip 8操作码

Decoding and matching Chip 8 opcodes in C/C++

本文关键字:Chip 操作码 解码 c++      更新时间:2023-10-16

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

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

Chip 8操作码每个2字节。这是来自指南的代码,我模糊地理解为在内存中添加8个额外的位空间[pc](使用<<8)然后将内存[pc + 1]与它合并(使用|)并将结果存储在opcode变量中。

现在我已经隔离了操作码,但是,我真的不知道该怎么处理它。我正在使用这个操作码表,我基本上失去了关于匹配十六进制操作码我读到操作码标识符在那张表。此外,我意识到我正在读取的许多操作码也包含操作数(我假设是后一个字节?),这可能使我的情况进一步复杂化。

帮助吗? !

基本上,一旦你有了指令,你需要解码它。例如,从您的操作码表:

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

并且猜测由于您每条指令访问两个字节,地址可能是一个(16位)字地址而不是字节地址,因此我将其左移一个(您需要研究这些指令是如何编码的,您提供的操作码表是不够的,那么不必做出假设)。

还有很多事情要做,我不知道我是否在我的github样本中写了任何关于它的东西。我建议你创建一个取指令的函数,一个读内存函数,一个写内存函数一个读寄存器函数,一个写寄存器函数。我推荐你的decode and execute函数一次只解码和执行一条指令。正常的执行只是在循环中调用它,它提供了中断和类似的能力,而不需要做很多额外的工作。它还使您的解决方案模块化。通过创建fetch() read_mem_byte() read_mem_word()等函数。你模块化你的代码(以轻微的性能代价),使调试更容易,因为你有一个地方,你可以观察寄存器或内存访问,并找出什么是或不是。

根据您的问题,以及您在此过程中的位置,我认为在编写模拟器之前需要做的第一件事是编写反汇编程序。作为一个固定的指令长度指令集(16位),这使它变得非常容易。你可以从房间里一些有趣的地方开始,如果你喜欢,也可以从最开始,解码你看到的所有东西。例如:

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

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

对于获取行代码的作用,你的理解不应该是模糊的,为了完成这个任务,你必须对位操作有深刻的理解。

我也会称你提供的那行代码有bug,或者至少有风险。如果memory[]是一个字节数组,编译器可能会很好地使用字节大小的数学方法执行左移,结果是零,那么第二个字节的零错误将只导致第二个字节。

基本上编译器在它的权利范围内转换这个:

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

这:

opcode = memory[pc + 1];

这对你根本不起作用,一个非常快速的修复:

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

会帮你省去一些麻烦。最小优化将使编译器不必为每个产生相同(期望的)输出/性能的操作将中间结果存储到ram中。

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

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

例如1NNN是跳转。所以:

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

那么游戏就是让代码变得更快或更小,或者更优雅。好干净的乐趣!

不同的cpu在内存中的存储值不同。大端机器在内存中存储像$FFCC这样的数字,顺序是FF,CC。小端机器以相反的顺序CC、FF存储字节(即"小端"在前)。

CHIP-8架构是大端序的,所以你要运行的代码中指令和数据都是大端序的。

在你的语句"opcode = memory[pc] <<8 | memory[pc + 1];",无论主机CPU(计算机的CPU)是小端还是大端都没关系。它将始终以正确的顺序将16位大端位值放入整数中。

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

为了解释这些操作码,你将不得不设置一堆不同的位掩码来从16位字中获得实际的操作码,并结合有限状态机,因为操作码的编码方式似乎有些复杂(即,某些操作码具有寄存器标识符等)。而另一些则相当直接,只有一个标识符)。

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

  1. 使用像"0xF000"这样的掩码获取第一口操作码。这将允许你"分类"操作码
  2. 基于步骤1的函数类别,应用更多的掩码来从操作码中获取寄存器值,或者使用操作码编码的任何其他变量,这将缩小需要调用的实际函数,以及它的参数。
  3. 一旦您有了操作码和变量信息,请查找一个固定长度的函数表,该函数表具有与操作码功能相一致的适当处理程序和与操作码一起使用的变量。虽然你可以,在你的状态机中,一旦你隔离了适当的功能,你就可以硬编码每个操作码的函数名,一个你用每个操作码的函数指针初始化的表是一种更灵活的方法,可以让你更容易地修改代码功能(也就是说,你可以很容易地在调试处理程序和"正常"处理程序之间切换,等等)。