第一部分在
这里 。
8080处理器反汇编程序
熟人
我们将需要有关操作码及其各自命令的信息。 在Internet上搜索信息时,您会注意到关于8080和Z80的信息很多。 Z80是8080的追随者-它以相同的十六进制代码执行所有8080指令,但还有其他指令。 我认为,尽管您应该避免有关Z80的信息,以免感到困惑。 我为我们的工作创建了一个操作码表,它在
这里 。
每个处理器都有制造商编写的参考指南。 通常,它被称为“程序员环境手册”之类的东西。 8080手册称为《英特尔8080微型计算机系统用户手册》。 它一直被称为“数据手册”,因此我也将其称为“数据手册”。 我可以从
http://www.datasheetarchive.com/下载8080参考。 该PDF是低质量的扫描,因此,如果您找到更好的版本,请使用它。
让我们开始吧,看看Space Invaders ROM。 (可在Internet上找到ROM文件。)我在Mac OS X上工作,因此我只使用hexdump命令查看其内容。 要进行进一步的工作,请为您的平台找到十六进制编辑器。 这是invaders.h文件的前128个字节:
$ hexdump -v invaders.h 0000000 00 00 00 c3 d4 18 00 00 f5 c5 d5 e5 c3 8c 00 00 0000010 f5 c5 d5 e5 3e 80 32 72 20 21 c0 20 35 cd cd 17 0000020 db 01 0f da 67 00 3a ea 20 a7 ca 42 00 3a eb 20 0000030 fe 99 ca 3e 00 c6 01 27 32 eb 20 cd 47 19 af 32 0000040 ea 20 3a e9 20 a7 ca 82 00 3a ef 20 a7 c2 6f 00 0000050 3a eb 20 a7 c2 5d 00 cd bf 0a c3 82 00 3a 93 20 0000060 a7 c2 82 00 c3 65 07 3e 01 32 ea 20 c3 3f 00 cd 0000070 40 17 3a 32 20 32 80 20 cd 00 01 cd 48 02 cd 13 ...
这是“太空侵略者”计划的开始。 每个十六进制数都是程序的命令或数据。 我们可以使用参考或其他参考信息来了解这些十六进制代码的含义。 让我们进一步探讨ROM映像代码。
该程序的第一个字节为$ 00。 查看表,我们看到它是NOP以及以下两个命令。 (但是请不要气,,“太空侵略者”可能会将这些命令用作延迟,以使系统在加电后能够稍微平静下来。)
第四个命令是$ C3,即从表判断,这是JMP。 JMP命令的定义指出它接收到一个两个字节的地址,即接下来的两个字节是JMP跳地址。 然后又有两个NOP出现了……那么,你知道吗? 让我自己签署前几条指示...
0000 00 NOP 0001 00 NOP 0002 00 NOP 0003 c3 d4 18 JMP $18d4 0006 00 NOP 0007 00 NOP 0008 f5 PUSH PSW 0009 c5 PUSH B 000a d5 PUSH D 000b e5 PUSH H 000c c3 8c 00 JMP $008c 000f 00 NOP 0010 f5 PUSH PSW 0011 c5 PUSH B 0012 d5 PUSH D 0013 e5 PUSH H 0014 3e 80 MVI A,#0x80 0016 32 72 20 STA $2072
似乎有某种方法可以自动执行此过程...
拆卸器,第1部分
反汇编程序是一个程序,它仅将十六进制数字流转换回使用汇编语言的源代码。 这正是我们在上一节中手工完成的任务-这是使这项工作自动化的绝佳机会。 编写这段代码,我们将熟悉处理器并获得一条方便的调试代码,这在编写CPU仿真器时非常有用。
这是8080代码反汇编算法:
- 将代码读入缓冲区
- 我们得到一个指向缓冲区开始的指针
- 使用指针中的字节确定操作码。
- 显示操作码的名称,必要时使用操作码后的字节作为数据
- 将指针移到该命令使用的字节数(1、2或3个字节)
- 如果缓冲区没有结束,请转到步骤3
为了奠定该过程的基础,我在下面添加了一些说明。 我将列出完整的下载过程,但建议您尝试自己编写。 不会花费很多时间,并且您将并行学习8080处理器的指令集。
int Disassemble8080Op(unsigned char *codebuffer, int pc) { unsigned char *code = &codebuffer[pc]; int opbytes = 1; printf ("%04x ", pc); switch (*code) { case 0x00: printf("NOP"); break; case 0x01: printf("LXI B,#$%02x%02x", code[2], code[1]); opbytes=3; break; case 0x02: printf("STAX B"); break; case 0x03: printf("INX B"); break; case 0x04: printf("INR B"); break; case 0x05: printf("DCR B"); break; case 0x06: printf("MVI B,#$%02x", code[1]); opbytes=2; break; case 0x07: printf("RLC"); break; case 0x08: printf("NOP"); break; case 0x3e: printf("MVI A,#0x%02x", code[1]); opbytes = 2; break; case 0xc3: printf("JMP $%02x%02x",code[2],code[1]); opbytes = 3; break; } printf("\n"); return opbytes; }
在编写此过程并研究每个操作码的过程中,我学到了很多有关8080处理器的知识。
- 我意识到大多数团队占用一个字节,其余两个或三个字节。 上面的代码假定命令的大小为1个字节,但是2字节和3字节的指令将更改“ opbytes”变量的值以返回正确的命令大小。
- 8080具有名称分别为A,B,C,D,E,H和L的寄存器。还有一个命令计数器(程序计数器,PC)和一个单独的堆栈指针(堆栈指针,SP)。
- 一些指令可成对使用寄存器:B和C是一对,DE和HL也是一对。
- A是一个特殊的寄存器,许多指令都可以使用它。
- HL还是一个特殊的寄存器,它用作每次将数据写入内存的地址。
- 我对“ RST”团队感到好奇,因此我读了一些指南。 我注意到它在固定的位置执行代码,而参考文献中提到了中断处理。 进一步阅读后发现,ROM开头的所有这些代码都是中断服务例程(ISR)。 中断可以使用RST命令以编程方式生成,也可以由第三方(而不是8080处理器)生成。
为了将所有这些变成一个工作程序,我刚刚编写了一个执行以下步骤的过程:
- 它打开一个文件,其中填充了编译后的代码8080
- 读取到内存缓冲区
- 穿过内存缓冲区,导致Disassemble8080Op
- 增加由Disassemble8080Op返回的PC
- 在缓冲区末尾退出
它可能看起来像这样:
int main (int argc, char**argv) { FILE *f= fopen(argv[1], "rb"); if (f==NULL) { printf("error: Couldn't open %s\n", argv[1]); exit(1); }
在第二部分中,我们将检查通过拆卸ROM Space Invaders获得的输出。
内存分配
在开始编写处理器仿真器之前,我们需要研究另一个方面。 所有CPU都有能力与一定数量的地址进行通信。 较旧的处理器具有16位,24位或32位地址。 8080有16个地址触点,因此地址范围是0- $ FFFF。
要了解游戏的内存分配,我们需要进行一次小型调查。 在
这里和
这里收集了信息之后,我发现ROM位于地址0,游戏具有8 KB RAM,起价为2000美元。
其中一页的作者发现,视频缓冲区从RAM开始,地址为$ 2,400,并且还告诉我们8080 I / O端口如何用于与控件和音频设备进行通信。 太好了!
在invaders.zip ROM文件(可以在Internet上找到)中,有四个文件:invaders.e,.f,.g和.h。 谷歌搜索之后,我遇到了
一篇内容丰富的
文章 ,告诉您如何将这些文件放入内存:
Space Invaders, (C) Taito 1978, Midway 1979
: Intel 8080, 2 ( Zilog Z80)
: $cf (RST 8) vblank, $d7 (RST $10) vblank.
: 256(x)*224(y), 60 , .
.
: 7168 , 1 (32 ).
: SN76477 .
:
ROM
$0000-$07ff: invaders.h
$0800-$0fff: invaders.g
$1000-$17ff: invaders.f
$1800-$1fff: invaders.e
RAM
$2000-$23ff:
$2400-$3fff:
$4000-:
仍有一些有用的信息,但我们尚未准备好使用它。
血腥细节
如果您想知道处理器具有的地址空间大小,则可以通过查看其特性来理解它。 规范8080告诉我们处理器具有16个地址触点,即它使用16位寻址。 (只需阅读手册,维基百科,谷歌等等,就可以了,而不是使用规范...)
在Internet上,有很多有关“太空侵略者”硬件的信息。 如果找不到此信息,则可以通过以下两种方式获取它:
- 观察在模拟器中运行的代码,并弄清楚它的作用。 做笔记并仔细观察。 例如,应该很简单地理解RAM在游戏中应该位于何处。 确定她正在寻找视频内存的位置也很容易(我们将花一些时间研究它)。
- 找到街机的电路图,并跟踪来自CPU地址触点的信号。 看看他们要去哪里。 例如,A15(最早的地址)只能进入ROM。 由此我们可以得出结论,ROM的地址从$ 8000开始。
通过观察代码执行来弄清楚自己,这可能是非常有趣和有益的。 有人不得不第一次处理所有这一切。
命令行开发
本教程的目的不是教您如何为特定平台编写代码,尽管我们将无法避免特定平台的代码。 我希望在项目开始之前,您已经知道如何针对目标平台进行编译。
当您使用独立代码(仅读取文件并在控制台中显示文本)时,不必使用某些过于复杂的开发系统。 实际上,它只会使事情复杂化。 您只需要一个文本编辑器和终端即可。
我认为任何想进行低级编程的人都应该知道如何从命令行创建简单的程序。 您可能会认为我在取笑您,但是如果您不能在Visual Studio之外运行,那么您的精英黑客技能就不值钱了。
在Mac上,可以使用TextEdit和Terminal进行编译。 在Linux上,您可以使用gedit和Konsole。 在Windows上,您可以安装cygwin和工具,然后使用N ++或其他文本编辑器。 如果您想变得很酷,那么所有这些平台都支持vi和emacs进行文本编辑。
使用命令行从单个文件编译程序是一项微不足道的任务。 假设您将程序保存在名为
8080dis.c
的文件中。 转到包含此文本文件的文件夹,并按如下所示进行编译:
cc 8080dis.c
。 如果未指定输出文件的名称,则将其称为
a.out
,可以通过键入
./a.out
来运行它。
实际上,仅此而已。
使用调试器
如果您使用的是基于Unix的系统之一,那么这里是使用GDB调试命令行程序的简要介绍。 您需要像这样编译程序:
cc -g -O0 8080dis.c
。
-g
参数生成调试信息(即,您可以基于源文本执行调试),-
-O0
参数禁用优化,以便在逐步执行程序时,调试器可以完全根据源文本准确地跟踪代码。
这是调试会话开始的带注释的日志。 我的评论在标有井号(#)的行中。
$ gdb a.out GNU gdb 6.3.50-20050815 (Apple version gdb-1708) (Mon Aug 8 20:32:45 UTC 2011) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries .. done # , (gdb) b Disassemble8080Op Breakpoint 1 at 0x1000012ef: file 8080dis.c, line 7. # "invaders.h" (gdb) run invaders.h Starting program: /Users/bob/Desktop/invaders/a.out invaders.h Reading symbols for shared libraries +........................ done Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=0) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; #gdb n "next". "next" (gdb) n 8 int opbytes = 1; #p - "print", *code (gdb) p *code $1 = 0 '\0' (gdb) n 9 printf("%04x ", pc); # "", gdb , "next" (gdb) 10 switch (*code) (gdb) n # , "NOP" 12 case 0x00: printf("NOP"); break; (gdb) n 285 printf("\n"); #c - "continue", (gdb) c Continuing. 0000 NOP # Disassemble8080Op. *opcode, # , NOP, . Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=1) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) c Continuing. 0001 NOP Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=2) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) n 8 int opbytes = 1; (gdb) p *code $2 = 0 '\0' # NOP, (gdb) c Continuing. 0002 NOP Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=3) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) n 8 int opbytes = 1; # ! (gdb) p *code $3 = 195 '?' # print , /x (gdb) p /x *code $4 = 0xc3 (gdb) n 9 printf("%04x ", pc); (gdb) 10 switch (*code) (gdb) # C3 - JMP. . 219 case 0xc3: printf("JMP $%02x%02x",code[2],code[1]); opbytes = 3; break; (gdb) 285 printf("\n");
拆卸器,第2部分
运行invaders.h ROM文件的反汇编程序,然后查看显示的信息。
0000 NOP 0001 NOP 0002 NOP 0003 JMP $18d4 0006 NOP 0007 NOP 0008 PUSH PSW 0009 PUSH B 000a PUSH D 000b PUSH H 000c JMP $008c 000f NOP 0010 PUSH PSW 0011 PUSH B 0012 PUSH D 0013 PUSH H 0014 MVI A,#$80 0016 STA $2072 0019 LXI H,#$20c0 001c DCR M 001d CALL $17cd 0020 IN #$01 0022 RRC 0023 JC $0067 0026 LDA $20ea 0029 ANA A 002a JZ $0042 002d LDA $20eb 0030 CPI #$99 0032 JZ $003e 0035 ADI #$01 0037 DAA 0038 STA $20eb 003b CALL $1947 003e SRA A 003f STA $20ea
第一条指令与我们之前手动记录的那些指令相对应。 在它们之后有几个新的说明。 我在下面插入了十六进制数据以供参考。 请注意,如果将内存与命令进行比较,则地址就像是以相反的顺序存储在内存中。 就是这样 这称为小尾数(little endian)-具有小尾数的计算机(例如8080)首先存储数字的最低有效字节。 (有关字节序的更多信息,如下所述。)
我在上面提到,此代码是“太空侵略者”游戏的ISR代码。 中断0、1、2,... 7的代码以地址$ 0,$ 8,$ 20,... $ 38开始。 看起来8080只是为每个ISR提供8个字节。 有时,太空侵略者程序会通过简单地移动到具有更多空间的另一个地址来绕过该系统。 (这发生在$ 000c)。
此外,ISR 2似乎比为其分配的内存更长。 她的代码为$ 0018(这是ISR 3的存放地)。 我认为太空侵略者不希望看到使用中断3的任何东西。
来自Internet的Space Invaders ROM文件包括四个部分。 我将在下面对此进行解释,但是现在,进入下一部分,我们需要将这四个文件合并为一个。 在Unix上:
cat invaders.h > invaders cat invaders.g >> invaders cat invaders.f >> invaders cat invaders.e >> invaders
现在,使用生成的“ invaders”文件运行反汇编程序。 当程序从$ 0000开始时,它要做的第一件事就是切换到$ 18d4。 我认为这是程序的开始。 让我们快速看一下这段代码。
18d4 LXI SP,#$2400 18d7 MVI B,#$00 18d9 CALL $01e6
因此,它执行两个操作并调用$ 01e6。 我将在过渡代码中插入部分代码:
01e6 LXI D,#$1b00 01e9 LXI H,#$2000 01ec JMP $1a32 ..... 1a32 LDAX D 1a33 MOV M,A 1a34 INX H 1a35 INX D 1a36 DCR B 1a37 JNZ $1a32 1a3a RET
从“太空侵略者”的内存分配中可以看到,其中一些地址很有趣。 $ 2000是“工作RAM”程序的开始。 $ 2,400是显存的开始。
让我们在代码中添加注释,以说明启动时直接执行的操作:
18d4 LXI SP,#$2400 ; SP=$2400 - 18d7 MVI B,#$00 ; B=0 18d9 CALL $01e6 ..... 01e6 LXI D,#$1b00 ; DE=$1B00 01e9 LXI H,#$2000 ; HL=$2000 01ec JMP $1a32 ..... 1a32 LDAX D ; A = (DE), , $1B00 1a33 MOV M,A ; A (HL), $2000 1a34 INX H ; HL = HL + 1 ( $2001) 1a35 INX D ; DE = DE + 1 ( $1B01) 1a36 DCR B ; B = B - 1 ( 0xff, 0) 1a37 JNZ $1a32 ; , , b=0 1a3a RET
看起来这段代码会将256个字节从$ 1b00复制到$ 2000。 怎么了 我不知道 您可以更详细地研究该程序并反思其功能。
这里有问题。 如果我们有一块任意的包含代码的内存,那么数据可能会与其交替。
例如,游戏角色的精灵可以与代码混合。 当反汇编程序陷入这种内存碎片时,它将认为这是代码并继续“咀嚼”它。 如果您不走运,那么在这段数据之后反汇编的任何代码可能都不正确。
虽然我们对此几乎无能为力。 请记住,存在这样的问题。 如果看到类似这样的内容:
- 从完全好的代码过渡到不在反汇编程序列表中的团队
- 无意义的代码流(例如POP B POP B POP B POP C XTHL XTHL XTHL)
在这里,可能是破坏了一些反汇编代码的数据。 如果发生这种情况,则需要从偏移处重新开始。
事实证明,太空侵略者会定期遇到零。 如果我们的拆卸程序停止了,则零将强制其执行重置。
可以在
这里找到有关“太空侵略者”代码的详细分析。
尾数
字节在不同的处理器模型中以不同的方式存储,并且存储取决于数据的大小。 大型字节序的计算机存储从旧到新的数据。 小尾数法使它们从最小到最大。 如果将32位整数0xAABBCCDD写入每台计算机的内存,则它将如下所示:
在小端:$ DD $ CC $ BB $ AA
大端:$ AA $ BB $ CC $ DD
我开始在使用big-endian的Motorola处理器上进行编程,因此在我看来更“自然”,但后来我也习惯了little-endian。
我的反汇编程序和仿真器完全避免了字节序问题,因为它们一次只能读取一个字节。 例如,如果要使用16位读取器从ROM读取地址,请注意,此代码在CPU体系结构之间不可移植。