Disassembler

Z80 Disassembler

Vor einigen Tagen habe ich meinen tzxtools einen Z80-Disassembler hinzugefügt. Da ich keinen für Python finden konnte, beschloss ich, meinen eigenen zu schreiben. Das Ergebnis passt in eine einzige Python-Quelldatei. Dieser Artikel ist das Making-of…

Der Zilog Z80 ist ein 8-Bit-Prozessor. Das bedeutet, dass (fast) alle Befehle nur 1 Byte verbrauchen. Zum Beispiel hat der Befehl ret (Rücksprung aus dem Unterprogramm) C9 als Byte-Darstellung. Einigen Befehlen folgt ein weiteres Byte (als zu verwendende Konstante oder als relativer Sprungabstand) oder zwei weitere Bytes (als 16-Bit-Konstante oder absolute Adresse). Einige Beispiele:

C9------retRücksprung aus dem Unterprogramm
3E23----ld a,$23Lade Konstante $23 in das A-Register
C33412--jp $1234Springe zu Adresse $1234

Beachte, dass bei 16-Bit-Konstanten die Bytes im Speicher scheinbar vertauscht sind. Das liegt daran, dass der Z80 eine sogenannte Little-Endian-CPU ist, bei der das niederwertige Byte zuerst kommt. Einige andere Prozessorfamilien (wie der 68000) sind Big-Endian und speichern das höherwertige Wort zuerst.

Es gibt also nur 256 Befehle, was es ziemlich einfach macht, sie zu disassemblieren. Ich habe ein Array mit 256 Einträgen verwendet, wobei jeder Eintrag den Befehl des jeweiligen Bytes als String enthält. Für Konstanten habe ich Platzhalter wie “##” oder “$” verwendet. Wenn ein solcher Platzhalter nach der Dekodierung im Befehlsstring gefunden wird, wird die entsprechende Anzahl von Bytes abgerufen und der Platzhalter durch den gefundenen Wert ersetzt.

Wenn wir einen Disassembler für die 8080-CPU schreiben würden, wären wir jetzt fertig. Allerdings hat der Z80 einige Erweiterungen, die abgedeckt werden müssen, nämlich zwei erweiterte Befehlssätze und zwei Indexregister.

Ein Satz erweiterter Befehle wird durch ein $ED-Präfix ausgewählt und enthält selten verwendete Befehle. Der andere Befehlssatz wird durch ein $CB-Präfix ausgewählt und verfügt über Bit-Manipulationen und einige Rotationsbefehle.

EDB0----ldirKopiere BC Bytes von HL nach DE
ED4B7856ld bc,($5678)Lädt den Wert von Adresse $5678 in das BC-Registerpaar
CBC7----set 0,aSetze Bit 0 im A-Register

Für das $ED-Präfix habe ich ein separates Array zur Dekodierung der Befehle verwendet. Die $CB-Befehle folgen einem einfachen Bit-Schema, sodass die Befehle durch ein paar Zeilen Python-Code dekodiert werden konnten.

Der Z80 bietet zwei Indexregister namens IX und IY. Sie werden verwendet, wenn dem Befehl ein $DD- bzw. $FD-Byte vorangestellt ist. Diese Präfixe verwenden im Grunde das ausgewählte Indexregister anstelle des HL-Registerpaars für den aktuellen Befehl. Wenn jedoch der Adressierungsmodus (HL) verwendet wird, wird ein zusätzlicher Byte-großer Offset bereitgestellt. Die Indexregister können mit dem $CB-Präfix kombiniert werden, was die Dinge kompliziert machen kann.

E5------push hlLege HL auf den Stack
DDE5----push ixLege IX auf den Stack (gleicher Opcode E5, aber jetzt mit DD-Präfix)
FDE5----push iyLege IY auf den Stack (jetzt mit FD-Präfix)
FD2180FFld iy,$FF80Lade Konstante $FF80 in das IY-Register
DD7E09--ld a,(ix+9)Lade Wert an Adresse IX+9 in das A-Register (Offset ist nach dem Opcode)
CBC6----set 0,(hl)Setze Bit 0 an der Adresse in HL
FDCB03C6set 0,(iy+3)Setze Bit 0 an der Adresse IY+3 (Offset ist vor dem Opcode)

Wenn der Disassembler ein $DD- oder $FD-Präfix erkennt, setzt er ein entsprechendes ix- oder iy-Flag. Später, wenn der Befehl dekodiert wird, wird jedes Vorkommen von HL entweder durch IX oder IY ersetzt. Wenn (HL) gefunden wurde, wird ein weiteres Byte aus dem Bytestream geholt und als Index-Offset für (IX+dd) oder (IY+dd) verwendet.

Es gibt eine Ausnahme. Die obigen Beispiele zeigen, dass der Index-Offset immer beim dritten Byte zu finden ist. Das bedeutet, dass, wenn das Indexregister mit einem $CB-Präfix kombiniert wird, der eigentliche Befehl nach dem Index steht. Dies ist ein Fall, der in meinem Disassembler eine Sonderbehandlung benötigte. Wenn diese Kombination erkannt wird, wird der Index-Offset abgerufen und gespeichert, bevor der Befehl dekodiert wird.

Puh, das war kompliziert. Jetzt sind wir in der Lage, den offiziellen Befehlssatz der Z80-CPU zu disassemblieren. Aber wir sind noch nicht fertig. Es gibt eine Reihe von undokumentierten Befehlen. Der Hersteller Zilog hat sie nie dokumentiert, sie sind nicht sonderlich nützlich, aber sie funktionieren trotzdem auf fast jeder Z80-CPU und werden tatsächlich verwendet.

Die meisten von ihnen werden einfach durch die Erweiterung der Befehls-Arrays abgedeckt. Zusätzlich wirken sich die $DD- oder $FD-Präfixe nicht nur auf das HL-Registerpaar aus, sondern auch nur auf die H- und L-Register, was IXH/IYH- und IXL/IYL-Register ergibt. Dies wird durch die Nachbearbeitung der Befehle abgedeckt. Ein ganz besonderer Fall ist das $CB-Präfix in Kombination mit Indexregistern, was eine ganze Reihe neuer Befehle ergibt, die das Ergebnis einer Bit-Operation in einem anderen Register speichern. Dies erforderte tatsächlich eine Sonderbehandlung durch einen separaten $CB-Präfix-Befehlsdekodierer.

Schließlich wird der ZX Spectrum Next einige neue Befehle wie Multiplikation oder Hardware-bezogene Dinge für den ZX Spectrum bringen. Auch diese wurden durch die Erweiterung der Befehls-Arrays abgedeckt. Die einzigen Ausnahmen sind der Befehl push [const], bei dem die Konstante als Big-Endian gespeichert wird, und der Befehl nextreg [reg],[val], dem (als einzigem Befehl) zwei Konstanten folgen.

Und das war’s. 😄 So schreibt man an einem einzigen Nachmittag einen Z80-Disassembler.