Forum: Mikrocontroller und Digitale Elektronik Z80 Opcodes parsen


von Tobias B. (tobiasz80)


Lesenswert?

Hallo Leute,

ich bin zur Zeit dran einen z80 Emulator/Disassembler zu schreiben. Über 
den Sinn und Unsinn möchte ich mich hier an der Stelle nicht 
unterhalten. Ich weiß, das es zuhauf alternativen gibt, aber das soll 
hier nicht weiter von belang sein. Es ist ein reines Spassprojekt. 
Jedoch stoße ich dabei an Probleme, die auch in der Gegenwart nicht 
unerheblich sein werden. Es geht dabei, den Opcode zu parsen, und das 
ist schwieriger, als zunächst anzunehmen ist.
Die Grundlagen, wie Parsing funktioniert und auch von Sprachen sind mir 
bekannt. Aber der Opcode lässt sich damit nicht wirklich gut parsen. Um 
einfach Beispiele zu nennen. Die 8Bit Register (8 Stück an der Zahl) 
werden durchnummeriert. Das heißt, es werden 3 Bit benötigt, womit 
wiederum 9 Kombinationen möglich sind. Die 9. Kombination wird natürlich 
auch verwendet, was die Arbeit mit Bitmasken erschwert. Um konkret zu 
werden. Opcodes können die Form
0 1 r r r r' r' r'
0 1 r r r 1 1 0
0 1 1 1 0 r r r
0 1 1 1 0 1 1 0
besitzen, wobei 1 1 0 die 9. Kombination ist, die nicht für die 
Identifikation von Registern benutzt wird.
Diesen kann man noch halbwegs mit Bitmasken parsen, aber es fängt schon 
dadurch an, schmutzig zu werden. Aber dann gibt es auch noch andere 
Opcodes in der Form
0 0 r r r x x x
0 0 q q x x x x
0 0 x x x x x x
1 0 x x x r r r
1 0 x x x x x x
Da fang ich mir nun schon richtig einen an, abzubrechen. Ich hab keine 
Idee, wie ich das intelligent parsen kann, ohne das der Code vollkommen 
dreckig wird. Ich kann nicht wirklich mit Bitmasken arbeiten, da
1. Ich die drei Bit, wo die Register liegen, nicht einfach mit einer 
Maske ausblenden kann, da es eben eine Kombination gibt, die kein 
Register identifiziert (1 1 0)
2. Ich kann bei manchen keine Maske verwenden, da manchmal der variable 
Anteil in den Opcodes mal zwei und mal drei Bits sind
3. Der variable Anteil ist mal in der Mitte und mal am Ende
Egal was ich mache. Es wird einfach schmutzig. Ich kann nicht immer nach 
dem gleichen Schema vorgehen, sondern muss mir immer Sonderfälle 
überlegen. Das verlangsamt zudem auch die Geschwindigkeit von dem 
Parser.
Ich habe mir natürlich auch den Sourcecode von den anderen Projekten mal 
angesehen. Die bauen wohl alle eine Lookuptabelle auf, und schauen 
einfach in dieser nach, um was es sich handelt. Der Z80pack 
Simulatorsourcecode ist sogar so Strange, das zum Beispiel für jede 
Permutation für LD r, r' eine eigene Funktion geschrieben wurde. Zudem 
mehrmals durch eine Tabelle mit 256 Einträgen zu iterieren, stell ich 
mir auch alles andere als schnell vor.
Gibt es denn keine Möglichkeit den Binärcode besser und effizienter zu 
parsen?
Ich hoffe, man kann in etwa verstehen, auf was ich hinaus will.

MfG
Tobias

von Possetitjel (Gast)


Lesenswert?

Tobias B. schrieb:

> Die 8Bit Register (8 Stück an der Zahl) werden durchnummeriert.

B, C, D, E, H, L (HL), A.

> Das heißt, es werden 3 Bit benötigt, womit wiederum 9
> Kombinationen möglich sind.

Nö. 3 Bit sind 8 Kombinationen. - Richtig ist, dass (HL)
bei vielen Befehlen durch irgendwas anderes ersetzt wird;
das ist wohl das Bitmuster 110.

> Die 9. Kombination wird natürlich auch verwendet, was die
> Arbeit mit Bitmasken erschwert.

