创建一个模拟器街机。 第三部分

图片

第一部分和第二部分

8080处理器仿真器


仿真器外壳


现在,您应该具有开始创建8080处理器仿真器的所有必要知识。

我将尝试使我的代码尽可能清晰,每个操作码都是单独实现的。 当您适应它时,可能需要重写它以优化性能或重用代码。

首先,我将创建一个内存结构,其中将包含编写反汇编程序时所需的所有内容的字段。 还将有一个用于内存缓冲区的地方,它将是RAM。

typedef struct ConditionCodes { uint8_t z:1; uint8_t s:1; uint8_t p:1; uint8_t cy:1; uint8_t ac:1; uint8_t pad:3; } ConditionCodes; typedef struct State8080 { uint8_t a; uint8_t b; uint8_t c; uint8_t d; uint8_t e; uint8_t h; uint8_t l; uint16_t sp; uint16_t pc; uint8_t *memory; struct ConditionCodes cc; uint8_t int_enable; } State8080; 

现在创建一个带有错误调用的过程,该过程将以错误结束该程序。 它看起来像这样:

  void UnimplementedInstruction(State8080* state) { // pc    ,     printf ("Error: Unimplemented instruction\n"); exit(1); } int Emulate8080Op(State8080* state) { unsigned char *opcode = &state->memory[state->pc]; switch(*opcode) { case 0x00: UnimplementedInstruction(state); break; case 0x01: UnimplementedInstruction(state); break; case 0x02: UnimplementedInstruction(state); break; case 0x03: UnimplementedInstruction(state); break; case 0x04: UnimplementedInstruction(state); break; /*....*/ case 0xfe: UnimplementedInstruction(state); break; case 0xff: UnimplementedInstruction(state); break; } state->pc+=1; //  } 

让我们实现一些操作码。

  void Emulate8080Op(State8080* state) { unsigned char *opcode = &state->memory[state->pc]; switch(*opcode) { case 0x00: break; //NOP -  ! case 0x01: //LXI B, state->c = opcode[1]; state->b = opcode[2]; state->pc += 2; //   2  break; /*....*/ case 0x41: state->b = state->c; break; //MOV B,C case 0x42: state->b = state->d; break; //MOV B,D case 0x43: state->b = state->e; break; //MOV B,E } state->pc+=1; } 

你去。 对于每个操作码,我们都会更改状态和内存,就像在真实8080上执行的命令一样。

8080大约有7种类型,具体取决于您如何对其进行分类:

  • 资料传输
  • 算术运算
  • 逻辑上
  • 分行
  • 叠放
  • 输入输出
  • 特别的

让我们分别看看它们。

算术组


算术指令是8080处理器的256个操作码中的许多,其中包括各种加法和减法。 大多数算术指令与寄存器A一起使用,并将结果保存在A中。(寄存器A也称为累加器)。

有趣的是,这些命令会影响条件代码。 根据执行命令的结果设置状态码(也称为标志)。 并非所有命令都会影响标志,并且并非所有影响标志的团队都会立即影响所有标志。

旗帜8080


在8080处理器中,标记称为Z,S,P,CY和AC。

  • 当结果为零时,Z(零,零)取值1
  • 给定数学命令的位7(最高有效位,最高有效位,MSB)时,S(符号)取值为1
  • P(奇偶校验,奇偶校验)在结果为偶数时设置,在结果为奇数时重置
  • 当命令的结果是执行转移或借入高阶位时,CY(进位)取值为1
  • AC(辅助进位)主要用于BCD(二进制编码的十进制)数学。 有关更多详细信息,请参见《太空侵略者》中的手册,该标志未使用。

状态代码用于条件分支命令中,例如,仅当设置了Z标志时,JZ才执行分支。

大多数指令具有三种形式:用于寄存器,用于立即数和用于存储器。 让我们实施一些指令以理解其形式,并查看使用状态码的样子。 (请注意,由于未使用辅助传输标志,因此未实现。如果实现,则无法测试。)

登记表格


这是两个具有寄存器形式的指令的示例实现; 在第一个示例中,我部署了代码以使其工作更易于理解,第二个示例中,我提出了一种更紧凑的形式来执行相同的操作。

  case 0x80: //ADD B { //      , //      uint16_t answer = (uint16_t) state->a + (uint16_t) state->b; //  :    , //    , //      if ((answer & 0xff) == 0) state->cc.z = 1; else state->cc.z = 0; //  :   7 , //    , //      if (answer & 0x80) state->cc.s = 1; else state->cc.s = 0; //   if (answer > 0xff) state->cc.cy = 1; else state->cc.cy = 0; //    state->cc.p = Parity( answer & 0xff); state->a = answer & 0xff; } //  ADD     case 0x81: //ADD C { uint16_t answer = (uint16_t) state->a + (uint16_t) state->c; state->cc.z = ((answer & 0xff) == 0); state->cc.s = ((answer & 0x80) != 0); state->cc.cy = (answer > 0xff); state->cc.p = Parity(answer&0xff); state->a = answer & 0xff; } 

我用16位数字模拟8位数学命令。 这样可以更轻松地跟踪计算产生进位的情况。

立即价值表格


立即值的形式几乎相同,除了命令后的字节是添加的源。 由于“操作码”是内存中当前命令的指针,因此操作码[1]将立即成为下一个字节。

  case 0xC6: //ADI  { uint16_t answer = (uint16_t) state->a + (uint16_t) opcode[1]; state->cc.z = ((answer & 0xff) == 0); state->cc.s = ((answer & 0x80) != 0); state->cc.cy = (answer > 0xff); state->cc.p = Parity(answer&0xff); state->a = answer & 0xff; } 

形状记忆


在存储形式中,将添加一个字节,一对HL寄存器中存储的地址指示该字节。

  case 0x86: //ADD M { uint16_t offset = (state->h<<8) | (state->l); uint16_t answer = (uint16_t) state->a + state->memory[offset]; state->cc.z = ((answer & 0xff) == 0); state->cc.s = ((answer & 0x80) != 0); state->cc.cy = (answer > 0xff); state->cc.p = Parity(answer&0xff); state->a = answer & 0xff; } 

注意事项


其余的算术指令以类似的方式实现。 补充:

  • 根据参考手册,在带有进位的不同版本(ADC,ACI,SBB,SUI)中,我们在计算中使用进位。
  • INX和DCX影响寄存器对;这些命令不影响标志。
  • DAD是一对寄存器的另一个命令,它仅影响进位标志
  • INR和DCR不影响进位标记

分支组


处理完状态代码后,分支组将对您足够清楚。 分支有两种类型-过渡(JMP)和调用(CALL)。 JMP只是将PC设置为跳转目标的值。 CALL用于例程,它将返回地址写入堆栈,然后为PC分配目标地址。 RET从CALL返回,从堆栈中接收地址并将其写入PC。

JMP和CALL都只能到达操作码后以字节编码的绝对地址。

Jmp


JMP命令无条件分支到目标地址。 对于所有状态代码(AC除外),还有条件分支命令:

  • JNZ和JZ为零
  • JNC和JC进行迁移
  • JPO和JPE实现奇偶校验
  • JP(加号)和JM(减号)

这是其中一些的实现:

  case 0xc2: //JNZ  if (0 == state->cc.z) state->pc = (opcode[2] << 8) | opcode[1]; else //    state->pc += 2; break; case 0xc3: //JMP  state->pc = (opcode[2] << 8) | opcode[1]; break; 

通话和退回


调用后,CALL将指令的地址压入堆栈,然后跳转到目标地址。 RET从堆栈接收地址并将其保存在PC上。 所有状态都存在条件版本的CALL和RET。

  • CZ,CNZ,RZ,RNZ为零
  • CNC,CC,RNC,RC进行传输
  • CPO,CPE,RPO,RPE用于奇偶校验
  • CP,CM,RP,RM表示符号

  case 0xcd: //CALL  { uint16_t ret = state->pc+2; state->memory[state->sp-1] = (ret >> 8) & 0xff; state->memory[state->sp-2] = (ret & 0xff); state->sp = state->sp - 2; state->pc = (opcode[2] << 8) | opcode[1]; } break; case 0xc9: //RET state->pc = state->memory[state->sp] | (state->memory[state->sp+1] << 8); state->sp += 2; break; 

注意事项


  • PCHL命令无条件地跳转到一对HL寄存器中的地址。
  • 我没有将先前讨论的RST包括在该组中。 它将返回地址写入堆栈,然后跳转到内存底部的预定义地址。

逻辑组


该小组执行逻辑操作(请参阅教程的第一篇文章 )。 从本质上讲,它们类似于算术组,因为大多数操作都使用寄存器A(驱动器),并且大多数操作会影响标志。 所有操作均针对8位值执行,在该组中,没有命令影响寄存器对。

布尔运算


AND,OR,NOT(CMP)和“异或”(XOR)被称为布尔运算。 OR和AND我已经在前面进行了解释。 NOT命令(对于8080处理器,它称为CMA或补码累加器)仅更改位值-所有单位变为零,而零变为1。

我将XOR视为“差异识别器”。 她的真值表如下所示:

Xÿ结果
000
01个1个
1个01个
1个1个0

AND,OR和XOR具有寄存器,存储器和立即数的形式。 (CMP仅具有区分大小写的命令)。 这是一对操作码的实现:

  case 0x2F: //CMA (not) state->a = ~state->a //  ,  CMA     break; case 0xe6: //ANI  { uint8_t x = state->a & opcode[1]; state->cc.z = (x == 0); state->cc.s = (0x80 == (x & 0x80)); state->cc.p = parity(x, 8); state->cc.cy = 0; //  ,  ANI  CY state->a = x; state->pc++; //   } break; 

循环移位命令


这些命令更改寄存器中位的顺序。 向右移动可将它们向右移动一位,向左移动-可向左移动一位:

(0b00010000) = 0b00001000

(0b00000001) = 0b00000010

他们似乎一文不值,但实际上并非如此。 它们可用于乘以除以二的幂。 以左移为例。 0b00000001是十进制1,将其向左移动将使其变为0b00000010 (即十进制2)。如果再向左移动, 0b00000100得到0b00000100 ,即4。再向左移动,然后再乘以8。这将适用于任何按数字:5( 0b00000101 )左移时得到10( 0b00001010 )。 另一个左移为20( 0b00010100 )。 向右移动的方法相同,但用于除法。

8080没有乘法命令,但是可以使用这些命令来实现。 如果您知道该怎么做,您将获得奖励积分。 在面试中问了这样一个问题。 (我做了,尽管花了我几分钟。)

这些命令周期性地旋转驱动器,并且仅影响进位标志。 这是几个命令:

  case 0x0f: //RRC { uint8_t x = state->a; state->a = ((x & 1) << 7) | (x >> 1); state->cc.cy = (1 == (x&1)); } break; case 0x1f: //RAR { uint8_t x = state->a; state->a = (state->cc.cy << 7) | (x >> 1); state->cc.cy = (1 == (x&1)); } break; 

比较方式


CMP和CPI的任务仅是设置标志(用于分支)。 他们通过减去标志来做到这一点,但不存储结果。

  • 同样:如果两个数字相等,则设置Z标志,因为它们彼此相减得出零。
  • 大于:如果A大于要比较的值,则清除CY标志(因为可以进行减法而无需借用)。
  • 较小:如果A小于比较值,则设置CY标志(因为A必须完成借位才能完成减法)。

这些命令有用于寄存器,存储器和立即数的版本。 该实现是一个简单的减法,但不保存结果:

  case 0xfe: //CPI  { uint8_t x = state->a - opcode[1]; state->cc.z = (x == 0); state->cc.s = (0x80 == (x & 0x80)); //  ,    p -   state->cc.p = parity(x, 8); state->cc.cy = (state->a < opcode[1]); state->pc++; } break; 

CMC和STC


他们完成了逻辑组。 它们用于设置和清除进位标志。

输入输出和特殊命令组


这些命令不能分配给任何其他类别。 为了完整起见,我会提到它们,但是在我看来,当我们开始模仿“太空侵略者”的硬件时,我们将不得不再次回到它们身上。

  • EI和DI启用或禁用处理器处理中断的能力。 我将interrupt_enabled标志添加到处理器状态结构,并使用以下命令对其进行设置/重置。
  • 似乎RIM和SIM主要用于串行I / O。 如果您有兴趣,可以阅读手册,但是这些命令在Space Invaders中不使用。 我不会效仿它们。
  • HLT是一站式服务。 我认为我们不需要模仿它,但是您可以在看到此命令时调用退出(或退出(0))代码。
  • IN和OUT是8080处理器设备用于与外部设备进行通信的命令。 虽然我们正在实现它们,但是它们除了跳过数据字节外不会做任何事情。 (稍后我们将返回给他们)。
  • NOP是“无操作”。 NOP的一种应用是控制面板定时(执行需要四个CPU周期)。

NOP的另一个应用是代码修改。 假设我们需要更改游戏的ROM代码。 我们不能只删除不必要的操作码,因为我们不想更改所有的CALL和JMP命令(如果至少移动一部分代码,它们将是不正确的)。 使用NOP,我们可以摆脱代码。 添加代码要困难得多! 您可以通过在ROM中的某个位置找到空间并将命令更改为JMP来添加它。

堆叠组


我们已经完成了堆栈组中大多数团队的机制。 如果您与我一起工作,那么这些命令将很容易实现。

推和弹出


PUSH和POP仅适用于寄存器对。 PUSH将一对寄存器写入堆栈,而POP从堆栈顶部获取2个字节,并将它们写入一对寄存器。

对于PUSH和POP,有四个操作码,每个对分别为:BC,DE,HL和PSW。 PSW是一对特殊的驱动器标志寄存器和状态代码。 这是我对BC和PSW的PUSH和POP的实现。 其中没有评论-我认为这里没有特别棘手的内容。

  case 0xc1: //POP B { state->c = state->memory[state->sp]; state->b = state->memory[state->sp+1]; state->sp += 2; } break; case 0xc5: //PUSH B { state->memory[state->sp-1] = state->b; state->memory[state->sp-2] = state->c; state->sp = state->sp - 2; } break; case 0xf1: //POP PSW { state->a = state->memory[state->sp+1]; uint8_t psw = state->memory[state->sp]; state->cc.z = (0x01 == (psw & 0x01)); state->cc.s = (0x02 == (psw & 0x02)); state->cc.p = (0x04 == (psw & 0x04)); state->cc.cy = (0x05 == (psw & 0x08)); state->cc.ac = (0x10 == (psw & 0x10)); state->sp += 2; } break; case 0xf5: //PUSH PSW { state->memory[state->sp-1] = state->a; uint8_t psw = (state->cc.z | state->cc.s << 1 | state->cc.p << 2 | state->cc.cy << 3 | state->cc.ac << 4 ); state->memory[state->sp-2] = psw; state->sp = state->sp - 2; } break; 

SPHL和XTHL


堆栈组中还有另外两个团队-SPHL和XTHL。

  • SPHL将HL移至SP(强制SP获得新地址)。
  • XTHL将堆栈顶部的内容与一对HL寄存器中的XTHL交换。 您为什么需要这样做? 我不知道

关于二进制数的更多信息


编写计算机程序时,需要做出的决定之一就是选择用于数字的数据类型-是否希望它们为负数,以及它们的最大大小应为多少。 对于CPU仿真器,我们需要数据类型与目标CPU的数据类型匹配。

已签名和未签名


当我们开始讨论十六进制数时,我们认为它们是无符号的-也就是说,十六进制数的每个二进制数都有一个正值,并且每个被认为是2的幂(单位,2、4等)。

我们处理负数的计算机存储问题。 如果您知道所讨论的数据带有符号,即它们可以是负数,则可以通过数字的最高有效位(最高有效位,MSB)来识别负数。 如果数据大小为一个字节,则每个具有给定MSB位值的数字均为负,而每个具有零MSB的值为正。

负数的值将作为附加代码存储。 如果我们有一个带符号的数字,并且MSB等于1,并且我们想找出这个数字是什么,则可以按如下所示进行转换:对十六进制数字执行二进制“ NOT”,然后加一个。

例如,对于十六进制数字0x80,MSB位置1,即为负。 数字0x80的二进制“ NOT”为0x7f,即十进制127。127 + 1 =128。即,十进制0x80为-128。 第二个示例:0xC5。 不是(0xC5)= 0x3A =十进制58 +1 =十进制59。也就是说,0xC5是十进制-59。

使用附加代码在数字上令人惊讶的是,我们可以像对无符号数字一样对它们执行计算,并且它们仍然可以使用 。 电脑不需要对标志做任何特殊的事情。 我将展示一些例子来证明这一点。

 例子1

     十进制十六进制二进制    
       -3 0xFD 1111 1101    
    + 10 0x0A +0000 1010    
    ----- -----------    
        7 0x07 1 0000 0111    
                        ^记录在进位位

   例子2    

     十进制十六进制二进制    
      -59 0xC5 1100 0101    
    + 33 0x21 +0010 0001    
    ----- -----------    
      -26 0xE6 1110 0110 


在示例1中,我们看到将10和-3的结果相加为7,将结果相加,因此可以设置C标志;在示例2中,相加结果为负,因此我们将其解码为:Not(0xE6)= 0x19 = 25 + 1 = 0xE6 = -26大脑爆炸!

如果需要,请阅读有关Wikipedia上的其他代码的更多信息。

资料类型


在C语言中,数据类型和用于此类型的字节数之间存在关系。 实际上,我们只对整数感兴趣。 标准/老式C数据类型为char,int和long,以及他们的朋友unsigned char,unsigned int和unsigned long。 问题在于,在不同的平台和不同的编译器中,这些类型的大小可能不同。

因此,最好为我们的平台选择一个明确声明数据大小的数据类型。 如果您的平台具有stdint.h,则可以使用int8_t,uint8_t等。

整数的大小确定可以存储在其中的最大数目。 如果是无符号整数,则可以8位存储0到255之间的数字,如果转换为十六进制,则是0x00到0xFF。 由于0xFF具有“所有位已设置”,并且它对应于十进制255,因此完全合乎逻辑的是,单字节无符号整数的间隔为0-255。 间隔告诉我们,所有整数大小都将完全相同-数字与设置所有位时获得的数字相对应。

型式间隔时间十六进制
8位无符号0-2550x0-0xFF
8位带符号-128-1270x80-0x7F
16位无符号0-655350x0-0xFFFF
16位带符号-32768-327670x8000-0x7FFF
32位无符号0-42949672950x0-0xFFFFFFFFFF
32位带符号-2147483648-21474836470x80000000-0x7FFFFFFF

更加有趣的是,每种有符号数据类型中的-1是设置了所有位的数字(有符号字节为0xFF,有符号16位数字为0xFFFF,有符号32位数字为0xFFFFFFFF)。 如果数据被认为是无符号的,那么对于所有给定的位,将获得此类数据的最大可能数目。

为了模拟处理器寄存器,我们选择与该寄存器的大小相对应的数据类型。 可能值得选择默认情况下的未签名类型,并在需要将其视为已签名时进行转换。 例如,我们使用uint8_t数据类型表示一个8位寄存器。

提示:使用调试器转换数据类型


如果gdb安装在您的平台上,那么使用它来处理二进制数字将非常方便。 下面,我将显示一个示例-在下面的会话中,以#开头的行是我稍后添加的注释。

# /c, gdb
(gdb) print /c 0xFD
$1 = -3 '?'

# /x, gdb hex
# "p" "print"
(gdb) p /c 0xA
$2 = 10 '\n'

# 2 " "
(gdb) p /c 0xC5
$3 = -59 '?'
(gdb) p /c 0xC5+0x21
$4 = -26 '?'

# print , gdb
(gdb) p 0x21
$9 = 33

# , gdb,
# ,
(gdb) p 0xc5
$5 = 197 #
(gdb) p /c 0xc5
$3 = -59 '?' #
(gdb) p 0xfd
$6 = 253

# ( 32- )
(gdb) p /x -3
$7 = 0xfffffffd

# 1
(gdb) print (char) 0xff
$1 = -1 '?'
# 1
(gdb) print (unsigned char) 0xff
$2 = 255 '?'


当我使用十六进制数字时,我总是在gdb中执行它-几乎每天都会发生。 这比通过GUI打开程序员的计算器容易得多。 在Linux(和Mac OS X)计算机上,要启动gdb会话,只需打开终端并输入“ gdb”。 如果在OS X上使用Xcode,则在启动程序后,可以在Xcode(输出printf输出的控制台)中使用控制台。 在Windows上,Cygwin提供了gdb调试器。

CPU仿真器终止


收到所有这些信息后,您就可以准备长途旅行。您必须决定如何实现仿真器-创建完整的8080仿真或仅实现完成游戏所需的命令。

如果决定进行全面仿真,则将需要更多工具。我将在下一节中讨论它们。

另一种方法是仅模拟游戏使用的指令。我们将继续填写在“仿真器外壳程序”部分中创建的庞大的开关构造。我们将重复以下过程,直到只有一个未实现的命令:

  1. 使用ROM Space Invaders启动仿真器
  2. UnimplementedInstruction()如果命令尚未准备就绪,则呼叫将退出
  3. 模拟此指令
  4. 转到1

开始编写仿真器时,我要做的第一件事是从反汇编器中添加代码。因此,我能够输出应按以下方式执行的命令:

  int Emulate8080Op(State8080* state) { unsigned char *opcode = &state->memory[state->pc]; Disassemble8080Op(state->memory, state->pc); switch (*opcode) { case 0x00: //NOP /* ... */ } /*    */ printf("\tC=%d,P=%d,S=%d,Z=%d\n", state->cc.cy, state->cc.p, state->cc.s, state->cc.z); printf("\tA $%02x B $%02x C $%02x D $%02x E $%02x H $%02x L $%02x SP %04x\n", state->a, state->b, state->c, state->d, state->e, state->h, state->l, state->sp); } 

我还在末尾添加了代码以显示所有寄存器和状态标志。

好消息:为了深入研究5万个团队的程序,我们只需要8080个操作码的子集,我什至会列出需要实现的操作码列表:

操作码团队
0x00p
0x01LXI B,D16
0x05DCR B
0x06MVI B,D8
0x09爸爸b
0x0dDCR C
0x0eMVI C,D8
0x0fRrc
0x11LXI D,D16
0x13英寸
0x19爸爸
0x1aLDAX D
0x21LXI H,D16
0x23x
0x26MVI H,D8
0x29爸爸
0x31LXI SP,D16
0x32STA ADR
0x36MVI M,D8
0x3aLDA ADR
0x3eMVI A,D8
0x56MOV D,M
0x5eMOV E,M
0x66MOV H,M
0x6fMOV L,A
0x77MOV M,A
0x7aMOV A,D
0x7bMOV A,E
0x7cMOV A,H
0x7eMOV A,M
0xa7全日空
0xafXRA A
0xc1流行音乐b
0xc2詹兹·阿德
0xc3JMP ADR
0xc5推B
0xc6ADI D8
0xc9t
0xcd致电adr
0xd1流行音乐
0xd3D8输出
0xd5推D
0xe1流行音乐
0xe5推H
0xe6ANI D8
0xebch
0xf1POP PSW
0xf5推PSW
0xfbi
0xfeCPI D8

这些只是50条指令,其中10条是微不足道的实现。

侦错


但是我有一个坏消息。几乎可以肯定,您的仿真器将无法正常工作,并且很难找到此类代码中的错误。如果您知道哪个命令的行为不正确(例如,转换或对无意义代码的调用),则可以尝试通过检查代码来修复错误。

除了仔细检查代码外,还有另一种解决问题的方法-通过将仿真器与实际可用的仿真器进行比较。我们假设另一个模拟器始终可以正常工作,并且所有区别都是模拟器中的错误。例如,您可以使用我的模拟器。您可以手动并行运行它们。如果将我的代码集成到项目中以进行以下过程,则可以节省时间:

  1. 为模拟器创建状态
  2. 为我的国家创造状态
  3. 对于下一队
  4. 用状态调用模拟器
  5. 用我的财富来呼唤我
  6. 比较我们的两个状态
  7. 寻找任何差异中的错误
  8. 转到3

另一种方法是手动使用此站点这是一个8080 Javascript处理器仿真器,甚至包括ROM Space Invaders。过程如下:

  1. 通过单击“空间侵略者”按钮来重新启动“空间侵略者”仿真
  2. 按“运行1”按钮执行命令。
  3. 我们在模拟器中执行以下命令
  4. 比较处理器状态与您的状态
  5. 如果条件匹配,请转到2
  6. 如果条件不匹配,则您的指令仿真是错误的。更正它,然后从步骤1重新开始。

我在开始调试8080仿真器时就使用了这种方法,但我不会撒谎-这个过程可能很漫长。结果,我的很多问题都变成了错别字和复制粘贴错误,这些错误在检测到后很容易解决。

如果逐步执行代码,则前3万条指令中的大多数指令都将以$ 1a5f的周期执行。如果您在模拟器中查看javascript,则可以看到该代码将数据复制到了屏幕上。我确定经常会调用此代码。

第一次渲染屏幕后,经过5万条命令,程序陷入了这一无休止的循环:

  0ada LDA $20c0 0add ANA A 0ade JNZ $0ada 

它一直等到$ 20c0的内存值变为零。由于此循环中的代码未完全更改$ 20c0,因此它必须是来自其他位置的信号。现在该讨论模拟街机的“铁”了。

在继续进行下一部分之前,请确保您的CPU仿真器陷入这种无休止的循环。

供参考,请参阅我的资料

完整的8080仿真


, : , . . , . , .

, 8080 . 8080 cpudiag.asm, 8080.

:

  1. , . , cpudiag.asm .
  2. 如您所见,该过程非常艰苦。我认为,如果未列出这些步骤,则调试汇编代码的新手将遇到很大的困难。

这就是我在模拟器上使用此测试的方式。您可以使用它,或者想出一种更好的方法来集成它。

测试组装


我尝试了几件事,但最终我决定使用这个漂亮的页面我将文本cpudiag.asm粘贴到左窗格中,并且构建完成,没有任何问题。我花了一分钟的时间弄清楚如何下载结果,但是通过单击左下方的“ Make Beautiful Code”按钮,我下载了一个名为test.bin的文件,该文件的编译代码为8080。我能够使用反汇编程序进行验证。从我的网站上的镜像

下载cpudiag.asm从我的网站

下载cpudiag.bin(编译代码8080)。

将测试上传到我的模拟器


而不是加载入侵者*文件,而是加载此二进制文件。

这里出现一些小困难。首先,源汇编程序代码中有一行ORG 00100H,即,这意味着在假定第一行代码为0x100 hex的情况下编译整个文件。我以前从未在汇编器8080中编写过代码,所以我不知道此行的作用。我花了一分钟的时间才发现所有分支分支地址都不正确,并且内存必须从0x100开始。

其次,由于仿真器是从头开始的,因此我必须首先过渡到真实代码。将十六进制值插入到零地址的内存中后JMP $0100,我进行了处理。 (或者您可以将PC初始化为0x100。)

第三,我在编译后的代码中发现了一个错误。我认为原因是对最后一行代码的处理不正确STACK EQU TEMPP+256,但是我不确定。尽可能地,编译期间的堆栈位于$ 6ad,并且前几个PUSH开始重写代码。我建议该变量也应像代码其余部分一样偏移0x100,因此我通过在初始化堆栈指针的代码行中插入“ 0x7”来修复该变量。

最后,由于我没有在模拟器中实现DAA或辅助迁移,因此我修改了代码以跳过此检查(我们只是使用JMP跳过了此检查)。

  ReadFileIntoMemoryAt(state, "/Users/kpmiller/Desktop/invaders/cpudiag.bin", 0x100); //  ,   JMP 0x100 state->memory[0]=0xc3; state->memory[1]=0; state->memory[2]=0x01; //Fix the stack pointer from 0x6ad to 0x7ad // this 0x06 byte 112 in the code, which is // byte 112 + 0x100 = 368 in memory state->memory[368] = 0x7; //  DAA state->memory[0x59c] = 0xc3; //JMP state->memory[0x59d] = 0xc2; state->memory[0x59e] = 0x05; 

测试试图得出一个结论


显然,该测试依赖于CP / M OS的帮助。我发现CP / M在$ 0005处有一些代码可以将消息打印到控制台,并更改了CALL仿真以处理此行为。我不确定是否一切正常,但是对于程序尝试打印的两条消息,它是否起作用。我运行此测试的CALL仿真如下所示:

  case 0xcd: //CALL  #ifdef FOR_CPUDIAG if (5 == ((opcode[2] << 8) | opcode[1])) { if (state->c == 9) { uint16_t offset = (state->d<<8) | (state->e); char *str = &state->memory[offset+3]; // - while (*str != '$') printf("%c", *str++); printf("\n"); } else if (state->c == 2) { //    ,   ,    printf ("print char routine called\n"); } } else if (0 == ((opcode[2] << 8) | opcode[1])) { exit(0); } else #endif { uint16_t ret = state->pc+2; state->memory[state->sp-1] = (ret >> 8) & 0xff; state->memory[state->sp-2] = (ret & 0xff); state->sp = state->sp - 2; state->pc = (opcode[2] << 8) | opcode[1]; } break; 

通过此测试,我在模拟器中发现了几个问题。我不确定他们会参与其中的游戏,但是如果他们参与了,那么很难找到他们。

我继续执行所有的操作码(DAA和他的朋友除外)。我花了3-4个小时来解决挑战中的问题并实施新的挑战。绝对比我上面描述的手动过程要快-在我找到此测试之前,我花了4个小时以上的时间进行手动过程。如果您能弄清楚这个解释,那么我建议您使用这种方法,而不要手动进行比较。但是,了解手动过程也是一项很好的技能,如果您想模拟另一个处理器,则应该返回到它。

如果您无法执行此过程或看起来过于复杂,那么绝对值得选择上述方法,并在程序中运行两个不同的模拟器。当程序中出现数百万个命令并添加了中断时,将无法手动比较两个仿真器。

Source: https://habr.com/ru/post/zh-CN418699/


All Articles