Der erste Teil ist
hier .
8080 Prozessor Disassembler
Bekanntschaft
Wir benötigen Informationen zu den Opcodes und ihren jeweiligen Befehlen. Wenn Sie im Internet nach Informationen suchen, werden Sie feststellen, dass es viele gemischte Informationen zu 8080 und Z80 gibt. Der Z80 war ein Anhänger des 8080 - er führt alle 8080-Anweisungen mit denselben Hex-Codes aus, verfügt jedoch auch über zusätzliche Anweisungen. Ich denke, während Sie Informationen über das Z80 vermeiden sollten, um nicht verwirrt zu werden. Ich habe eine Opcode-Tabelle für unsere Arbeit erstellt, sie ist
hier .
Jeder Prozessor verfügt über ein vom Hersteller geschriebenes Referenzhandbuch. Normalerweise wird es so etwas wie "Programmer's Environment Manual" genannt. Das 8080-Handbuch wird als Benutzerhandbuch für Intel 8080-Mikrocomputersysteme bezeichnet. Es wurde immer als "Datenbuch" bezeichnet, daher werde ich es auch so nennen. Ich konnte die 8080-Referenz von
http://www.datasheetarchive.com/ herunterladen. Bei diesem PDF handelt es sich um einen Scan von geringer Qualität. Wenn Sie also eine bessere Version finden, verwenden Sie sie.
Lassen Sie uns loslegen und einen Blick auf das Space Invaders ROM werfen. (Die ROM-Datei befindet sich im Internet.) Ich arbeite unter Mac OS X, daher verwende ich nur den Befehl hexdump, um den Inhalt anzuzeigen. Suchen Sie für weitere Arbeiten den Hex-Editor für Ihre Plattform. Hier sind die ersten 128 Bytes der Datei invaders.h:
$ 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 ...
Dies ist der Start des Space Invaders-Programms. Jede Hexadezimalzahl ist ein Befehl oder Daten für das Programm. Wir können eine Referenz oder andere Referenzinformationen verwenden, um zu verstehen, was diese Hex-Codes bedeuten. Lassen Sie uns den ROM-Image-Code etwas genauer untersuchen.
Das erste Byte dieses Programms ist $ 00. Wenn wir uns die Tabelle ansehen, sehen wir, dass es sich um NOP handelt, sowie die folgenden zwei Befehle. (Aber lassen Sie sich nicht entmutigen, Space Invaders haben diese Befehle wahrscheinlich als Verzögerung verwendet, um das System nach dem Einschalten etwas beruhigen zu lassen.)
Der vierte Befehl ist $ C3, dh nach der Tabelle ist dies JMP. Die Definition eines JMP-Befehls besagt, dass er eine Zwei-Byte-Adresse empfängt, dh die nächsten zwei Bytes sind die JMP-Hop-Adresse. Dann kommen zwei weitere NOPs ... weißt du was? Lassen Sie mich nur die ersten paar Anweisungen selbst unterschreiben ...
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
Es scheint eine Möglichkeit zu geben, diesen Prozess zu automatisieren ...
Disassembler, Teil 1
Ein Disassembler ist ein Programm, das einfach einen Strom von Hex-Zahlen in Assemblersprache zurück in den Quellcode übersetzt. Dies ist genau die Aufgabe, die wir im vorherigen Abschnitt von Hand ausgeführt haben - eine großartige Gelegenheit, diese Arbeit zu automatisieren. Wenn wir diesen Code schreiben, lernen wir den Prozessor kennen und erhalten einen praktischen Debugging-Code, der beim Schreiben eines CPU-Emulators hilfreich ist.
Hier ist der 8080-Code-Zerlegungsalgorithmus:
- Lesen Sie den Code in den Puffer
- Wir erhalten einen Zeiger auf den Anfang des Puffers
- Verwenden Sie das Byte im Zeiger, um den Opcode zu bestimmen.
- Zeigen Sie den Namen des Opcodes an, ggf. mit Bytes nach dem Opcode als Daten
- Bewegen Sie den Zeiger auf die Anzahl der von diesem Befehl verwendeten Bytes (1, 2 oder 3 Bytes).
- Wenn der Puffer nicht endet, fahren Sie mit Schritt 3 fort
Um den Grundstein für das Verfahren zu legen, habe ich nachfolgend einige Anweisungen hinzugefügt. Ich werde das vollständige Verfahren zum Herunterladen festlegen, aber ich empfehle Ihnen, es selbst zu schreiben. Es wird nicht viel Zeit in Anspruch nehmen und parallel dazu lernen Sie den Befehlssatz des 8080-Prozessors.
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; }
Während ich dieses Verfahren schrieb und jeden Opcode studierte, lernte ich viel über den 8080-Prozessor.
- Mir wurde klar, dass die meisten Teams ein Byte benötigen, der Rest zwei oder drei. Der obige Code setzt voraus, dass der Befehl eine Byte groß ist, aber die Zwei- und Drei-Byte-Anweisungen ändern den Wert der Variablen "opbytes", um die korrekte Größe des Befehls zurückzugeben.
- Der 8080 hat Register mit den Namen A, B, C, D, E, H und L. Es gibt auch einen Programmzähler (Programmzähler, PC) und einen separaten Stapelzeiger (Stapelzeiger, SP).
- Einige Anweisungen arbeiten paarweise mit Registern: B und C sind ein Paar sowie DE und HL.
- A ist ein spezielles Register, mit dem viele Anweisungen arbeiten.
- HL ist auch ein spezielles Register, es wird als Adresse für jedes Lesen und Schreiben von Daten in den Speicher verwendet.
- Ich wurde neugierig auf das „RST“ -Team und las den Leitfaden ein wenig. Ich habe festgestellt, dass der Code an festen Stellen ausgeführt wird und in der Referenz die Interrupt-Behandlung erwähnt wird. Beim weiteren Lesen stellte sich heraus, dass der gesamte Code am Anfang des ROM Interrupt Service Routines (ISRs) waren. Interrupts können programmgesteuert mit dem Befehl RST oder von Quellen von Drittanbietern (nicht vom 8080-Prozessor) generiert werden.
Um all dies in ein Arbeitsprogramm umzuwandeln, habe ich mir gerade eine Prozedur ausgedacht, die die folgenden Schritte ausführt:
- Es öffnet eine Datei mit kompiliertem Code 8080
- Liest es in den Speicherpuffer
- Durchläuft den Speicherpuffer und verursacht Disassemble8080Op
- Erhöht den von Disassemble8080Op zurückgegebenen PC
- Wird am Ende des Puffers beendet
Es könnte ungefähr so aussehen:
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); }
Im zweiten Teil werden wir die Ausgabe untersuchen, die durch Zerlegen der ROM Space Invaders erhalten wird.
Speicherzuordnung
Bevor wir mit dem Schreiben eines Prozessoremulators beginnen, müssen wir einen anderen Aspekt untersuchen. Alle CPUs können mit einer bestimmten Anzahl von Adressen kommunizieren. Ältere Prozessoren hatten 16-, 24- oder 32-Bit-Adressen. Der 8080 verfügt über 16 Adresskontakte, sodass die Adressen im Bereich von 0- $ FFFF liegen.
Um die Speicherzuordnung des Spiels zu verstehen, müssen wir eine kleine Untersuchung durchführen. Nachdem ich die Informationen
hier und
hier gesammelt hatte , stellte ich fest, dass sich das ROM unter der Adresse 0 befindet und das Spiel 8 KB RAM ab 2000 US-Dollar hat.
Der Autor einer der Seiten fand heraus, dass der Videopuffer im RAM mit einer Adresse von 2.400 US-Dollar startet, und erklärte uns auch, wie die 8080-E / A-Ports für die Kommunikation mit Steuerelementen und Audiogeräten verwendet werden. Großartig!
In der ROM-Datei invaders.zip, die im Internet zu finden ist, befinden sich vier Dateien: invaders.e, .f, .g und .h. Nach dem Googeln stieß ich auf einen informativen
Artikel , in dem erklärt wird, wie diese Dateien gespeichert werden:
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-:
Es gibt noch einige nützliche Informationen, aber wir sind noch nicht bereit, sie zu verwenden.
Blutige Details
Wenn Sie wissen möchten, wie groß der Adressraum des Prozessors ist, können Sie dies anhand seiner Eigenschaften verstehen. Die Spezifikation 8080 sagt uns, dass der Prozessor 16 Adresskontakte hat, dh er verwendet eine 16-Bit-Adressierung. (Anstelle von Spezifikationen reicht es aus, das Handbuch, Wikipedia, Google usw. zu lesen ...)
Im Internet gibt es eine Menge Informationen über die Hardware von Space Invaders. Wenn Sie diese Informationen nicht finden konnten, können Sie sie auf verschiedene Arten abrufen:
- Beobachten Sie den Code, der im Emulator ausgeführt wird, und finden Sie heraus, was er tut. Machen Sie sich Notizen und beobachten Sie genau. Es sollte einfach genug sein, um beispielsweise zu verstehen, wo sich nach Meinung des Spiels RAM befinden sollte. Es ist auch einfach zu bestimmen, wo sie nach Videospeicher sucht (wir werden einige Zeit damit verbringen, dies zu studieren).
- Suchen Sie den Schaltplan des Arcade-Automaten und verfolgen Sie die Signale von den Adresskontakten der CPU. Sehen Sie, wohin sie gehen. Zum Beispiel kann A15 (älteste Adresse) nur zum ROM gehen. Daraus können wir schließen, dass die Adressen des ROM bei 8000 US-Dollar beginnen.
Es kann sehr interessant und informativ sein, es selbst herauszufinden, indem Sie die Codeausführung beobachten. Jemand musste sich zum ersten Mal mit all dem befassen.
Befehlszeilenentwicklung
Das Ziel dieses Tutorials ist es nicht, Ihnen das Schreiben von Code für eine bestimmte Plattform beizubringen, obwohl wir plattformspezifischen Code nicht vermeiden können. Ich hoffe, dass Sie bereits vor Projektbeginn wussten, wie Sie für Ihre Zielplattform kompilieren können.
Wenn Sie mit eigenständigem Code arbeiten, der einfach Dateien liest und Text in der Konsole anzeigt, ist es nicht erforderlich, ein überkompliziertes Entwicklungssystem zu verwenden. Tatsächlich macht es die Dinge nur komplizierter. Sie benötigen lediglich einen Texteditor und ein Terminal.
Ich denke, dass jeder, der auf einer niedrigen Ebene programmieren möchte, wissen sollte, wie man einfache Programme über die Befehlszeile erstellt. Sie mögen denken, dass ich Sie ärgere, aber Ihre Elite-Hacker-Fähigkeiten sind nicht viel wert, wenn Sie nicht außerhalb von Visual Studio funktionieren können.
Auf einem Mac können Sie TextEdit und Terminal zum Kompilieren verwenden. Unter Linux können Sie gedit und Konsole verwenden. Unter Windows können Sie Cygwin und Tools installieren und dann N ++ oder einen anderen Texteditor verwenden. Wenn Sie wirklich cool sein möchten, unterstützen alle diese Plattformen vi und emacs für die Textbearbeitung.
Das Kompilieren von Programmen aus einer einzelnen Datei über die Befehlszeile ist eine triviale Aufgabe. Angenommen, Sie haben Ihr Programm in einer Datei namens
8080dis.c
. Gehen Sie zu dem Ordner mit dieser Textdatei und kompilieren Sie ihn wie
cc 8080dis.c
:
cc 8080dis.c
. Wenn Sie den Namen der Ausgabedatei nicht angeben, wird sie
a.out
und Sie können sie ausführen, indem Sie
./a.out
.
Das ist in der Tat alles.
Verwenden eines Debuggers
Wenn Sie auf einem der Unix-basierten Systeme arbeiten, finden Sie hier eine kurze Einführung in das Debuggen von Befehlszeilenprogrammen mit GDB. Sie müssen das Programm wie
cc -g -O0 8080dis.c
:
cc -g -O0 8080dis.c
. Der Parameter
-g
generiert Debugging-Informationen (
-O0
Sie können das Debugging basierend auf dem Quelltext durchführen), und der Parameter
-O0
deaktiviert Optimierungen, sodass der Debugger beim Durchlaufen des Programms den Code genau gemäß dem Quelltext genau verfolgen kann.
Hier ist das kommentierte Protokoll des Starts einer Debugging-Sitzung. Meine Kommentare sind in Zeilen mit einem Nummernzeichen (#) gekennzeichnet.
$ 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");
Disassembler, Teil 2
Führen Sie den Disassembler für die ROM-Datei invaders.h aus und überprüfen Sie die angezeigten Informationen.
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
Die ersten Anweisungen entsprechen denen, die wir zuvor manuell notiert haben. Nach ihnen gibt es mehrere neue Anweisungen. Unten habe ich Hex-Daten als Referenz eingefügt. Beachten Sie, dass beim Vergleich des Speichers mit den Befehlen die Adressen so sind, als wären sie in umgekehrter Reihenfolge im Speicher gespeichert. So ist es. Dies wird als Little Endian bezeichnet - Maschinen mit Little Endian wie der 8080 speichern zuerst die niedrigstwertigen Zahlenbytes. (Mehr über Endian wird unten beschrieben.)
Ich habe oben erwähnt, dass dieser Code der ISR-Code für das Space Invaders-Spiel ist. Der Code für die Interrupts 0, 1, 2, ... 7 beginnt mit der Adresse $ 0, $ 8, $ 20, ... $ 38. Es scheint, dass der 8080 nur 8 Bytes für jeden ISR liefert. Manchmal umgeht das Space Invaders-Programm dieses System, indem es einfach zu einer anderen Adresse mit mehr Speicherplatz wechselt. (Dies geschieht bei $ 000c).
Außerdem scheint ISR 2 länger zu sein als der dafür zugewiesene Speicher. Ihr Code geht an $ 0018 (dies ist der Ort für ISR 3). Ich denke, dass Space Invaders nichts erwarten, was Interrupt 3 verwendet.
Die Space Invaders ROM-Datei aus dem Internet besteht aus vier Teilen. Ich werde dies weiter unten erläutern, aber um mit dem nächsten Abschnitt fortzufahren, müssen wir diese vier Dateien zu einer zusammenführen. Unter Unix:
cat invaders.h > invaders cat invaders.g >> invaders cat invaders.f >> invaders cat invaders.e >> invaders
Führen Sie nun den Disassembler mit der resultierenden "Invaders" -Datei aus. Wenn ein Programm bei $ 0000 startet, wechselt es zuerst zu $ 18d4. Ich werde dies als den Beginn des Programms betrachten. Werfen wir einen kurzen Blick auf diesen Code.
18d4 LXI SP,#$2400 18d7 MVI B,#$00 18d9 CALL $01e6
Es führt also zwei Operationen aus und ruft $ 01e6 auf. Ich werde einen Teil des Codes mit Übergängen in diesen Code einfügen:
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
Wie wir aus der Speicherzuordnung der Space Invaders gesehen haben, sind einige dieser Adressen interessant. $ 2000 ist der Beginn eines "funktionierenden RAM" -Programms. 2.400 US-Dollar sind der Beginn des Videospeichers.
Fügen wir dem Code Kommentare hinzu, um zu erklären, was er direkt beim Start tut:
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
Es sieht so aus, als würde dieser Code 256 Bytes von $ 1b00 auf $ 2000 kopieren. Warum? Ich weiß nicht. Sie können das Programm genauer studieren und darüber nachdenken, was es tut.
Hier gibt es ein Problem. Wenn wir einen beliebigen Speicher haben, der Code enthält, wechseln sich die Daten wahrscheinlich ab.
Beispielsweise können Sprites für Spielcharaktere mit Code gemischt werden. Wenn ein Disassembler in ein solches Speicherfragment fällt, wird er denken, dass dies Code ist, und ihn weiterhin „kauen“. Wenn Sie Pech haben, ist möglicherweise der nach diesem Datenelement zerlegte Code falsch.
Wir können zwar kaum etwas dagegen tun. Denken Sie daran, dass ein solches Problem besteht. Wenn Sie so etwas sehen:
- Übergang von genau gutem Code zu einem Team, das nicht in der Disassembler-Liste enthalten ist
- bedeutungsloser Codestream (z. B. POP B POP B POP B POP C XTHL XTHL XTHL)
Hier gibt es wahrscheinlich Daten, die einen Teil des zerlegten Codes ruiniert haben. In diesem Fall müssen Sie erneut mit dem Offset beginnen.
Es stellt sich heraus, dass Space Invaders regelmäßig auf Nullen stoßen. Wenn unsere Demontage jemals stoppt, zwingen die Nullen sie, einen Reset durchzuführen.
Eine detaillierte Analyse des Space Invaders-Codes finden Sie
hier .
Endian
Bytes werden in verschiedenen Prozessormodellen unterschiedlich gespeichert, und die Speicherung hängt von der Größe der Daten ab. Big-Endian-Maschinen speichern Daten von älter bis jünger. Little-Endian halten sie vom jüngsten bis zum ältesten. Wenn eine 32-Bit-Ganzzahl 0xAABBCCDD in den Speicher jedes Computers geschrieben wird, sieht dies folgendermaßen aus:
In Little-Endian: $ DD $ CC $ BB $ AA
Big-Endian: $ AA $ BB $ CC $ DD
Ich habe angefangen, auf Motorola-Prozessoren zu programmieren, die Big-Endian verwenden, daher schien es mir „natürlicher“ zu sein, aber dann habe ich mich auch an Little-Endian gewöhnt.
Mein Disassembler und Emulator vermeiden das Endian-Problem vollständig, da sie jeweils nur ein Byte lesen. Wenn Sie beispielsweise einen 16-Bit-Leser zum Lesen der Adresse aus dem ROM verwenden möchten, beachten Sie, dass dieser Code nicht zwischen CPU-Architekturen portierbar ist.