Die Ausnahmen musst Du zuerst abprüfen; wenn keine Ausnahme
vorliegt, kannst Du mit der Maske weiterarbeiten.

> Um konkret zu werden. Opcodes können die Form
> 0 1 r r r r' r' r'

Ja. "LD r, r'"

> 0 1 r r r 1 1 0

Ja. "LD r, M" bzw. gleichwertig: "LD r, (HL)".

> 0 1 1 1 0 r r r

Ja. "LD (HL), r" bzw. "LD M, r".

> 0 1 1 1 0 1 1 0

Das ist 0x76, also "HALT".
Das ist die einzige Ausnahme, die Du halt vorher abtesten
musst. Alles andere geht nach Schema F, sofern Du "(HL)"
als Pseudo-Register behandelst.

> besitzen, wobei 1 1 0 die 9. Kombination ist, die nicht
> für die Identifikation von Registern benutzt wird.

Siehe oben.
Erstens ist es die ACHTE Kombination, und zweitens kann
man "(HL)" als 8-bit-Pseudoregister auffassen.

> Diesen kann man noch halbwegs mit Bitmasken parsen, aber
> es fängt schon dadurch an, schmutzig zu werden. Aber dann
> gibt es auch noch andere Opcodes in der Form
> 0 0 r r r x x x
> 0 0 q q x x x x
> 0 0 x x x x x x
> 1 0 x x x r r r
> 1 0 x x x x x x

Ja, richtig.

> Da fang ich mir nun schon richtig einen an, abzubrechen. Ich
> hab keine Idee, wie ich das intelligent parsen kann, ohne das
> der Code vollkommen dreckig wird. Ich kann nicht wirklich mit
> Bitmasken arbeiten,

Quark.

> da 1. Ich die drei Bit, wo die Register liegen, nicht einfach
> mit einer Maske ausblenden kann, da es eben eine Kombination
> gibt, die kein Register identifiziert (1 1 0)

Siehe oben: Die eine Ausnahme testet man vorher ab und behandelt
sie entsprechend. Was übrig bleibt, geht nach Schema F.

> Egal was ich mache. Es wird einfach schmutzig.

Quark.

> Die bauen wohl alle eine Lookuptabelle auf,

Genau.
Unter anderem mein eigener (nie fertiggestellter) Disassembler
macht das so. :)

> Zudem mehrmals durch eine Tabelle mit 256 Einträgen zu
> iterieren, stell ich mir auch alles andere als schnell vor.

Kann es sein, dass Du das Prinzip der Lookup-Tabelle nicht
ganz verstanden hast?

Der OpCode ist der Tabellenindex ; Du benötigst exakt
EINE Leseoperation der Art "BefehlsTyp := Tabelle[OpCode]".
Das ist rasend schnell; schneller geht's kaum.

Anschließend machst Du eine Fallunterscheidung nach
Befehlstyp.

von Georg (Gast)


Lesenswert?

Tobias B. schrieb:
> Zudem
> mehrmals durch eine Tabelle mit 256 Einträgen zu iterieren, stell ich
> mir auch alles andere als schnell vor.

Glaubst du im Ernst, dass der Z80 das bei der Befehlsausführung so 
macht? Was so ein Uraltprozessor leistet, sollte ein Programmierer 
Jahrzehnte später doch auch effektiv hinkriegen.

Tobias B. schrieb:
> Die Grundlagen, wie Parsing funktioniert und auch von Sprachen sind mir
> bekannt.

Tobias B. schrieb:
> Das heißt, es werden 3 Bit benötigt, womit
> wiederum 9 Kombinationen möglich sind.

Aber die Grundlagen der Programmierung überhaupt bzw. der Kombinatorik 
wohl eher nicht. Da solltest du zuerst mal ansetzen.

Georg

von MaWin (Gast)


Lesenswert?

Tobias B. schrieb:
> Gibt es denn keine Möglichkeit den Binärcode besser und effizienter zu
> parsen?

Bau eine 256 Einträge Tabelle, die sagt, WELCHE Operation aufgerufen 
wird, und jede dieser Funktionen verwendet gewisse Bits des OpAmps als 
Parameter für Register und andere.

