Z80-TRICKS

Die folgende Zusammenstellung von Tricks und Tips für Z80-Assembler-Programmierer basiert auf einer Veröffentlichung aus dem Jahre 1980! Kelly Smith, damals Sysop des CP/M-Net, verriet einige Tricks, die er bei seinen "Datenreisen" durch die damalige Softwarelandschaft entdeckt hatte. Obwohl nun schon über 16 Jahre vergangen sind, haben sie sich unter deutschen Assembler-Programmierern wenig herumgesprochen. Und da der Z80 immer noch im Einsatz ist (auch "neue" Geräte wie NC100 oder Amstrads PcW16 arbeiten mit dem Z80), sind diese Tips immer noch aktuell.

Der LD BC - Trick
Stellen wir uns folgende Situation vor: wir haben eine zentrale Fehlerroutine geschrieben, die von unserem Programm von diversen Stellen aus angesprochen wird. Das könnte dann so aussehen:
fehl1:        ld        a,1
        jp        fehler
fehl2:        ld        a,2
        jp        fehler
fehl3:        ld        a,3
fehler:
; Meldung mit Nummer in A ausgeben
Was passiert da? Wenn vom Programm aus an eine der Fehlerroutinen wie z.B. "fehl1" gesprungen wird, wird der Akkumulator mit einer Nummer geladen und anschließend weiterverzweigt.
Eingefleischte Z80-Coder rümpfen natürlich über die "Jump-Befehle" die Nase, da die immerhin drei Bytes lang sind und durch einen relativen Befehl wie "JR ..." ersetzt werden können. Das spart pro Sprungbefehl immerhin ein Byte!
Aber es geht noch kürzer! Wir definieren uns zu Anfang eine Konstante; dann sieht es wie folgt aus:
ldbc        equ        1
;
fehl1:        ld        a,1
        db        ldbc
fehl2:        ld        a,2
        db        ldbc
fehl3:        ld        a,3
fehler:
; weiter wie gehabt
Und das soll laufen? Sicher, wer es nicht glaubt, kann es ruhig ausprobieren. Welches Geheimnis steckt dahinter? Nun, wenn der Z80-Prozessor auf ein Byte stößt, das den Wert "1" besitzt, dann meint der Z80, daß es sich um einen Befehl wie

LD BC,nn

handelt. Ein solcher Befehl ist aber stets drei Bytes lang. Jetzt läuft folgendes ab: Das Programm verzweigt zu "fehl1", der Z80 lädt zunächst wie gewünscht den Akkumulator und erkennt dann einen Befehl "LD BC". Stur wie er nun einmal ist, wird jetzt das Registerpaar "BC" mit dem folgenden zwei Bytes geladen, nämlich zufällig die Bytes, aus denen der folgende Befehl "LD A,2" besteht. Der Befehlszähler wird aber um drei Bytes weitergesetzt, und das Spielchen wiederholt sich.

Dieser Trick ist doch ganz schön schlau? Wir brauchen je Verzweigung nur ein Byte. Dieser Trick war übrigens bereits zu Zeiten des "Altair" unter den Assemblerfreaks bekannt. Wenn wir so etwas in unseren Programmen einsetzen, sollten wir das besonders gut dokumentieren! Dieser Trick ist auch für Disassembler eine unlösbare Aufgabe. Wenn wir also unsere Programme davor schützen wollen, daß der Code disassembliert wird, dann sollten wir so etwas einbauen. Wetten, daß da so mancher Cracker daran scheitern wird?

Der OR Trick
Jetzt zu einer anderen Situation: Eine unserer Routinen soll zu Beginn auf zwei Zustände prüfen (JA oder NEIN). Wir legen fest: Wenn im Register A der Wert "Null" steht, dann liegt der Fall "NEIN" vor, sonst der Fall "JA". Ein Z80-Anfänger codiert dann sicherlich:
JA:        ld        a,1
        jp        weiter
NEIN:        xor        a
;
weiter:
Wenn unser Programm zu "JA" verzweigt, wird der Akku mit "1" geladen und dann zur weiteren Behandlung gesprungen. Beim Einsprung "NEIN" dagegen wird der Akku gelöscht, und es geht wie gehabt.
Jetzt wollen wir uns einmal folgenden Code ansehen, von dem ich behaupte, daß er dieselbe Aufgabe bewältigt:
ORC        equ        0f6h
;
JA:        db        ORC
NEIN:        xor        a
;
weiter:
Wieso funktioniert das? Das Geheimnis liegt wieder einmal in der definierten Konstante, diesmal ORC. Verzweigt nämlich unser Programm zu "JA", dann wird die Konstante ORC als Befehl "OR byte" erkannt, d.h. es wird der aktuelle Inhalt des Akkumulators mit dem folgenden Byte (nämlich 0AFH für XOR a) "geodert"; das Endergebnis in A wird auf jeden Fall von Null verschieden sein, und darauf kommt es doch an.
Auch dieser Trick ist eine unlösbare Falle für Disassembler. Aber wenn wir so etwas einsetzen, dann sollten wir es uns gut dokumentieren, sonst könnte es sein, daß wir später unser eigenes Programm nicht mehr verstehen.

Der EX (SP),HL Befehl
Dieser Befehl wird recht selten eingesetzt, obwohl er viel bewirkt. Er tauscht nämlich das oberste Wort im Stack mit dem HL-Register. Wenn uns z.B. die Register ausgegangen sind (das kommt bei den wenigen Z80-Registern leider nur allzu oft vor - schon der mickrigste SPARC besitzt 32 Register!) und wir die Register BC und HL vertauschen müssen, dann können wir uns behelfen:
        push        bc
        ex        (sp),hl
        pop        bc
