Z80-TRICKSDie 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.
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
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?
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.
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.
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.
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.
DangSoftAnmerkung 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 |