Definiere pc als pointer auf so viele gepackten Bitfeld-Strukturen, wie 
es opcode-Typen gibt.
1
typedef union
2
{
3
  struct regreg {unsigned char reg1:3, unsigned chasr reg2:3, unsigned char op:2};
4
  struct reg{unsigned char reg:3; unsigned char op:5};
5
  struct wordreg{unsigned char:op; unsignedc hsar reg:2; unsigned char op2;};
6
}
7
INSTRUCTION;
8
INSTRUCTION *pc;
9
10
  enum {LD,LXI,STAX,STA,NOP,ADD,ADC,SUB,..} opcode[256]={NOP,LXI,STAX,..,ADD,ADD,ADD,ADD,ADD,ADD,ADD,ADD,ADC,ADC,ADC,ADC,ADC,ADC,ADC,ADC,SUB,SUB,SUB,SUB,SUB,SUB,SUB,SUB,...};
11
  // sogar 5 Tabellen, je nach dem ob ein BC, CD, ED, FD, vorweg kam
12
13
  switch(opcode[*pc])
14
  {
15
  case NOP:
16
    break;
17
  case LXI:
18
    lxi(op->wordreg.reg);
19
    break;
20
  case STAX:
21
    stax(op->wordreg.reg);
22
    break;
23
  case LD: // 0 1 r r r r' r' r'
24
    ld(op->regreg.reg1,op->regreg->reg2);
25
    break;
26
  case ADD: // 0 1 1 1 0 r r r
27
    add(op->reg.reg);
28
    break;
29
  case ADC: // 0 1 1 1 0 r r r
30
    carry=add(op->reg.reg,carry);
31
    break;
32
  case SUB: // 0 1 1 1 0 r r r
33
    sub(op->reg.reg);
34
    break;
35
  :
36
  }
Da du alle möglichen 256 opcodes auf andere Funktionen (mit ihren 
Parametern) auflösen kannst, kannst du problemlos solche unterbringen, 
bei 22 und 32 brauchst du kein STAX eintragen sondern kannst SHLD und 
STA eintragen.

von spess53 (Gast)


Lesenswert?

Hi

>Bau eine 256 Einträge Tabelle, die sagt, WELCHE Operation aufgerufen
>wird, und jede dieser Funktionen verwendet gewisse Bits des OpAmps als
>Parameter für Register und andere.

Unnötig. Ein Disassembler ließ sich mit mit ca. 3..4 
Schreibmaschinenseiten Assemblercode bewerkstelligen.

Ein interessanter Ansatz ist z.B. hier

http://www.z80.info/decoding.htm

zu finden.

MfG Spess

von avr (Gast)


Lesenswert?

spess53 schrieb:
> Unnötig. Ein Disassembler ließ sich mit mit ca. 3..4
> Schreibmaschinenseiten Assemblercode bewerkstelligen.

Aha. Es ging auch um einen Emulator. Diese trimmt man oft auf 
Geschwindigkeit. Bei nicht zu großen LUTs sind diese jeglichen anderen 
Lösung meist überlegen.

von Olaf (Gast)


Lesenswert?

Erinnere ich mich da eigentlich falsch, oder hatte nicht gerade der Z80 
noch einen SEHR grossen Berg an undokumentierten Opcodes die man auch 
verwenden konnte?

Olaf

von Michael L. (michaelx)


Lesenswert?

Tobias B. schrieb:
> ...
> Jedoch stoße ich dabei an Probleme, die auch in der Gegenwart nicht
> unerheblich sein werden. ...

Ah ja.

> ... Das heißt, es werden 3 Bit benötigt, womit
> wiederum 9 Kombinationen möglich sind. ...

Wenn ich das so lese, sehe ich ganz genau, auf welche Probleme du stößt:

1. Großen Mangel an Grundlagen.
2. Geringes Vorstellungsvermögen.

Ich 'ne ganz schlechte Kombination. ;-)

von Bernd (Gast)


Lesenswert?

Michael L. schrieb:

> Ich 'ne ganz schlechte Kombination. ;-)

Das glaube ich dir gern.

von spess53 (Gast)


Lesenswert?

Hi

>Aha. Es ging auch um einen Emulator.

Nicht unbedingt. Z.B. hatte der MC80

