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 ausgebenWas 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 gehabtUnd 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 bcWas 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 uproHier 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 3Hier 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),hlUm 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 retZunä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,filterWir 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 bcZunä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 deSo trickreich das auch aussehen mag, der "normale" Weg ist schneller: ld d,b ld e,cAuch 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 afDa 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 |