Was passiert da? Zunächst wird BC an die Spitze des Stack abgelegt. Anschließend vertauscht der Ex-Befehl das HL-Register mit der Stackspitze, d.h. der Inhalt vom BC ist jetzt HL, und der Wert von HL liegt auf dem Stack. Der POP-Befehl holt nun den ursprünglichen HL-Wert in das BC-Register.
Auch dieser EX-Befehl läßt sich auch zur (trickreichen) Ansteuerung von Unterprogrammen einsetzen. Nehmen wir folgendes Programm:
        ld        c,1
        call        upro
        ld        c,2
        call        upro
        ld        c,3
        call        upro
Hier wird also wieder ein zentrales Unterprogramm "UPRO" angesprungen, das im Register "C" einen bestimmten Wert erwartet und auswertet. Mit sog. "Inline"-Code läßt sich das speichersparend erreichen:
        call        upro
        db        1
        call        upto
        cb        2
        call        urpro
        db        3
Hier steht der Wert für das Unterprogramm immer unmittelbar hinter dem CALL-Aufruf. Unser Unterprogramm muß dies, natürlich, berücksichtigen.
upro:
        ex        (sp),hl
        ld        c,(hl)
        inc        hl
        ex        (sp),hl
Um das zu verstehen, muß man wissen, daß der CAL-Befehl die Rücksprungadresse auf den Stack legt. Der erste EX-Befehl bringt nun diese Adresse ins HL-Register. Diese Adresse zeigt aber auf das erste Byte hinter dem CALL-Befehl und damit auf unseren gewünschten Wert. Wir können also mit den LD-Befehl unseren Wert in C-Register laden. Dann müssen wir den Zeiger im HL um eine Stelle weiterschalten, was der INC-Befehl durchführt. Der nun folgende zweite Ex-Befehl bringt diese neue Adresse auf den Stack und rekonstruiert unser HL-Register. Wenn dann unser Unterprogramm mit einem RET-Befehl beendet wird, springt es automatisch an die richtige Stelle zurück.

Indirekte Stack Sprünge
Nehmen wir folgende Programmsituation an:
        call        upro
        jp        weiter
upro:
;        ld        hl,xwert
        ret
weiter:
; erwartet in HL xwert

Unser Unterprogramm UPRO hat u.a. die Aufgabe, HL mit einem gewissen Wert "xwert" zu versorgen. Diese Aufgabe können wir auch anders lösen (und dabei ein paar Bytes sparen):

        ld        hl,weiter
        push        hl
        ld        hl,xwert
        ret
Zunächst wird in das HL-Register die Adresse "weiter" geladen und auf den Stack gelegt. Dann versorgen wir HL mit dem gewünschten Wert "xwert". Der nun folgende RET-Befehl holt sich vom Stack die Adresse (nämlich "weiter").
Diese Methode läßt sich auch für sog. Filterprogramme einsetzen:
Nehmen wir einmal an, wir wollen im Register A die Sonderzeichen wie Punkt, Komma, Semikolon oder Doppelpunkt auswerten. Da könnte unsere Routine etwa so aussehen:
        cp        '.'
        jp        z,filter
        cp        ','
        jp        z,filter
        cp        ';'
        jp        z,filter
        cp        ':'
        jp        z,filter
Wir fragen also der Reihe nach das A-Register auf den gewünschten Wert ab und verzweigen im JA-Fall nach "filter". Wieder können wir ein paar Bytes einsparen, wenn wir wie folgt codieren:
        ld        bc,filter
        push        bc
        cp        '.'
        ret        z
        cp        ','
        ret        z
        cp        ';'
        ret        z
        cp        ':'
        ret        z
        pop        bc
Zunächst laden wir BC mit der gewünschten Adresse "filter" und deponieren das Ganze auf dem Stack. Dann fragen wir den Akkumulator ab. Bei Übereinstimmung springt dann der RET-Befehl an die am Stack liegende Adresse, also nach "filter". Wir dürfen nur nicht vergessen, am Ende der Vergleichsbefehle mit dem POP-Befehl den Stack wieder in Ordnung zu bringen.

Registertausch
Wenn wir also trickreich programmieren wollen, kann es leicht passieren, daß wir uns selbst austricksen. Wollen wir z.B. das Register DE nach BC bringen, so wird mancher programmieren:
        push        bc
        pop        de
So trickreich das auch aussehen mag, der "normale" Weg ist schneller:
        ld        d,b
        ld        e,c
Auch hier bewahrheitet sich das Motto von Albert Einstein: "Make it simple as possible, but not simpler!" - ein Motto übrigens, das vom geistigen Vater der Programmiersprache Pascal, Niklaus Wirth, immer wieder zitiert wurde und wird.

Zum Schluß noch ein Trick, der so manchen in Grübeln versetzten wird:

        ld        c,81h
        push        bc
        pop        af
Da wird zunächst das C-Register mit dem Wert 81h geladen und später BC auf den Stack abgelegt. Noch etwas später wird mit diesem Inhalt das Register A und das Flag-Register geladen. Nach dem POP-Befehl sind nämlich das Carry- und Sign-Bit gesetzt und alle anderen Flags zurückgesetzt! Ich denke, daß Programmierer, die sogar den Sourcecode vor sich liegen haben, einige Zeit darübe nachdenken müssen.

DangSoft


Anmerkung der Redaktion:
Weitere Tips & Tricks finden sich in der 'PC Amstrad International' August 1989, Seite 74:
Geheimbefehle, Assemblerprogrammierung mit Pfiff ...

Abgedruckt in Klubzeitung Nr. 43. Autor: DangSoft


... zurück ...