(http://www.robotrontechnik.de/index.htm?/html/computer/mc80.htm)

einen Quellcodeassembler. Das heißt es existierte kein Quelltext, nur 
der Code und eine Markentabelle. Zur Bildschirmausgabe musste der Code 
also direkt wieder rückübersetzt werden.

MfG Spess

von Hans-Georg L. (h-g-l)


Lesenswert?

Ich hab mal im letzten Jahrhundert einen ROM-Debugger für 8080/8085 
geschrieben und der lief auf auch auf einem 8085 zusätzlich zur 
Anwendung aber Heute muss das ja mindestens ein PC oder Raspi sein ...

Die meisten Befehle bestehen doch, soweit ich mich erinnere, aus 2 Bit 
Code und jeweils 3 Bit für Quell und Zielregister, das ist mit ein 
bissel Bitpopelei doch schnell gemacht. Und das was noch übrigbleibt und 
mehr als 1 Byte benötigt ist doch auch nicht die Welt.

von (prx) A. K. (prx)


Lesenswert?

Hans-Georg L. schrieb:
> Die meisten Befehle bestehen doch, soweit ich mich erinnere, aus 2 Bit
> Code und jeweils 3 Bit für Quell und Zielregister,

Nur ein Viertel vom 8080 Opcode Bereich, nämlich 40-7F für MOV. Nicht 
ohne Ausnahme natürlich, denn der Opcode für MOV M,M ist etwas gänzlich 
anderes.

Ausserdem wären da beim Z80 noch die Präfixe.

Ist aber kinderleicht verglichen mit der Codierung des 68000, mit den 
Ausnahmen von der Ausnahme von der Regel.

von Jobst M. (jobstens-de)


Lesenswert?

A. K. schrieb:
> Ist aber kinderleicht verglichen mit der Codierung des 68000, mit den
> Ausnahmen von der Ausnahme von der Regel.

Den 68k empfand ich immer als relativ einfach. Da konnte ich zu meiner 
aktiven Zeit auf dem Teil den Code direkt in Hex lesen, weil es so 
einfach war. An Ausnahmen kann ich mich da nicht erinnern. Wenn, dann 
waren es nur wenige.


Gruß

Jobst

von (prx) A. K. (prx)


Lesenswert?

Jobst M. schrieb:
> Den 68k empfand ich immer als relativ einfach.

Hier gehts grad nicht um die Programmierung von Programmen in Assembler, 
sondern um einen Disassembler oder Emulator dafür. Hast du mal dafür 
einen Disassembler geschrieben? Der genau nur die gültigen Befehle 
darstellt bzw. ausführt?

Da gibt es die regulär codierten Befehle mit den Adressierungsarten der 
Operanden. Bei denen je nach Befehl aber nicht alle Adressierungsarten 
zugelassen oder sinnvoll sind. So ist PC-relativ als Zieladresse aus 
"politischen" Gründen nicht zulässig. In den Codes solcher Ausnahmen 
stecken dann oft andere Befehle wie:
  MOVEP = Einzelbitbefehl auf Adressregister
  SWAP  = PEA vom Datenregister
  EOR   = CMP reg => mem
  CMPM  = EOR mit Adressregister
  etc

Daraus erklärt sich auch, weshalb der EOR Befehl merkwürdigerweise nur 
in einer Variante vom Register zum Speicher hin existiert, nicht aber 
mit Speicher zu Register. Im CMP Befehl ist diese Richtung redundant, 
also versteckte man den EOR Befehl darin. Und in einer Ausnahme dieser 
Ausnahme dann den CMPM Befehl.

Das wars dann aber immer noch nicht, denn die Codierungen von
    <op> reg,reg
existieren von der Systematik her zweimal, in
    reg = reg <op> r/m
und r/m = r/m <op> reg
wobei die zweite davon oft anderweitig verwendet wird (SUBX statt SUB, 
SBCD statt OR), im von der Systematik her analogen EOR Befehl mit 
Datenregister aber zulässig ist.

Aus Sicht des Programmierers ist 68000 leidlich orthogonal. Aus Sicht 
der Codierung aber nicht.

> Da konnte ich zu meiner
> aktiven Zeit auf dem Teil den Code direkt in Hex lesen,

Respekt. Der ist nämlich nur oktal wirklich leicht zu lesen, weil sich 
der Opcode meist als Codierung aus 4.3.3.3.3 Bits darstellt. Also nur 
die obersten 4 Bits in Hex gut abbildbar sind.

NOP kann ich allerdings auch heute noch auswendig in Hex. ;-)

