第一部分和
第二部分 。
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) {
让我们实现一些操作码。
void Emulate8080Op(State8080* state) { unsigned char *opcode = &state->memory[state->pc]; switch(*opcode) { case 0x00: break;
你去。 对于每个操作码,我们都会更改状态和内存,就像在真实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:
我用16位数字模拟8位数学命令。 这样可以更轻松地跟踪计算产生进位的情况。
立即价值表格
立即值的形式几乎相同,除了命令后的字节是添加的源。 由于“操作码”是内存中当前命令的指针,因此操作码[1]将立即成为下一个字节。
case 0xC6:
形状记忆
在存储形式中,将添加一个字节,一对HL寄存器中存储的地址指示该字节。
case 0x86:
注意事项
其余的算术指令以类似的方式实现。 补充:
- 根据参考手册,在带有进位的不同版本(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:
通话和退回
调用后,CALL将指令的地址压入堆栈,然后跳转到目标地址。 RET从堆栈接收地址并将其保存在PC上。 所有状态都存在条件版本的CALL和RET。
- CZ,CNZ,RZ,RNZ为零
- CNC,CC,RNC,RC进行传输
- CPO,CPE,RPO,RPE用于奇偶校验
- CP,CM,RP,RM表示符号
case 0xcd:
注意事项
- PCHL命令无条件地跳转到一对HL寄存器中的地址。
- 我没有将先前讨论的RST包括在该组中。 它将返回地址写入堆栈,然后跳转到内存底部的预定义地址。
逻辑组
该小组执行逻辑操作(请参阅
本教程的
第一篇文章 )。 从本质上讲,它们类似于算术组,因为大多数操作都使用寄存器A(驱动器),并且大多数操作会影响标志。 所有操作均针对8位值执行,在该组中,没有命令影响寄存器对。
布尔运算
AND,OR,NOT(CMP)和“异或”(XOR)被称为布尔运算。 OR和AND我已经在前面进行了解释。 NOT命令(对于8080处理器,它称为CMA或补码累加器)仅更改位值-所有单位变为零,而零变为1。
我将XOR视为“差异识别器”。 她的真值表如下所示:
AND,OR和XOR具有寄存器,存储器和立即数的形式。 (CMP仅具有区分大小写的命令)。 这是一对操作码的实现:
case 0x2F:
循环移位命令
这些命令更改寄存器中位的顺序。 向右移动可将它们向右移动一位,向左移动-可向左移动一位:
(0b00010000) = 0b00001000
(0b00000001) = 0b00000010
他们似乎一文不值,但实际上并非如此。 它们可用于乘以除以二的幂。 以左移为例。
0b00000001
是十进制1,将其向左移动将使其变为
0b00000010
(即十进制2)。如果再向左移动,
0b00000100
得到
0b00000100
,即4。再向左移动,然后再乘以8。这将适用于任何按数字:5(
0b00000101
)左移时得到10(
0b00001010
)。 另一个左移为20(
0b00010100
)。 向右移动的方法相同,但用于除法。
8080没有乘法命令,但是可以使用这些命令来实现。 如果您知道该怎么做,您将获得奖励积分。 在面试中问了这样一个问题。 (我做了,尽管花了我几分钟。)
这些命令周期性地旋转驱动器,并且仅影响进位标志。 这是几个命令:
case 0x0f:
比较方式
CMP和CPI的任务仅是设置标志(用于分支)。 他们通过减去标志来做到这一点,但不存储结果。
- 同样:如果两个数字相等,则设置Z标志,因为它们彼此相减得出零。
- 大于:如果A大于要比较的值,则清除CY标志(因为可以进行减法而无需借用)。
- 较小:如果A小于比较值,则设置CY标志(因为A必须完成借位才能完成减法)。
这些命令有用于寄存器,存储器和立即数的版本。 该实现是一个简单的减法,但不保存结果:
case 0xfe:
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:
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-255 | 0x0-0xFF |
8位带符号 | -128-127 | 0x80-0x7F |
16位无符号 | 0-65535 | 0x0-0xFFFF |
16位带符号 | -32768-32767 | 0x8000-0x7FFF |
32位无符号 | 0-4294967295 | 0x0-0xFFFFFFFFFF |
32位带符号 | -2147483648-2147483647 | 0x80000000-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仿真或仅实现完成游戏所需的命令。如果决定进行全面仿真,则将需要更多工具。我将在下一节中讨论它们。另一种方法是仅模拟游戏使用的指令。我们将继续填写在“仿真器外壳程序”部分中创建的庞大的开关构造。我们将重复以下过程,直到只有一个未实现的命令:- 使用ROM Space Invaders启动仿真器
UnimplementedInstruction()
如果命令尚未准备就绪,则呼叫将退出- 模拟此指令
- 转到1
开始编写仿真器时,我要做的第一件事是从反汇编器中添加代码。因此,我能够输出应按以下方式执行的命令: int Emulate8080Op(State8080* state) { unsigned char *opcode = &state->memory[state->pc]; Disassemble8080Op(state->memory, state->pc); switch (*opcode) { case 0x00:
我还在末尾添加了代码以显示所有寄存器和状态标志。好消息:为了深入研究5万个团队的程序,我们只需要8080个操作码的子集,我什至会列出需要实现的操作码列表:操作码 | 团队 |
---|
0x00 | p |
0x01 | LXI B,D16 |
0x05 | DCR B |
0x06 | MVI B,D8 |
0x09 | 爸爸b |
0x0d | DCR C |
0x0e | MVI C,D8 |
0x0f | Rrc |
0x11 | LXI D,D16 |
0x13 | 英寸 |
0x19 | 爸爸 |
0x1a | LDAX D |
0x21 | LXI H,D16 |
0x23 | x |
0x26 | MVI H,D8 |
0x29 | 爸爸 |
0x31 | LXI SP,D16 |
0x32 | STA ADR |
0x36 | MVI M,D8 |
0x3a | LDA ADR |
0x3e | MVI A,D8 |
0x56 | MOV D,M |
0x5e | MOV E,M |
0x66 | MOV H,M |
0x6f | MOV L,A |
0x77 | MOV M,A |
0x7a | MOV A,D |
0x7b | MOV A,E |
0x7c | MOV A,H |
0x7e | MOV A,M |
0xa7 | 全日空 |
0xaf | XRA A |
0xc1 | 流行音乐b |
0xc2 | 詹兹·阿德 |
0xc3 | JMP ADR |
0xc5 | 推B |
0xc6 | ADI D8 |
0xc9 | t |
0xcd | 致电adr |
0xd1 | 流行音乐 |
0xd3 | D8输出 |
0xd5 | 推D |
0xe1 | 流行音乐 |
0xe5 | 推H |
0xe6 | ANI D8 |
0xeb | ch |
0xf1 | POP PSW |
0xf5 | 推PSW |
0xfb | i |
0xfe | CPI D8 |
这些只是50条指令,其中10条是微不足道的实现。侦错
但是我有一个坏消息。几乎可以肯定,您的仿真器将无法正常工作,并且很难找到此类代码中的错误。如果您知道哪个命令的行为不正确(例如,转换或对无意义代码的调用),则可以尝试通过检查代码来修复错误。除了仔细检查代码外,还有另一种解决问题的方法-通过将仿真器与实际可用的仿真器进行比较。我们假设另一个模拟器始终可以正常工作,并且所有区别都是模拟器中的错误。例如,您可以使用我的模拟器。您可以手动并行运行它们。如果将我的代码集成到项目中以进行以下过程,则可以节省时间:- 为模拟器创建状态
- 为我的国家创造状态
- 对于下一队
- 用状态调用模拟器
- 用我的财富来呼唤我
- 比较我们的两个状态
- 寻找任何差异中的错误
- 转到3
另一种方法是手动使用此站点。这是一个8080 Javascript处理器仿真器,甚至包括ROM Space Invaders。过程如下:- 通过单击“空间侵略者”按钮来重新启动“空间侵略者”仿真
- 按“运行1”按钮执行命令。
- 我们在模拟器中执行以下命令
- 比较处理器状态与您的状态
- 如果条件匹配,请转到2
- 如果条件不匹配,则您的指令仿真是错误的。更正它,然后从步骤1重新开始。
我在开始调试8080仿真器时就使用了这种方法,但我不会撒谎-这个过程可能很漫长。结果,我的很多问题都变成了错别字和复制粘贴错误,这些错误在检测到后很容易解决。如果逐步执行代码,则前3万条指令中的大多数指令都将以$ 1a5f的周期执行。如果您在模拟器中查看javascript,则可以看到该代码将数据复制到了屏幕上。我确定经常会调用此代码。第一次渲染屏幕后,经过5万条命令,程序陷入了这一无休止的循环: 0ada LDA $20c0 0add ANA A 0ade JNZ $0ada
它一直等到$ 20c0的内存值变为零。由于此循环中的代码未完全更改$ 20c0,因此它必须是来自其他位置的信号。现在该讨论模拟街机的“铁”了。在继续进行下一部分之前,请确保您的CPU仿真器陷入这种无休止的循环。供参考,请参阅我的资料。完整的8080仿真
, : , . . , . , .
, 8080 .
8080 cpudiag.asm, 8080.
:
- , . , cpudiag.asm .
- 如您所见,该过程非常艰苦。我认为,如果未列出这些步骤,则调试汇编代码的新手将遇到很大的困难。
这就是我在模拟器上使用此测试的方式。您可以使用它,或者想出一种更好的方法来集成它。测试组装
我尝试了几件事,但最终我决定使用这个漂亮的页面。我将文本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);
测试试图得出一个结论
显然,该测试依赖于CP / M OS的帮助。我发现CP / M在$ 0005处有一些代码可以将消息打印到控制台,并更改了CALL仿真以处理此行为。我不确定是否一切正常,但是对于程序尝试打印的两条消息,它是否起作用。我运行此测试的CALL仿真如下所示: case 0xcd:
通过此测试,我在模拟器中发现了几个问题。我不确定他们会参与其中的游戏,但是如果他们参与了,那么很难找到他们。我继续执行所有的操作码(DAA和他的朋友除外)。我花了3-4个小时来解决挑战中的问题并实施新的挑战。绝对比我上面描述的手动过程要快-在我找到此测试之前,我花了4个小时以上的时间进行手动过程。如果您能弄清楚这个解释,那么我建议您使用这种方法,而不要手动进行比较。但是,了解手动过程也是一项很好的技能,如果您想模拟另一个处理器,则应该返回到它。如果您无法执行此过程或看起来过于复杂,那么绝对值得选择上述方法,并在程序中运行两个不同的模拟器。当程序中出现数百万个命令并添加了中断时,将无法手动比较两个仿真器。