von дамрфкнилх (Gast)


Lesenswert?

Die Schwierigkeiten eines Disassemblers liegen ganz anderswo. Naemlich 
:wo liegt der Code. Ich musste mal 6809 code disassembleren.
1) am dem Resetvektor. Man beginnt mit dem Reset Vektor und hangelt sich 
dem Code entlang. Irgendwann kommt ein PC relativer Jump, eine Switch 
anweisung, die als PC Relativ implemetiert wird. Da beginnt man mal mit 
dem Code grad danach, bis der wieder wegspringt. Dann kommt 
moeglicherweise die naechste Switch-Moeglichkeit. Moeglicherweise wurde 
der Bereich der Switch-Moeglichkeiten vor dem PC relativen Jump 
abgetestet. Das muss man dann genau anschauen. und alle Faelle 
durchgehen. Wenn ein ungueltiger OpCode kommt, hat man vorher einen 
Fehler gemacht.
Dann geht man dem code aller Interruptvektoren nach.
Dann gibt es Datenstrukturen im Codesegment, die Konstanten, als DB, DW, 
DS. Die muss man auch rausziehen.

von Jobst M. (jobstens-de)


Lesenswert?

A. K. schrieb:
> Hast du mal dafür einen Disassembler geschrieben?

Nein, war ja nicht notwendig :-)

A. K. schrieb:
> Der ist nämlich nur oktal wirklich leicht zu lesen, weil sich
> der Opcode meist als Codierung aus 4.3.3.3.3 Bits darstellt. Also nur
> die obersten 4 Bits in Hex gut abbildbar sind.

Okay, Hex2Bin passiert im Auge schon :-D
Das ist auch immer noch so.

> NOP kann ich allerdings auch heute noch auswendig in Hex. ;-)

Sicher bin ich mir noch bei RET: 4E75

Für alles Andere müsste ich nun auch wieder nachsehen ...


Gruß

Jobst

von Klaus (Gast)


Lesenswert?

@ Tobias Bla

Ich sehe das Problem im Moment eher darin, dass Du in bestimmten Fällen, 
die Du ja in Deinem Eröffnungspost geschildert hast, den nötigen Code 
als "dreckig" oder "schmutzig" bezeichnest. Du führst da eine Wertung 
ein, die Dich an dem Punkt an dem Du nun bist, innehalten und hier 
fragen lässt.

Mein Rat wird Dich vielleicht nicht wirklich befriedigen, aber ich 
möchte ihn dennoch geben: Trenne Dich innerlich von solchen Wertungen. 
Du erschwerst Dir nur selbst die Arbeit in dem Du innerliche 
(psychologische ) Hemmnisse erzeugst.

Betrachte Die Tatsachen möglichst nüchtern und liste einfach der Reihe 
nach die Fälle auf. Ich habe das mal für einen AVR gemacht. Der Fall ist 
leider dem Umfang nach nicht wirklich gleich, aber die Systematik ist 
die selbe.

Ich habe einfach alle Opcodes binär aufgelistet und Gruppen gebildet. 
Als Hilfsmittel habe ich Ternärvektoren verwendet. Daraus ergeben sich 
leicht Systematiken und eben auch ein paar Sonderfälle. Meist ist es 
aber so, dass sogar die Sonderfälle für sich eine oder mehrere 
Systematiken bilden. Das dauert alles ein paar Stunden oder auch Tage, 
aber es funktioniert.

Das ist jetzt nur grob angedeutet, aber vielleicht reicht Dir das ja 
schon.

Wenn Du willst, poste ich mal die CSV-Datei.

In Summe also:
1. Vermeide unsachliche wertende Vorwegnahmen.
2. Wende eine Methode an und führe die zuende.
3. Dann erst werte das, aber nach objektiven Kriterien. Z.B. wieviele 
verschiedene Bedingungen hast Du um die Sonderfälle zu erfassen.

Viel Erfolg.

von Possetitjel (Gast)


Lesenswert?

Klaus schrieb:

> Als Hilfsmittel habe ich Ternärvektoren verwendet.

Verblüffend. Alter Chemnitzer?

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.