Effektives Programmieren in Assembler
Es gibt viele Möglichkeiten, ein Basic-Programm schneller und komfortabler zu gestalten. Aber auch für die Assemblerprogrammierung gibt es einige Tricks und Kniffe, die wir Ihnen in diesem praxisnahen Kurs verraten wollen.
Wer das Optimum an Geschwindigkeit aus seinem Computer herausholen will, kommt an Maschinensprache nicht vorbei. Die Grundlagen zur Maschinenprogrammierung wurden bereits im Kurs »Assembler ist keine Alchimie«, den Sie in diesem Sonderheft finden, geschaffen. Das Thema dieses Artikels ist es nun, die Möglichkeiten von Maschinensprache optimal zu nutzen. Sie erfahren, wie man
a) Programme beschleunigen und
b) Speicherplatz sparen kann.
Dazu werden Ihnen eine Vielzahl von Programmiertechniken, Tips und Tricks vermittelt, die Ihnen die Programmierung erleichern.
1. Beschleunigungen des Betriebssystems (in Assembler)
Der C 64 muß viele Aufgaben gleichzeitig erledigen: Bearbeiten des Hauptprogramms, Ablauf der Systeminterrupts und Senden des Video-Signals (an den Monitor/Fernseher). Alle diese Funktionen erfordern
- viele Zugriffe auf den Datenbus des Prozessors
- und dadurch Ausführungszeit.
Unser Grundproblem ist nun, wie wir den Computer dazu bewegen, diese Aufgaben nicht (oder nur teilweise) auszuführen.
a) Eingriffe in den Systeminterrupt
Eine detaillierte Beschreibung des Systeminterrupts finden Sie im bereits erwähnten Kurs »Assembler ist keine Alchimie«. Hier möchte ich nur zusammenfassen, was im normalen Interrupt des Betriebssystems geschieht: 60 mal in der Sekunde wird das Hauptprogramm verlassen und die Routine ab $EA31 angesprungen. Ist diese abgearbeitet, wird wieder ins Hauptprogramm zurückgesprungen. Während dieser Unterbrechung (»interrupt«) tut sich einiges:
- die RUN/STOP-Taste wird überprüft
- die Tastatur und der Datasettenmotor werden abgefragt
- das Cursorblinken wird erledigt
- die interne Uhr (TI$) wird gestellt.
Überlegen wir uns, welche Funktionen verzichtbar sind: Die RUN/STOP-Taste bewirkt nur in Basic-Programmen einen Abbruch, in Assembler müßte sie zum Beispiel über »JSR $FFE1« zusätzlich abgefragt werden. Die interne Uhr findet von Maschinensprache aus praktisch keine Verwendung. Kurz und gut, ein Maschinenprogramm kann auf beide Funktionen verzichten. Dies wird durch ein
LDA #$34
STA $0314
erreicht. Weil der Computer dadurch entlastet wird, läuft das Hauptprogramm etwas schneller ab.
Die Normaleinstellung erhält man mit
LDA #$31
STA $0314
Können Sie zwischenzeitlich auf die ganze Interrupt-Routine verzichten, genügt ein einziger Befehl:
SEI (»set Interrupt«)
Er verhindert grundsätzlich das Auftreten von Interrupts.
Die Normaleinstellung bewirkt:
CLI (»clear interrupt«)
Es gibt aber noch eine Möglichkeit, im Zusammenhang mit dem Systeminterrupt: Von der Adresse $DC05, die als Zähler dient, hängt die Anzahl der Interrupts (in der Regel 60 Aufrufe pro Sekunde) in einer bestimmten Zeit ab. Diese Adresse kann durch Schreibzugriff geändert werden. Schreibt man in $DC05 einen niedrigen Wert (im Extremfall 0), so werden sehr viele Interrupts ausgelöst. Dies macht sich in der Geschwindigkeit der Interrupt-Routine bemerkbar. Cursor und Tastaturabfrage werden sehr schnell, die interne Uhr geht vor, und so weiter. Verwendet man eine eigene, eventuell zeitkritische Interrupt-Routine, kann sie auf diese Weise beschleunigt werden.
Dieser Geschwindigkeitszuwachs geht allerdings auf Kosten des Hauptprogramms, das stark verlangsamt wird. Bei wenigen Interrupts (große Zahl in $DC05) wird es beschleunigt. Die entsprechenden Assemblerbefehle lauten:
LDA #$FF
STA $DC05
um eine starke Beschleunigung zu bewirken.
Die Normaleinstellung wird durch
LDA #$3A
STA $DC05
erreicht.
b) VIC-Register Nummer 17
Ist Ihnen schon bei Hypra-Load, beim Arbeiten mitder Datasette und einigen Kopierprogrammen aufgefallen, daß manchmal der Bildschirm abgeschaltet wird (ähnlich wie im FAST-Mode des C 128)? Dies kann man mit einem Vorhang vergleichen, der zwischenzeitlich den Bildschirm verdeckt. Der Bildschirm kann zwar nach wie vor (hinter dem Vorhang) geändert werden (PRINT-Anweisungen werden also ausgeführt), aber sichtbar wird die Wirkung erst, wenn der Vorhang entfernt wird.
Verantwortlich für das Ein-/Ausschalten des Bildschirms ist das VIC-Register Nummer 17:
| Bit 4 gesetzt: | Bildschirm wird angezeigt |
| Bit 4 gelöscht: | Bildschirm wird abgeschaltet und nimmt Rahmenfarbe an. |
Da wir die theoretischen Grundlagen haben, brauchen wir nur noch unser Wissen in Befehle umzusetzen:
Bildschirm abschalten:
LDA $D011 ($D011 ist VIC-Register #17)
AND #$EF ($EF = %11101111)
STA $D011 ↑
Bit 4
In diesem Zustand arbeiten manche Kopierprogramme um zirka 15% schneller. Programme, dienichtaufexterneGeräte wie die Floppy zugreifen, laufen zirka 5% schneller ab. Bildschirm wieder einschalten:
LDA $D011
ORA#$10 ($10 = %00010000)
STA $D011 ↑
Bit 4
Dies ist der Normalzustand.
c) Hinweise zum bisher Gesagten
Alle bis zu dieser Stelle genannten Tricks beziehen sich auf die Beschleunigung von Programmen. Sie lassen sich leicht nachträglich einfügen, weil am Programmalgorithmus keine Änderungen erforderlich sind.
Sie können das Abschalten des Bildschirms mit dem Abschalten oder Einschränken des Interrupts verknüpfen, um die Geschwindigkeit noch weiter zu erhöhen. Wenn Sie den Interrupt ganz abschalten (SEI), bringt es keinen zusätzlichen Gewinn, ihn einzuschränken oder die Zahl der Aufrufe zu ändern.
Beachten Sie bitte, daß alle beschriebenen Tricks durch RUN/STOP-RESTORE, einem Reset oder den Assemblerbefehl BRK rückgängig gemacht werden.
2. Systembeschleunigungen in Basic
Hier erfahren Sie, wie sich die Systembeschleunigungen von Basic aus verwerten lassen. Die Nebenwirkungen bleiben allerdings die gleichen, wie unter 1. genannt.
a) Interrupt einschränken
POKE788,52 verkürzt die Interrupt-Routine um das Abfragen der RUN/STOP-Taste und das Stellen von TI$.
POKE 788,49 Normalzustand
In Basic ist das Ausfallen von RUN/STOP und TI$ wesentlich störender als in Maschinensprache. Überprüfen Sie daher Ihre Programme auf Verwendung von TI$ und fügen Sie den POKE erst nach (!) der Fertigstellung des Programms ein.
b) Interrupt abschalten
POKE 56334,PEEK (56334) AND 254
schaltet den Interrupt ab,
POKE 56334,PEEK(56334) OR 1
schaltet ihn wieder ein. Dies geschieht dadurch, daß der Timer ab- beziehungsweise wieder eingeschaltet wird.
c) Anzahl der Interrupt-Aufrufe ändern
POKE 56325,0: Extrem viele Interruptaufrufe
POKE 56325,255: Extrem wenige (daraus folgt: Interrupt langsam, Basic-Programm schnell)
d) Bildschirm abschalten
POKE 53265,PEEK(53265) AND 239
schaltet den Bildschirm ab.
POKE 53265,PEEK(53265) OR 16
schaltet ihn wieder ein.
An dieser Stelle sei noch einmal auf Punkt 1c hingewiesen, damit keine (vermeidbaren) Probleme auftreten.
Anhand von Listing 1 wollen wir uns nun mit der Anwendung der Systembeschleunigungen befassen. Dieses kleine Beispielprogramm, an dem Sie nach Herzenslust experimentieren können, versucht, mit Hilfe von TI$ die Arbeitsdauer der Schleife (Zeile 150) zu messen.
90 goto 200
100 rem >> up - schleife <<
110 :
120 print" <taste>";:wait 198,1:poke198,0:fori=1to7:printchr$(20);:next
130 :
140 for i=1 to 100:next
150 ti$="000000":for i = 0 to 255:poke 53280,i and 15:next:printti$:return
160 :
170 rem >> up - cursorblinken aus <<
180 :
190 poke 207,0:poke 204,1:print" ":return
200 rem -------------------
210 rem -- hauptprogramm --
220 rem -------------------
230 :
240 printchr$(147)"demo fuer systembeschleunigungen (basic)";
250 print"----------------------------------------"
260 print"{down}{down}1) normalzustand";:gosub100
270 :
280 print"{down}2) verkuerzter interrupt";:poke788,52:gosub 100:poke 788,49
290 :
300 print"{down}3) haeufige interrupts";:poke 56325,20:poke204,0:gosub100:gosub170
310 :
320 print"4) seltene interrupts";:poke 56325,150:poke204,0:gosub100:gosub170
330 sys64931:rem normalzustand ein
340 :
350 print"5) bildschirm abgeschaltet ";:poke53265,peek(53265) and 239:gosub140
360 poke 53265,peek(53265) or 16:print"{down}** ende **"
Während des Ablaufs dieser Schleife, die kontinuierlich die Rahmenfarbe ändert, sollten Sie keine Taste drücken, um die Meßwerte nicht zu verfälschen.
Wenn Sie dies beachten, erhalten Sie folgende Werte:
- Normalzustand: 000003
- Verkürzter Interrupt: 000000
An der gemessenen Zeit können Sie erkennen, daß TI$ abgeschaltet wurde. - Häufige Interrupts: 000010
Aufgrund vieler Interrupt-Anforderungen wurde die Uhr TI$ sehr oft erhöht. - Seltene Interrupts: 000001
Da die IRQ-Routine nur selten durchlaufen wurde, ist TI$ kaum weitergezählt worden. - Bildschirm abgeschaltet: 000002
Nur bei diesem Punkt (und natürlich auch bei »1«) hat TI$ volle Aussagekraft bezüglich der Ablaufzeit. An dieser Zeit können wir erkennen, daß durch das Abschalten des Bildschirms tatsächlich gegenüber »1« ein Zeitgewinn anfällt.
Bei den Punkten »3« und »4« wurde der Cursor eingeschaltet. Bei »3« (häufige Interrupts) ist er sehr schnell, bei »4« dagegen sehr langsam.
An Punkt »5« können Sie erkennen, daß bei abgeschaltetem Bildschirm der Hintergrund immer die Rahmenfarbe ($D020) annimmt, ohne daß wir die entsprechende Farbe ins Register$DO21 »POKEn«.
3. Optimierung der Bildschirmausgabe
Ohne die Bildschirmausgabe kommt kein Programm aus, aber oft kostet sie unnötig viel Rechenzeit. Der Grund ist hier nicht beim Betriebssystem zu suchen, sondern bei umständlicher Programmierung. Diese wiederum ist auf mangelndes Know-how zurückzuführen, welches wir nun ändern wollen.
In der Regel wird zur Ausgabe eines Zeichens dieses in den Akku geladen und die Routine BASOUT ($FFD2) aufgerufen. Veranschaulichen wir uns einmal die Arbeitsweise von BASOUT: Das Betriebssystem prüft bei jedem Zeichen, ob es sich um einen Buchstaben oder ein Steuerzeichen, zum Beispiel »Bildschirm löschen« handelt. Buchstaben werden in den Bildschirmcode umgewandelt und ins Bildschirm-RAM ab $0400 geschrieben.
Für Steuerzeichen existieren jeweils Unterroutinen die zum Beispiel eine Leerzeile einfügen, den Bildschirm löschen oder ähnliches.
Diese aufwendige Überprüfung verlangsamt die Bildschirmausgabe erheblich. BASOUT läßt sich zwar geringfügig beschleunigen, indem man statt bei $FFD2 (Kerneleinsprung) bei $E716 einsteigt, aber es geht noch schneller:
a) Bildschirm löschen
Langsam:
| LDA #$93 | $93 = 147 = Code für »Bildschirm löschen«, entspricht PRINT CHR$(147) |
| JSR $FFD2 | (oder $E176) |
Schnell:
| JSR $E544 | (Routine für »Bildschirm löschen«) |
b) Cursor in Home-Position (linke obere Ecke)
Langsam:
| LDA #$13 | ; $13 = Code für »Cursor Home« |
| JSR $FFD2 | (oder $E176) |
Schnell:
| JSR $E566 | (Routine für »Cursor Home«) |
c) Cursor-Positionierung
Langsam:
Senden von Steuerzeichen (CRSR DOWN, UP und so weiter) über BASOUT.
Schnell:
LDX #Zeile
LDY #Spalte
JSR $E50C (Cursorposition setzen)
d) Textausgabe
Unkomfortable Lösung:
Senden von Zeichen (Buchstaben, Grafikzeichen) über BASOUT.
Eine solche Schleife finden Sie in Listing 2, Zeilen 148 -220 und 320 - 330. Nach dem Start durch »SYS 49152« gibt Listing 2 zweimal hintereinander den Text »DAS IST DER TEXT« aus. Das erste Mal wird der Text über eine BASOUT-Schleife gedrückt, beim zweiten Mal nimmt das Programm die Komfortable Lösung:
Ab der Adresse »TEXT« muß der Text (in ASCII-Darstellung) stehen, in dem keine Anführungszeichen vorkommen dürfen. Am Ende des Textes muß $00 als Endmarkierung zu finden sein. Die Ausgabe erfolgt dann über
LDA #<(TEXT) Low-Byte der Adresse
LDY #>(TEXT) High-Byte
JSR $AB1E
Die Routine $AB1E wird fortan als »STROUT« (STRing-OUTput = String-Ausgabe) bezeichnet. STROUT ist zwar etwas langsamer als BASOUT; dafür erlaubt die komfortable Parameterübergabe eine wesentlich bequemere Programmierung, wie Sie am zweiten Teil von Listing 2 (Zeilen 260 -300, 320 - 330) sehen können. Mit nur drei Befehlen wird der Text ausgegeben!
100 -.LI 1,3,0 110 -; 120 -; TEXTAUSGABE (UEBER BASOUT) 130 -; 140 -.BA $C000 ; START: SYS 49152 150 -; 160 -.GL BASOUT = $FFD2 170 -; 180 - LDX #0 190 -SCHLEIFE LDA TEXT,X ; ZEICHEN LESEN 200 - INX 210 - JSR BASOUT ; UND AUSGEBEN 220 - BNE SCHLEIFE ; SCHON ENDMARKIERUNG? 230 -; 240 -; TEXTAUSGABE (UEBER STROUT) 250 -; 260 -.GL STROUT = $AB1E 270 -; 280 - LDA #<(TEXT) ; LOW-BYTE IN AKKU 290 - LDY #>(TEXT) ; HIGH-BYTE IN Y 300 - JMP STROUT ; TEXTAUSGABE UND ENDE 310 -; 320 -TEXT .TX "DAS IST DER TEXT!" 330 -.BY 0 ; ENDMARKIERUNG DES TEXTES
Eine Anwendung von (fast) allen Routinen aus der Beschleunigungsmethode 5 zeigt Listing 3.
100 -.LI 1,3,0 110 -; 120 -; TEXTAUSGABE (UEBER STROUT) 130 -; 140 -.BA $C000 ; START: SYS 49152 150 -; 160 -.GL STROUT = $AB1E 170 -.GL CURSOR = $E50C 180 -.GL CLRSCR = $E544 ; BILDSCHIRM LOESCHEN 190 -; 200 -.GL ZEILE = 12 210 -.GL SPALTE = 10 220 -; 230 - JSR CLRSCR ; = PRINT CHR$(147) 240 - LDX #ZEILE ; ZEILE IN X 250 - LDY #SPALTE ; SPALTE IN Y 260 - JSR CURSOR ; CURSOR SETZEN 270 - LDA #<(TEXT) ; LOW-BYTE IN AKKU 280 - LDY #>(TEXT) ; HIGH-BYTE IN Y 290 - JMP STROUT ; TEXTAUSGABE & ENDE 300 -; 310 -TEXT .TX "DAS IST DER TEXT!" 320 -.BY 0 ; ENDMARKIERUNG FUER STROUT
Der Bildschirm wird gelöscht und in Zeile 12 ab Spalte 10 ein Text ausgegeben. Auch in diesem Programm sollten Sie zur Übung etwas experimentieren!
e) Kopieren des Textes in den Bildschirmspeicher
Dies ist die schnellste Methode: Der Text wird in den Bildschirmspeicher kopiert. Die lange Umwandlung entfällt völlig, da der Text als fertiger Bildschirmcode im Speicher abgelegt wird. Wenn einige Kopfzeilen (zum Beispiel mit Copyright-Vermerken) an verschiedenen Stellen ausgegeben werden sollen, ist es ratsam, ein kleines Unterprogramm zu erstellen. Dieses schreibt dann die Kopfzeilen direkt in den Bildschirmspeicher, ohne die aktuelle Cursor-Position zu beeinflussen.
Eines müssen Sie aber unbedingt beachten: Die Farbgebung ist nur durch Ändern des Farb-RAMs möglich.
Eine Tabelle der Bildschirmcodes finden Sie übrigens im Anhang des C 64-Handbuchs und am Schluß dieser Ausgabe.
Beschäftigen wir uns nun mit Listing 4:
100 -.LI 1,3,0 110 -; 120 -; TEXT IN VIDEO-RAM SCHREIBEN 130 -; 140 -.BA $C000 ; START: SYS 49152 150 -; 180 -.GL CLRSCR = $E544 ; BILDSCHIRM LOESCHEN 190 -; 200 -.GL ZEILE = 12 210 -.GL SPALTE = 10 220 -; 230 -.GL VIDEORAM = 1024 ; BILDSCHIRMSPEICHER 240 -.GL ADRESSE = VIDEORAM + (40*ZEILE) + SPALTE 250 -; 255 - JSR CLRSCR ; = PRINT CHR$(147) 260 - LDX #0 270 -SCHLEIFE LDA TEXT,X ; BILDSCHIRMCODE LESEN 280 - BEQ ENDE ; =0, DANN ENDE 290 - STA ADRESSE,X ; IN BILDSCHIRMSPEICHER 295 - INX 296 - JMP SCHLEIFE ; NAECHSTES ZEICHEN 300 -ENDE RTS 305 -; 310 -TEXT .BY 4,1,19," ",9,19,20," " 311 -.BY 4,5,18," ",20,5,24,20,"!" 320 -.BY 0 ; ENDMARKIERUNG DES TEXTES
Dieses Programm entspricht in der Wirkung Listing 3, gibt den Text jedoch nicht über die Betriebssystem-Routinen CURSOR und STROUT aus, sondern schreibt ihn direkt in den Bildschirm.
In den Zeilen 310 - 320 steht der Bildschirmcode des Textes.
Zurück zur Routine STROUT: Diese Routine arbeitet, da sie sich auf die BASOUT-Routine stützt, auch mit Peripheriegeräten wie Floppy und Drucker, wenn diese über dem CMD-Befehl als Ausgabegeräte definiert wurden. In »Assembler ist keine Alchimie« wurde gezeigt, wie man mit der BASOUT-Routine die Drucker-Ausgabe betreibt. Dort wurden alle wichtigen Routinen bis ins Detail beschrieben.
Listing 5 gibt einen Text zuerst auf dem Drucker und dann auf dem Bildschirm aus. Daran soll außer dem Druckerbetrieb auch gezeigt werden, wie man die Parameterübergabe an STROUT als Makro (Zeilen 230 - 270) definiert und sich somit einen bequemen Ausgabe-Befehl schafft.
100 -.LI 1,3,0 110 -; 120 -; DRUCKER-AUSGABE MIT 130 -; DER STROUT-ROUTINE 140 -; 150 -.GL STROUT = $AB1E 160 -.GL SETNAM = $FFBD ; DIE BEDEUTUNG 170 -.GL SETLFS = $FFBA ; DIESER ROUTINEN 180 -.GL OPEN = $FFC0 ; ENTNEHMEN SIE 190 -.GL CHKOUT = $FFC9 ; BITTE DEM KURS 200 -.GL CLRCHN = $FFCC ; "ASSEMBLER IST 210 -.GL CLOSE = $FFC3 ; KEINE ALCHIMIE" 220 -; 230 -.MA PRINT (ADRESSE) 240 - LDA #<(ADRESSE) 250 - LDY #>(ADRESSE) 260 - JSR STROUT 270 -.RT 280 -; 290 -.BA $C000 ; START: SYS 49152 300 -; 310 - LDA #0 ; KEINEN 320 - JSR SETNAM ; FILENAMEN 330 -; 340 - LDA #4 ; LOG. FILENUMMER =4 350 - TAX ; GERAETEADRESSE 4 360 - LDY #0 ; SEKUNDAERADRESSE 0 370 - JSR SETLFS ; PARAMETER SETZEN 380 -; 390 - JSR OPEN ; FILE OEFFNEN 400 -; 410 - LDX #4 ; FILENUMMER 4 420 - JSR CHKOUT ; AUSGABE AUF DRUCKER LENKEN 430 -; 440 -...PRINT (TEXT) ; TEXT AUSGEBEN 450 -; 460 - JSR CLRCHN ; WIEDER BILDSCHIRMAUSGABE 470 -; 480 -...PRINT (TEXT) ; JETZT AUF BILDSCHIRM 490 -; 500 - LDA #4 ; LOG. FILENUMMER 4 510 - JMP CLOSE ; FILE SCHLIESSEN 520 -; & PROGRAMM BEENDEN 530 -; 540 -TEXT .TX "DIESER TEXT WIRD AUF" 550 -.TX " DEN DRUCKER AUSGEGEBEN !" 560 -.BY 13,13,13,0 ; 3 * CAR.RETURN
4. Unterprogramme
Ohne die Unterprogramm-Befehle JSR und RTS kommt fast kein Maschinenprogramm aus. Es ist allerdings ziemlich unbekannt, daß beide Befehle das Programm stark verlangsamen. Grund genug für uns, JSR und RTS näher zu betrachten:
Trifft der Prozessor auf JSR, schiebt er den aktuellen Programmzähler plus 2 (= Rücksprungadresse - 1) auf den Stack und springt dann zu der Adresse, die hinter JSR steht. Trifft er auf RTS, holt er die Adresse vom Stapel zurück, erhöht sie um 1 und verwendet sie wieder als Programmzähler.
Bemerkenswert ist, daß die Zugriffe auf den Stapel sich in keiner Weise von den Zugriffen über die Befehle PHA und PLA unterscheiden. Daher muß jedesmal der Stapelzeiger neu errechnet werden. Diese vielen Operationen sind schuld daran, daß JSR und RTS so langsam sind.
Da wir das Problem erkannt haben, können wir damit beginnen, unser Wissen anzuwenden.
a) Unterprogrammverschachtelung
Stellen wir uns folgendes Beispiel vor: ein Hauptprogramm ruft das Unterprogramm 1 auf. Dieses ruft an seinem Ende das Unterprogramm 2 auf, um dann mit RTS ins Hauptprogramm zurückzukehren.
Alles ziemlich schwierig, oder?
Deshalb gehen wir mit Hilfe einer Grafik vor: In Bild 1 sehen Sie ein Flußdiagramm nach obigem Aufbau. In der Beschriftung soll »Code« nicht »Kennwort« bedeuten, sondern heißt einfach »Befehlsnummer«.

Wie an den Pfeilen zu erkennen ist, werden zwei RTS-Befehle hintereinander abgearbeitet (von Unterprogramm 2 nach Unterprogramm 1 und von dort zum Hauptprogramm). Dies ist immer ein Indiz dafür, daß das Programm noch optimiert werden kann.
Eine »Übersetzung« von Bild 1 in Assembler ist Listing 6: Wenn Sie dieses über »SYS 49152« starten, ist aus den ausgegebenen Texten ersichtlich, welcher Programmteil wann abgearbeitet wird.
100 -.LI 1,3,0 110 -.BA $C000 ; START: SYS 49152 120 -; 130 -; UNTERPROGRAMMVERSCHACHTELUNG IN ASSEMBLER 140 -; 150 -.GL STROUT = $AB1E 160 -; 170 -.MA PRINT (ADRESSE) 180 - LDA #<(ADRESSE) 190 - LDY #>(ADRESSE) 200 - JSR STROUT 210 -.RT 220 -; 230 -; --------------- HAUPTPROGRAMM 240 -; 250 -...PRINT (TEXT1) 260 -; 270 - JSR UP1 280 -; ^ AUFRUF VON UNTERPROGRAMM 1 290 -; 300 -...PRINT (TEXT2) 310 -; 320 - JMP $A474 ; WARMSTART 330 -; 340 -; 350 -; --------------- UNTERPROGRAMM 1 360 -; 365 -UP1 NOP ; BELIEBIGER CODE 370 -...PRINT (TEXT3) 380 -; 390 - JSR UP2 400 -; ^ AUFRUF VON UNTERPROGRAMM 2 410 -; 420 - RTS ; UP1 VERLASSEN 430 -; 440 -; 450 -; --------------- UNTERPROGRAMM 2 460 -; 465 -UP2 NOP ; BELIEBIGER CODE 470 -...PRINT (TEXT4) 480 -; 490 - RTS ; UP2 VERLASSEN 500 -; 10000 -; 10010 -; --------------- TEXTE 10020 -; 10030 -TEXT1 .TX "HIER IST DAS HAUPTPROGRAMM." 10040 -.BY 13,13 ; 1 LEERZEILE 10050 -.BY 0 ; ENDMARKIERUNG 10060 -; 10070 -TEXT2 .TX "HIER IST WIEDER DAS HAUPTPROGRAMM." 10080 -.BY 13,13,0 10090 -; 10100 -TEXT3 .TX "HIER IST DAS UNTERPROGRAMM 1." 10110 -.BY 13,13,0 10120 -; 10130 -TEXT4 .TX "HIER IST DAS UNTERPROGRAMM 2." 10140 -.BY 13,13,0
Sobald Sie die Strukturvon Bild 1 beziehungsweise Listing 6 verstanden haben, können wir uns mit der optimierten Form befassen, die in Bild 2 beziehungsweise Listing 7 zu finden ist.

100 -.LI 1,3,0 110 -.BA $C000 ; START: SYS 49152 120 -; 130 -; UNTERPROGRAMMVERSCHACHTELUNG 140 -; (OPTIMIERTE ASSEMBLERVERSION) 150 -; 160 -.GL STROUT = $AB1E 170 -; 180 -.MA PRINT (ADRESSE) 190 - LDA #<(ADRESSE) 200 - LDY #>(ADRESSE) 210 - JSR STROUT 220 -.RT 230 -; 240 -; --------------- HAUPTPROGRAMM 250 -; 260 -...PRINT (TEXT1) 270 -; 280 - JSR UP1 290 -; ^ AUFRUF VON UNTERPROGRAMM 1 300 -; 310 -...PRINT (TEXT2) 320 -; 330 - JMP $A474 ; WARMSTART 340 -; 350 -; 360 -; --------------- UNTERPROGRAMM 1 370 -; 380 -UP1 NOP ; BELIEBIGER CODE 390 -...PRINT (TEXT3) 400 -; 410 -; 420 -; 430 -; --------------- CODE VON UNTERPROGRAMM 2 440 -; 450 -UP2 NOP ; BELIEBIGER CODE 460 - LDA #<(TEXT4) ; LOW-BYTE 470 - LDY #>(TEXT4) ; HIGH-BYTE 480 - JMP STROUT ; TEXTAUSGABE 490 -; UND RUECKSPRUNG VOM UNTERPROGRAMM, 500 -; WEIL AM ENDE DER STROUT-ROUTINE 510 -; EIN RTS-BEFEHL STEHT. 10000 -; 10010 -; 10020 -; --------------- TEXTE 10030 -; 10040 -TEXT1 .TX "HIER IST DAS HAUPTPROGRAMM." 10050 -.BY 13,13 ; 1 LEERZEILE 10060 -.BY 0 ; ENDMARKIERUNG 10070 -; 10080 -TEXT2 .TX "HIER IST WIEDER DAS HAUPTPROGRAMM." 10090 -.BY 13,13,0 10100 -; 10110 -TEXT3 .TX "HIER IST DAS UNTERPROGRAMM 1." 10120 -.BY 13,13,0 10130 -; 10140 -TEXT4 .TX "HIER IST DAS UNTERPROGRAMM 2." 10150 -.BY 13,13,0
Hier wird das ehemalige Unterprogramm 2 ans Ende von Unterprogramm 1 gehängt (wobei es ebenfalls über JMP UP2 angesprungen werden könnte). Auf diese Weise muß es nicht über JSR aufgerufen werden, was auch einen RTS-Befehl überflüssig macht.
Trotz dieser Änderung kann das Unterprogramm 2 auch weiterhin als Unterprogramm aufgerufen werden, da bei JSR UP2 die CPU auf einen RTS-Befehl trifft (Bild 2).
In Listing 7 muß noch der JMP-Befehl in Zeile 480 erläutert werden:
Dort muß nichtJSR STROUT:RTS stehen, weil am Ende der STROUT-Routine im ROM ohnehin ein RTS steht. Deshalb benötigt unser Programm keinen eigenen RTS-Befehl zur Rückkehr ins Hauptprogramm.
Die folgende Regel gilt für Aufrufe von Betriebssystemroutinen:
Voraussetzung ist, daß im Unterprogramm ab $XXXX keine Stapelmanipulation erfolgt, wie sie gleich beschrieben wird. Das geschilderte Verfahren zur Unterprogrammverschachtelung und die entsprechenden Regeln können Sie dann auf jede (!) Programmiersprache übertragen.
b) Stapelmanipulation
Wenn Sie »Exbasic Level II« kennen, wissen Sie sicher den Befehl »DISPOSE RETURN« zu schätzen. Er dient dazu, ein Unterprogramm ohne RETURN abzuschließen. Dadurch kann dieses zum Beispiel über GOTO verlassen werden.
In Assembler ist dies auch möglich. Die Befehlseingabe
PLA
PLA
entspricht in der Wirkung »DISPOSE RETURN«.
Da die Rücksprungadresse auf den Stapel abgelegt wird und dort 2 Byte in Anspruch nimmt, kann sie über PLA:PLA wieder vom Stapel geholt werden. Ein Unterprogramm ist nach PLA:PLA eigentlich kein Unterprogramm mehr, sondern Bestandteil des aufrufenden Programms. PLA:PLA findet vor allem in der Fehlerbehandlung Anwendung. An einem späteren Listing werden wir dies noch sehen. Nach PLA:PLA kann ein Unterprogramm über JMP verlassen werden. Dies machen wir uns zunutze, um den Rücksprung an eine beliebige Adresse zu simulieren. Dies ist sonst nicht möglich, da bei RTS immer hinter den Befehl gesprungen wird, der das Unterprogramm aufgerufen hat.
Ein RTS an eine beliebige Adresse müßte »RTS XXXX« heißen, doch diesen Befehl gibt es beim 6510 nicht. So wird er aber simuliert:
PLA ; holt Rücksprungadresse
PLA ; vom Stapel und
JMP $XXXX ; springt nach $XXXX
So sieht ein Makro dazu aus:
-.MA RTS (RUECKSPRUNGADRESSE)
- PLA
- PLA
- JMP RUECKSPRUNGADRESSE
-.RT
Und noch ein Mangel der Unterprogrammbefehle soll beseitigt werden: Obwohl es JMP (indirekt) gibt, kennt der 6510 keinen Befehl wie JSR (indirekt); über Stapelmanipulation ist dies dennoch möglich (siehe dazu auch im 64’er, Ausgabe 1/86: Assembler-Bedienung leicht gemacht).
Nehmen wir an, im Vektor $14/$15 steht die Adresse $C000. Nun soll über den $14/$15-Vektor ein Unterprogramm aufgerufen werden (also das ab $C000). Bild 3 zeigt, was im einzelnen geschehen muß.

Die Rücksprungadresse steht zwar in Bild 3 direkt hinter dem JMP ($0014)-Befehl, kann aber auch anderswo im Programm stehen.
Folgendes Makro ermöglicht die Simulation von JSR (indirekt):
-.MA JSRIND(VEKTOR, RUECKSPRUNGADRESSE)
- LDA #>(RUECKSPRUNGADRESSE-1)
- PHA
- LDA #<(RUECKSPRUNGADRESSE-1)
- PHA
- JMP (VEKTOR)
-.RT
Diese Simulation von JSR ($XXXX) verwendet auch der SYS-Befehl (disassemblieren Sie von $E12A bis $E155 und betrachten Sie dazu Bild 3).
Zuerst holt er die Zahl nach SYS in die Adressen $14/$15, dann legt er die Rücksprungadresse ($E147) -1 auf dem Stack ab. Nun holt er die Register P, A, X, Y aus den Adressen $030F, $030C, $030D, $030E. Es folgt ein indirekter Sprung über $0014/$0015.
Nach dem Rücksprung werden die Register wieder im Speicher dort abgelegt, woher sie genommen wurden und ein Sprung ins Basic wird durchgeführt.
Später werden wir noch eine weitere Möglichkeit für JSR (ind) kennenlernen, die aber nicht auf Stapelmanipulation beruht.
c) Vergleich zwischen Unterprogramm und Makro bezüglich Geschwindigkeit
Wenn Sie den Hypra-Ass (oder einen anderen Makro-Assembler) besitzen, haben Sie die Möglichkeit, Befehlsfolgen als Makros zu definieren. Makros sind deswegen so beliebt, wei! sie den größten Vorteil von Unterprogrammen bieten, nämlich Übersichtlichkeit. Da Makros aber wie »normale« Befehle im Speicher stehen, entfällt der Aufruf über JSR und RTS. Dies ist der Grund, weshalb Makros etwas schneller (wenige Taktzyklen) als Unterprogramme sind. Das Problem, wann Makros und wann Unterprogramme vorteilhaft sind, wird später noch aufgegriffen.
5. Tabellen
lm allgemeinen Sprachgebrauch werden Tabellen als »geordnete Zusammenstellungen von Daten« verstanden. Diese Funktion haben sie auch in Computerprogrammen, wo man sie daran erkennt, daß Tabellen keinen Befehlscharakter haben.
SMON-Benutzer können mit »FT« ein Programm nach Tabellen durchsuchen lassen; dann sucht SMON im Programm nach Bytes, die nicht zu Maschinensprachebefehlen gehören.
Wozu werden nun Tabellen verwendet?
In der Regel dienen Tabellen einem Computerprogramm als »elektronischer Rechenschieber«. So wie das Kopfrechnen durch einen Rechenschieber ersetzt werden kann, weil man nur in einer geordneten Zusammenstellung von Ergebnissen das richtige suchen muß, kann ein Programm aus seinen Tabellen denselben Nutzen ziehen: die Berechnungen entfallen, die Programmierung wird einfacher.
Aus den weniger erforderlichen Berechnungen entsteht ein deutlicher Geschwindigkeitszuwachs, der Hauptvorteil von Tabellen. Wie man Tabellen einsetzt, erfahren Sie im folgenden.
a) Tabellen aus Rechenergebnissen
Noch einmal zum Rechenschieber. Es geht beim Kopfrechnen viel schneller, 4x10 auszurechnen als 4x7. Bei einem Rechenschieber besteht kaum ein Unterschied in der »Rechenzeit«.
Dementsprechend existiert fast kein Algorithmus, dessen Ausführungszeit bei unterschiedlichen Parametern immer gleich bliebe. Wer den Artikel »Dem Klang auf der Spur (5)« (64’er, Ausgabe 5/85, Seite 152 ff.) gelesen hat, weiß, welch grobe Differenzen bei Multiplikationen auftreten können.
Ersetzt (beziehungsweise unterstützt) man einen Algorithmus durch eine Multiplikationstabelle, fällt eine einheitlichere (und kürzere) Ausführungszeit an.
Für das Rechnen mit einzelnen Bits in einem Byte werden oft die Zweierpotenzen benötigt; es empfiehlt sich, diese als Tabelle anzulegen:
| 1000 - | ; Zweierpotenzen als Tabellle |
| 1010 - | ; im DOS der Floppy 1541 ab $EFE9 |
| 1020 - | ; zu finden |
| 1030 - | ; ZWEIPOT .BY2↑O, 2↑1, 2↑2, 2↑3, 2↑4, 2↑5, 2↑6, 2↑7 |
Folgende Unterroutine legt im Akkumulator den Wert 2tA ab, wobei mit A der Inhalt des Akkumulators bei Aufruf der Routine gemeint ist:
| 10000 - | ; |
| 10010 - | ; Subroutine zur Berechnung von |
| 10020 - | ; 21A (Ergebnis kommt in den Akku) |
| 10030 - | ; |
| 10040 - | TAX ; Akku in Indexregister |
| 10050 - | LDA ZWEIPOT,X; aus Tabelle einlesen |
| 10060 - | RTS ; Das war’s schon! Wer ein schnelleres und zugleich so einfaches Verfahren kennt, möge sich melden… |
| 10070 - | ZWEIPOT .BY 2↑0,2↑1,2↑2,2↑3,2↑4,2↑5,2↑6,2↑7 |
Wenn A größerals 7 ist, liefert das Programm falsche Werte. Sie können es noch erweitern, wenn Sie es für nötig halten.
b) Tabellen aus Fließkommawerten
Zu den zeitraubendsten Operationen gehört die Rechnung mit Fließkommazahlen. Daß diese selbst in Maschinenprogrammen lähmend wirkt, sehen Sie am HiRes-3-Befehl »FUNKT« (64’er, Ausgabe 3/85, Grafikkurs-Anwendung). Daher sollte man nur dann auf die Fließkommaroutinen zugreifen, wenn es unvermeidbar ist. Berechnen Sie soviele Werte wie möglich voraus, hierfür eignet sich der Direktmodus des Basic-Interpreters besonders gut! Wie Sie einen auf diese Weise berechneten Wert ins MFLPT-(Floating Point)Format umwandeln können, zeigt Ihnen der folgende Kasten.
Damit wir uns unter Zuhilfenahme präziser Fachausdrücke und Abkürzungen verständigen können, sollten Sie den Abschnitt in »Assembler ist keine Alchimie« aufmerksam lesen, der sich mit Fließkommazahlen befaßt. Nach dem Studium dieses Abschnitts sollten Ihnen Begriffe wie »MFLPT«, »FAC« oder »ARG« geläufig sein.
Im Falle der Zahl 1.23456 erhalten wir als Ergebnis:
:0805 81 1E06 0FE5…
Diese Werte legen wir folgendermaßen als Tabelle ab:
540 -BSPZAHL .BY $81, $1E, $06, $0F, $E5
Wie wir nun diese Zahl verarbeiten, zeigt Ihnen Listing 8. Das Makro (200 - 240) stützt sich auf die Interpreter-Routine MEMFAC, die eine Zahl (Adresse wird in Akku/Y-Register übergeben) vom Speicherformat MFLPT in den FAC als FLPT-Zahl schreibt und dabei die erforderliche MFLPT-—FLPT-Umwandlung durchführt.
100 -.LI 1,3,0 110 -.BA $C000 ; START: SYS 49152 120 -; 130 -; RECHNUNG MIT FLIESSKOMMAWERTEN 140 -; 150 -.GL MEMFAC = $BBA2 160 -.GL FACOUT = $AABC 170 -.GL SQRFAC = $BF71 180 -.GL LOGNAT = $B9EA 190 -; 200 -.MA HOLE (ADRESSE) ; MAKRO-DEF. 210 - LDA #<(ADRESSE); HOLT MFLPT-ZAHL 220 - LDY #>(ADRESSE); VON ADRESSE IN 230 - JSR MEMFAC ; DEN FAC 240 -.RT 250 -; 260 -; 270 -...HOLE (BSPZAHL) 280 -; 290 - JSR FACOUT ; AUSDRUCKEN 300 -; 310 -...HOLE (BSPZAHL) 320 -; 330 - JSR SQRFAC ; QUADRATWURZEL 340 -; 350 - JSR FACOUT ; AUSDRUCKEN 360 -; 370 -...HOLE (BSPZAHL) 380 -; 390 - JSR LOGNAT ; LOGARITHMUS NATURALIS 400 -; 410 - JMP FACOUT ; AUSDRUCKEN 500 -; 510 -; BEISPIELZAHL 1.23456 520 -; IM MFLPT-FORMAT 530 -; 540 -BSPZAHL .BY $81,$1E,$06,$0F,$E5 550 -;
In der Tabelle in Zeile 540 können Sie beliebige Fließkommawerte (sofern Sie diese wie angegeben berechnet haben) einsetzen, das Programm rechnet dann mit der jeweiligen Fließkommazahl, die ab BSPZAHL im MFLPT-Format steht.
Diese Zahl wird zunächst nur in den FAC geladen und der FAC wird dann ausgedruckt (270 - 290), dann wird die Zahl wieder geholt, die Wurzel berechnet und ausgegeben (310-350). Schließlich wird die Zahl wieder in den FAC geholt, der natürliche Logarithmus errechnet und auch ausgegeben (370-410).
Zur Routine FACOUT sind, außer daß sie den Inhalt des FAC ausgibt, noch zwei Bemerkungen zu machen:
- Nach der Zahl wird noch ein CARRIAGE RETURN ausgegeben.
- Nach dem Aufruf von FACOUT hat sich der Inhalt des FAC aufgrund mehrerer Divisionen durch Zehnerpotenzen verändert.
Auf das Thema »Fließkommaarithmetik« geht Texteinschub 1 noch näher ein. Dort werden auch weitere Interpreter-Routinen vorgestellt.
c) Sprungtabelle
Beim Thema »Unterprogramme« wurde Ihnen eine Methode vorgestellt, um JSR (ind) zu simulieren. Diese erweistsich in Verbindung mit einer Tabelle, in der die Sprungadressen gespeichert sind, als sehr nützlich. So kann beispielsweise eine Parallele zum Basic-Befehl ON…GOSUB ZIEL1,ZIEL2…. geschaffen werden.
Ein Beispiel: Wenn der Basic-Interpreter auf einen Basic-Befehl trifft, holt er aus der Tabelle $AOOC - $A09D die Adresse der zugehörigen Routine. Diese springt er dann durch Stapelmanipulation an.
Der SMON arbeitet genauso: Seine Sprungtabelle liegt im Bereich $C02B - $C06B.
Die Anwendung von Sprungtabellen werden wir noch ausführlich im folgenden Abschnitt d) sowie bei der Besprechung von Listing 11 behandeln.
100 -.BA $C000 ; START: SYS 49152 110 -; 120 -; ************************* 130 -; * * 140 -; * TABELLEN - BEISPIEL * 150 -; * =================== * 160 -; * * 170 -; * BY FLORIAN MUELLER * 180 -; * * 190 -; ************************* 200 -; 210 -.GL STROUT = $AB1E 220 -.GL CURSORHOME = $E566 230 -.GL GET = $FFE4 240 -.GL BASIN = $FFCF 250 -.GL BASOUT = $FFD2 260 -.GL RESET = $FCE2 ; SOFTWARE-RESET 270 -; 280 -START JSR $E544 ; = PRINT CHR$(147) 290 - LDA #0 ; TASTATURPUFFER 300 - STA 198 ; LOESCHEN 310 - STA MPT 320 -; ^ SETZT AKTUELLEN MENUEPUNKT AUF 0 330 -HSCHLEIFE JSR CURSORHOME 340 -; ^ HSCHLEIFE = HAUPTSCHLEIFE 350 - LDA #0 360 - TAX 370 -SCHLEIFE1 STA RVSTAB,X 380 - INX 390 - CPX #4 400 - BNE SCHLEIFE1 410 - LDX MPT 420 - LDA #18 ; 18 = REVERS EIN 430 - STA RVSTAB,X 440 - LDX #0 450 -; ^ SCHLEIFENZAEHLER INITIALISIEREN 460 -SCHLEIFE2 STX XSAVE ; X RETTEN 470 - LDA RVSTAB,X 480 - JSR BASOUT 490 - LDA TEXTLO,X ; ERKLAERUNG 500 - LDY TEXTHI,X ; ZUM MENUEPUNKT 510 - JSR STROUT ; AUSGEBEN 520 - LDX XSAVE ; X WIEDER HOLEN 530 - INX 540 - CPX #4 550 - BNE SCHLEIFE2 560 -; 570 -; 580 -; HIER IST DAS MENUE BEREITS AUF 590 -; DEN BILDSCHIRM AUSGEGEBEN WORDEN. 600 -; 610 -TASTE JSR GET ; TASTATURABFRAGE 620 - BEQ TASTE ; WARTEN AUF TASTENDRUCK 630 - LDX #0 640 -SCHLEIFE3 CMP TASTEN,X 650 - BEQ WEITER1 660 - INX 670 - CPX #16 680 - BNE SCHLEIFE3 690 - JMP TASTE 700 -WEITER1 TXA 710 - LSR ; DIVIDIERT AKKU- 720 - LSR ; MULATOR DURCH 4 730 - TAX 740 - LDA SP1LO,X 750 - STA SPRUNG 760 - LDA SP1HI,X 770 - STA SPRUNG+1 780 -; 790 -.EQ RUECKSPRUNG = HSCHLEIFE-1 800 -; ^ LEGT RUECKSPRUNGADRESSE DES 810 -; UNTERPROGRAMMS FEST. 820 -; 830 - LDA #>(RUECKSPRUNG) 840 - PHA 850 - LDA #<(RUECKSPRUNG) 860 - PHA 870 - JMP (SPRUNG) 880 -; 890 -; 900 -HOME LDX #0 910 - STX MPT 920 -ENDE RTS ; ENDE DES UNTERPRG 930 -; 940 -DOWN LDX MPT ; MENUEPUNKT 950 - INX ; UM 1 ERHOEHEN 960 - CPX #4 ; GROESSER ALS 3? 970 - BEQ HOME ; DANN =0 980 - STX MPT ; SONST UEBERNEHMEN 990 - RTS ; ZUR HAUPTSCHLEIFE 1000 -; 1010 -UP LDX MPT ; MENUEPUNKT 1020 - DEX ; DEKREMENTIEREN 1030 - BPL ENDUP ; > 0? 1040 - LDX #3 ; NEIN, DANN =3 1050 -ENDUP STX MPT ; UND UEBERNEHMEN 1060 - RTS ; ZUR HAUPTSCHLEIFE 1070 -; 1080 -; 1090 -EXEC PLA ; STAPELMANIPULATION 1100 - PLA 1110 - LDX MPT 1120 - LDA SP2LO,X 1130 - STA SPRUNG 1140 - LDA SP2HI,X 1150 - STA SPRUNG+1 1160 - JMP (SPRUNG) 1170 -; 1180 -; 1190 -; 1200 -ZAHLWORT LDA #<(TZAHL) ; AUFFORDERUNG 1210 - LDY #>(TZAHL) ; ZUR EINGABE 1220 - JSR STROUT ; AUSGEBEN 1230 - JSR BASIN ; HOLT ZEICHEN 1240 - SEC ; IN BINAERZAHL 1250 - SBC #"0" ; UMWANDELN 1260 - TAX ; INS X-REGISTER 1270 -; 1280 -; JETZT STEHT IM X-REGISTER 1290 -; DIE EINGEGEBENE ZAHL 1300 -; 1310 - CMP #10 ; > 10? 1320 - BCC ZAHLWORT1 ; NEIN=> WEITER 1330 - JMP ZAHLWORT ; NEUEINGABE 1340 -; 1350 -ZAHLWORT1 STX XSAVE ; X RETTEN 1360 - LDA #<(TWORT) ; AUFFORDERUNG 1370 - LDY #>(TWORT) ; ZUR EINGABE 1380 - JSR STROUT ; AUSGEBEN 1390 - LDX XSAVE ; X WIEDER HOLEN 1400 - LDA ZWLO,X ; ADRESSE DES 1410 - LDY ZWHI,X ; ZAHLWORTES HOLEN 1420 - JSR STROUT ; UND Z.WORT DRUCKEN 1430 -; 1440 -WAIT JSR GET ; WARTET AUF 1450 - BEQ WAIT ; TASTENDRUCK 1460 - JMP START ; ZUM HAUPTMENUE 1470 -; 1480 -; 1490 -; 1500 -FARBE LDA #<(TFARBE) 1510 - LDY #>(TFARBE) 1520 - JSR STROUT 1530 - LDX #0 1540 -FARBE1 JSR BASIN ; HOLT EINGABE 1550 - CMP #" " ; SPACE ? 1560 - BEQ FARBE1 ; JA=>UEBERLESEN 1570 - CMP #13 ; ENDE DER EINGABE? 1580 - BEQ FARBE2 ; JA, DANN WEITER 1590 - STA FARBWORT,X ; EINGABE SPEICHERN 1600 - INX ; ZAEHLER ERHOEHEN 1610 - JMP FARBE1 ; ZUR SCHLEIFE 1620 -FARBE2 STX 2 ; LAENGE MERKEN 1630 - LDX #0 1640 - TXA 1650 -FARBE3 ROL 1660 - EOR FARBWORT,X 1670 - INX 1680 - CPX 2 ; SCHON FERTIG? 1690 - BNE FARBE3 ; NEIN,ZUR SCHLEIFE 1700 - CLC ; LAENGE 1710 - ADC 2 ; ADDIEREN 1720 -; 1730 -; HIER STEHT IM AKKU DIE PRUEFSUMME 1740 -; 1750 - LDX #0 1760 -FARBE4 CMP PRUEFSUMMEN,X 1770 - BEQ FARBE5 ; GEFUNDEN 1780 - INX 1790 - CPX #16 1800 - BNE FARBE4 1810 - JMP FARBE ; NEUE EINGABE 1820 -FARBE5 STX 53280 ; BILDSCHIRM- 1830 - STX 53281 ; FARBE SETZEN 1840 - JMP START ; ZUM MENUE 1850 -; 10000 -; 10010 -; TABELLEN 10020 -; ======== 10030 -; 10040 -; TEXTE: 10050 -; 10060 -PUNKT0 .TX "ZAHL IN ZAHLWORT UMWANDELN" 10070 -.BY 13,13,0 10080 -; 10090 -PUNKT1 .TX "BILDSCHIRMFARBE" 10100 -.BY 13,13,0 10110 -; 10120 -PUNKT2 .TX "RESET AUSLOESEN" 10130 -.BY 13,13,0 10140 -; 10150 -PUNKT3 .TX "PROGRAMMENDE UEBER RTS" 10160 -.BY 13,13,13 10170 -.TX "BITTE AUSWAEHLEN !" 10180 -.BY 0 10190 -; 10200 -; 10210 -TASTEN .BY 133,13,"_","="; 133=F1,13=RETURN 10220 -.BY 19,"0","@",0 ; 19=HOME,0=DUMMY 10230 -.BY 17,"D",135,"+" ; 17=CRSR DOWN,135=F5 10240 -.BY 145,"U",134,"-" ; 145=CRSR UP,134=F3 10250 -; 10260 -; 10270 -TZAHL .BY 147 ; CLEAR HOME 10280 -.TX "ZAHL (0-9) ? " 10290 -.BY 0 10300 -; 10310 -TWORT .TX " IN WORTEN : " 10320 -.BY 0 10330 -; 10340 -; 10350 -; ZAHLWOERTER (0-9) 10360 -; 10370 -; 10380 -NULL .TX "NULL" 10390 -.BY 0 10400 -; 10410 -EINS .TX "EINS" 10420 -.BY 0 10430 -; 10440 -ZWEI .TX "ZWEI" 10450 -.BY 0 10460 -; 10470 -DREI .TX "DREI" 10480 -.BY 0 10490 -; 10500 -VIER .TX "VIER" 10510 -.BY 0 10520 -; 10530 -FUENF .TX "FUENF" 10540 -.BY 0 10550 -; 10560 -SECHS .TX "SECHS" 10570 -.BY 0 10580 -; 10590 -SIEBEN .TX "SIEBEN" 10600 -.BY 0 10610 -; 10620 -ACHT .TX "ACHT" 10630 -.BY 0 10640 -; 10650 -NEUN .TX "NEUN" 10660 -.BY 0 10670 -; 10680 -; 10690 -TFARBE .BY 147 ; CLEAR HOME 10700 -.TX "WELCHE FARBE ? " 10710 -.BY 0 10720 -; 10730 -; 10740 -RVSTAB .BY 0,0,0,0 ; 4 BYTES RESERVIEREN 10750 -; 10760 -; 10770 -; ZAHLEN: 10780 -; 10790 -; ADRESSEN DER TEXTE, DIE DIE 10800 -; MENUEPUNKTE BESCHREIBEN 10810 -; 10820 -TEXTLO .BY <(PUNKT0),<(PUNKT1) 10830 -.BY <(PUNKT2),<(PUNKT3) 10840 -; 10850 -TEXTHI .BY >(PUNKT0),>(PUNKT1) 10860 -.BY >(PUNKT2),>(PUNKT3) 10870 -; 10880 -; 10890 -; ADRESSEN DER ZAHLWOERTER 10900 -; 10910 -ZWLO .BY <(NULL),<(EINS),<(ZWEI),<(DREI) 10920 -.BY <(VIER),<(FUENF),<(SECHS),<(SIEBEN) 10930 -.BY <(ACHT),<(NEUN) 10940 -; 10950 -ZWHI .BY >(NULL),>(EINS),>(ZWEI),>(DREI) 10960 -.BY >(VIER),>(FUENF),>(SECHS),>(SIEBEN) 10970 -.BY >(ACHT),>(NEUN) 10980 -; 10990 -; 11000 -; ADRESSEN DER UNTERROUTINEN 11010 -; FUER DIE MENUESTEUERUNG 11020 -; 11030 -SP1LO .BY <(EXEC),<(HOME),<(DOWN),<(UP) 11040 -; 11050 -SP1HI .BY >(EXEC),>(HOME),>(DOWN),>(UP) 11060 -; 11070 -; 11080 -; ADRESSEN DER EINZELNEN 11090 -; MENUEPUNKTE 11100 -; 11110 -SP2LO .BY <(ZAHLWORT),<(FARBE) 11120 -.BY <(RESET),<(ENDE) ; BEI ENDE STEHT 11130 -SP2HI .BY >(ZAHLWORT),>(FARBE) 11140 -.BY >(RESET),>(ENDE) ; EIN RTS-BEFEHL 11150 -; 11160 -; PRUEFSUMMEN DER FARB-WOERTER 11170 -; 11180 -PRUEFSUMMEN.BY 41,158,137,212,159,101 11190 -.BY 3,2,33,69,201,116,113,121,127,114 11200 -; 11210 -; 11220 -; ZWISCHENSPEICHER 11230 -; 11240 -MPT .BY 0 ; 1 BYTE RESERVIEREN 11250 -XSAVE .BY 0 11260 -SPRUNG .WO 0 ; 2 BYTES FREIHALTEN 11270 -FARBWORT .BY 0 11280 -; ^ AB 'FARBWORT' WIRD DIE EINGABE 11290 -; DER FARB-BEZEICHNUNG ABGELEGT.
d) Vergleichstabellen
Weder der SMON noch der Basic-Interpreter benutzen zum Suchen der zum jeweiligen Befehl gehörenden Routine eine Reihe von CMP-Abfragen mit BRANCH-Befehlen. Auch für die Vergleichswerte (in diesem Fall die Befehlswörter) gibt es eine Tabelle: Beim SMON liegt sie im Bereich $COOB -$C02A, beim Basic-Interpreter $A09E - $A327.
Sprung- und Vergleichstabellen sind in gleicher Befehlsfolge angeordnet; wird der Befehl an einer bestimmten Stelle in der Vergleichstabelle gefunden, erfolgt ein Sprung an die Adresse, die an gleicher Stelle in der Sprungtabelle steht. So sehen die Befehls- und Vergleichstabellen im SMON aus:
| Spalte Nr. | 1 | 2 | 3 | 4 | … |
| Befehl | / | # | $ | % | … |
| Sprungadr. $ | CADB | C920 | C908 | C91C | … |
Die Sprungadressen sind wegen der Stapelmanipulation in der Tabelle ab $C02B um 1 dekrementiertgespeichert; in der Darstellung sehen Sie aber das tatsächliche Sprungziel.
Wir werden jetzt anhand des SMON die Verwendung einer Vergleichs-Sprungtabelle in Assembler erläutern.
Wenn wir die zum Befehl » # « gehörende Sprungadresse finden wollen, gehen wir folgendermaßen vor:
- Wir suchen in Reihe 2 das # -Zeichen.
- Wir gehen (in derselben Spalte) eine Reihe nach unten und finden dort die Sprungadresse ($C92C).
Der Computer hat nicht die Möglichkeit, direkt eine Reihe weiter unten die Suche fortzusetzen. Er muß einen Umweg wählen und sich die Spalte merken. Ein Beispiel:
- Der SMON sucht unter den Elementen aus Reihe 2 das »#«. In einem Zähler merkt er sich die Spalte, in der der Befehl gefunden wurde.
- Nun sucht er in Reihe 3 in der Spalte, die im Zähler steht, die zugehörige Sprungadresse.
Wie ähnlich beide Suchvorgänge sind, erkennen Sie daran, daßjedesmal die Hauptschritte 1. und 2. vorkommen.
Nach so viel Theorie sehen wir uns nun umso ausführlicher die Routine im SMON an, die für die Steuerung der Vergleichs-Befehlstabelle verantwortlich ist. Dazu können Sie »D C303 C323« eingeben.
Bei Adresse $C303 steht im Akku der ASCII-Code des Kommandos, das der SMON ausführen soll (zum Beispiel $40, wenn ein M-Befehl eingegeben wurde).
| C303 | LDX #$20 | 32–1 Befehle müssen durchsucht werden. Weshalb »–1« erforderlich ist, liegt an der Schleifenstruktur und ist unbedeutend. |
| C305 | CMP $C00A,X | Akku (enthält Befehl) mit X-tem Element der Befehlstabelle vergleichen; $C00A = Befehlstabelle –1, weil Adresse $C00A nie zum Vergleich herangezogen wird. |
| C308 | BEQ $C30F | Vergleich positiv; im X-Register steht jetzt die Spalte. |
| C30A | DEX | Zähler wird dekrementiert; es handelt sich hier um eine »Dekrementierschleife« (dieses Thema wird noch behandelt). |
| C30B | BNE $C305 | Wenn der Zähler noch nicht gleich 0 ist, folgt ein Sprung zum Schleifenbeginn. |
| C30D | BEQ $C2D1 | Wenn X=0, dann wurde die ganze Tabelle durchsucht, und der Befehl nicht gefunden! Deshalb wird in die SMON-Fehlerbehandlung gesprungen. |
| C30F | JSR $C315 | Diese Stelle wird von $C308 aus angesprungen; hier wiederum steht ein Aufruf des Unterprogramms ab $C315, das etwas weiter unten besprochen wird. |
| C312 | JMP $C2D6 | Nachdem nun der Befehl durch die Subroutine $C315 abgearbeitet wurde, folgt ein Sprung zur Eingabe des nächsten Befehls. |
| C30A | DEX | Zähler wird dekrementiert; es handelt sich hier um eine »Dekrementierschleife« (dieses Thema wird noch behandelt). |
| C308 | BNE $C305 | Wenn der Zähler noch nicht gleich 0 ist, folgt ein Sprung zum Schleifenbeginn. |
| C30D | BEQ $C2D1 | Wenn X=0, dann wurde die ganze Tabelle durchsucht, und der Befehl nicht gefunden! Deshalb wird in die SMON-Fehlerbehandlung gesprungen. |
| C30F | JSR $C315 | Diese Stelle wird von $C308 aus angesprungen; hier wiederum steht ein Aufruf des Unterprogramms ab $C315, das etwas weiter unten besprochen wird. |
| C312 | JMP $C2D6 | Nachdem nun der Befehl durch die Subroutine $C315 abgearbeitet wurde, folgt ein Sprung zur Eingabe des nächsten Befehls. |
| C315 | TXA | Das ist sie, die Subroutine! Weil im X-Register die Nummer des Befehls (= Spalte in Tabelle) steht, kommt das X-Register ins Hauptrechenregister. |
| C316 | ASL | Die Befehlsnummer wird mit 2 multipliziert... |
| C317 | TAX | und kommt wieder ins X-Register. Die Multiplikation mit 2 ist erforderlich, weil in der Sprungtabelle ein Element doppelt so lang ist, wie in der Vergleichstabelle, nämlich 2 Byte. Die Sprungadressen belegen deshalb 2 Byte, weil sie aus Low- und High-Bytes bestehen. |
| C318 | INX | Das X-Register wird um 1 erhöht, da das High-Byte eine Position hinter dem Low-Byte steht. |
| C319 | LDA $C029,X | High-Byte wird gelesen. Die Sprungtabelle beginnt zwar 2 Byte nach $C029, aber weil es keine Spalte 0 gibt, muß der Speicherbedarf einer Sprungadresse (=2) abgezogen werden. |
| C31C | PHA | Das High-Byte der Adresse wird auf den Stapel gelegt. |
| C31D | DEX | –1, weil Low-Byte eine Adresse vor High-Byte steht. |
| C31E | LDA $C029,X | Nun wird auch das Low-Byte der Adresse |
| C321 | PHA | auf den Stapel geschoben. |
| C322 | RTS | Der Befehl RTS wird hier zur Simulation von JMP (ind) verwendet. Auf dieses (unpraktische) Verfahren soll nicht weiter eingegangen werden, weil der 6510 den Befehl JMP (ind) kennt. Wichtig ist für uns nur, daß jede SMON-Routine mit einem RTS abgeschlossen wird, dann erfolgt ein Rücksprung zur Adresse $C312. |
Damit haben wir SMONs Schleife zum Suchen eines Befehls und dessen Routine durchleuchtet. Sofern Sie ein ROM-Listing zur Verfügung haben, können Sie sich zusätzlich die entsprechenden Stellen im Basic-Interpreter an|sehen. Dieser aber benötigt wegen seiner unterschiedlich langen Befehle einen etwas komplizierteren Suchalgorithmus, was wiederum zu erheblich höherer Ausführungszeit beiträgt.
6. Vergleiche von Prüfsummen
Nun lernen wir ein besonders raffiniertes Vergleichsverfahren kennen:
Wie gesagt, benötigen Vergleiche mit Wörtern, die aus unterschiedlich vielen Zeichen bestehen, mehr Taktzyklen. Dies wäre nicht so, wenn wir alle Zeichen auf eine einheitliche Länge bringen würden. Genau dies tut der Basic-Interpreter: Bei Eingabe einer Zeile wandelt er alle Basic-Befehlswörter in Token um. Jedes Token vertritt einen Befehl und kann, da es nur ein Byte benötigt, schneller erkannt werden, als es bei mehreren Bytes möglich wäre.
Ein Nachteil ist jedoch der Speicherplatzaufwand; für die Umwandlung müssen die Befehle irgendwo im Speicher in Langform vorhanden sein.
Es gibt aber noch ein anderes Verfahren, einer Zeichenkette einen Wert zuzuweisen: Die Prüfsummenberechnung. Diese führen zum Beispiel die Eingabehilfen »Checksummer« und »MSE« durch: Aus 8 Byte Programmcode und 2 Byte Adresse errechnet der MSE eine 1 Byte Prüfsumme.
In Bild 4 sehen Sie einen sehr zuverlässigen Algorithmus zur Berechnung von Prüfsummen (insofern zuverlässig, als er sehr unterschiedliche Prüfsummen ermittelt). Listing 9 stellt ein Hilfsprogramm dar, das zu einer Eingabe die Prüfsumme nach dem Algorithmus aus Bild 4 errechnet.

100 -.LI 1,3,0 110 -.BA $C000 ; START: SYS 49152 120 -; 130 -.GL BASIN = $FFCF 140 -.GL NUMOUT = $BDCD 150 -.GL STROUT = $AB1E 160 -; 170 -ANFANG LDA #<(TEXT1) 180 - LDY #>(TEXT1) 190 - JSR STROUT 200 -; 210 - LDX #0 220 -SCHLEIFE1 JSR BASIN 230 - CMP #13 ; 13 = RETURN 240 - BEQ WEITER 250 - STA STORE,X 260 - INX 270 - JMP SCHLEIFE1 280 -; 290 -WEITER STX LAENGE 300 - LDA #<(TEXT2) 310 - LDY #>(TEXT2) 320 - JSR STROUT 330 - LDA #0 340 -; 0 = AUSGANGSWERT DER PRUEFSUMME 350 - TAX ; ZAEHLER = 0 360 -SCHLEIFE2 ROL ; PRUEFSUMME * 2 370 - EOR STORE,X 380 - INX ; ZAEHLER ERHOEHEN 390 - CPX LAENGE 400 - BNE SCHLEIFE2 410 - CLC 420 - ADC LAENGE ; LAENGE ADDIEREN 430 - TAX ; PRUEFSUMME 440 - LDA #0 ; AUSGEBEN 450 - JSR NUMOUT 460 - JMP ANFANG ; NOCH EINMAL 1000 -; 1010 -; TEXTE 1020 -; 1030 -TEXT1 .BY 13 1040 -.TX "----------------------------------------" 1050 -.TX "EINGABE ? " 1060 -.BY 0 1070 -; 1080 -TEXT2 .BY 13 1090 -.TX "PRUEFSUMME " 1100 -.BY 0 2000 -; 2010 -; ZWISCHENSPEICHER 2020 -; 2030 -LAENGE .BY 0 ; ZWISCHENSPEICHER 2040 -STORE .BY 0 2050 -; ^ AB STORE WIRD DIE EINGABE ABGELEGT
In Listing 9 ist Ihnen eventuell die Routine NUMOUT nicht bekannt. Daher eine Kurzbeschreibung: NUMOUT gibt eine positive Integerzahl, die im Akkumulator (High-Byte) und im X-Register (Low-Byte) übergeben wird, aus. NUMOUT wird zum Beispiel von der LIST-Routine bei der Ausgabe einer Zeilennummer aufgerufen.
Die Routine BASIN soll ebenfalls erklärt werden, da sie in allen folgenden Programmen verwendet werden wird. Wenn die Routine BASIN zum ersten Mal aufgerufen wird, erwartet das Betriebssystem eine Eingabe (normalerweise von Tastatur), die der Eingabe einer Basic-Zeile entspricht. Nach der Eingabe wird das erste eingegebene Byte in den Akku geladen, jeder weitere Aufruf von BASIN holt das nächste Zeichen in den Akku. Wurden alle Bytes eingelesen, wird im Akku der Wert 13 ($0D, RETURN) übergeben. Danach führt ein weiterer Aufruf von BASIN zu erneuter Eingabe von Tastatur.
Ein großer Vorteil von Prüfsummen ist, daß die Vergleiche mit nur einem Byte, nämlich der Prüfsumme, durchgeführt werden müssen.
Wie man in den Genuß dieses Vorteils kommt, zeigt Listing 10. Wenn Sie den Namen eines Computers (C 64, VC 20, PC 128 oder AMIGA) eingeben, nennt das Programm den in diesem Computer installierten Mikroprozessor. Bei der Eingabe der Computernamen kann man aufgrund der Zeilen 230 und 248 beliebig viele Leerzeichen eingeben. Bei der Errechnung der Prüfsummen mit Listing 9 dürfen allerdings keine eingegeben werden, da Listing 9 diese nicht überliest und somit ein falsches Ergebnis liefern würde.
100 -.LI 1,3,0 110 -.BA $C000 ; START: SYS 49152 120 -; 130 -.GL BASIN = $FFCF 140 -.GL NUMOUT = $BDCD 150 -.GL STROUT = $AB1E 160 -; 170 -ANFANG LDA #<(TEXT1) 180 - LDY #>(TEXT1) 190 - JSR STROUT 200 -; 210 - LDX #0 220 -SCHLEIFE1 JSR BASIN 230 - CMP #" " ; SPACE? 240 - BEQ SCHLEIFE1 ; DANN UEBERLESEN 250 - CMP #13 ; 13 = RETURN 260 - BEQ WEITER1 270 - STA STORE,X 280 - INX 290 - JMP SCHLEIFE1 300 -; 310 -WEITER1 STX LAENGE 320 - LDA #<(TEXT2) 330 - LDY #>(TEXT2) 340 - JSR STROUT 350 - LDA #0 360 -; 0 = AUSGANGSWERT DER PRUEFSUMME 370 - TAX ; ZAEHLER = 0 380 -SCHLEIFE2 ROL ; PRUEFSUMME * 2 390 - EOR STORE,X 400 - INX ; ZAEHLER ERHOEHEN 410 - CPX LAENGE 420 - BNE SCHLEIFE2 430 - CLC 440 - ADC LAENGE ; LAENGE ADDIEREN 450 -; HIER STEHT DIE PRUEFSUMME IM AKKU 460 -; 470 - LDX #0 480 -SCHLEIFE3 CMP PRUEFSUMMEN,X 490 - BEQ WEITER2 500 - INX 510 - CPX #4 520 - BNE SCHLEIFE3 530 -; PRUEFSUMME NICHT GEFUNDEN 540 -; 550 - PLA 560 - PLA 570 - LDA #<(TEXT3) 580 - LDY #>(TEXT3) 590 - JSR STROUT 600 - JSR ANFANG ; VON VORNE 610 -; 620 -WEITER2 LDA LOWTAB,X ; LOW-BYTE 630 - LDY HIGHTAB,X ; HIGH-BYTE 640 - JSR STROUT 650 - JMP ANFANG ; NOCH EINMAL! 660 -; 1000 -; 1010 -; TEXTE 1020 -; 1030 -TEXT1 .BY 13 1040 -.TX "----------------------------------------" 1050 -.TX "COMPUTER : " 1060 -.BY 0 1070 -; 1080 -TEXT2 .BY 13 1090 -.TX "PROZESSOR: " 1100 -.BY 0 1110 -; 1120 -TEXT3 .TX "WEISS ICH NICHT!" 1130 -.BY 0 1140 -; 1150 -; 1160 -T6502 .TX "MOS 6502" 1170 -.BY 0 1180 -; 1190 -T6510 .TX "MOS 6510" 1200 -.BY 0 1210 -; 1220 -T8502 .TX "MOS 8502 & Z80" 1230 -.BY 0 1240 -; 1250 -T68000 .TX "MOTOROLA 68000" 1260 -.BY 0 1270 -; 2000 -; 2010 -; NUMERISCHE TABELLEN 2020 -; 2030 -LOWTAB .BY <(T6502),<(T6510),<(T8502),<(T68000) 2040 -HIGHTAB .BY >(T6502),>(T6510),>(T8502),>(T68000) 2050 -; 2060 -PRUEFSUMMEN.BY 228,83,149,136 2070 -; REIHENFOLGE: VC20,C64,PC128,AMIGA 3000 -; 3010 -; ZWISCHENSPEICHER 3020 -; 3030 -LAENGE .BY 0 ; ZWISCHENSPEICHER 3040 -STORE .BY 0 3050 -; ^ AB STORE WIRD DIE EINGABE ABGELEGT
Der Programmteil, der die Prüfsumme der Eingabe berechnet, ist mit Ausnahmen der Zeilen 230/240 aus Listing 9 übernommen worden. Nach Zeile 450 wird die ermittelte Prüfsumme mit der Tabelle »PRÜFSUMMEN« (Zeile 2060) verglichen.
Bei »WEITER2« (Zeile 620) steht im X-Register die Spalte, in der die Prüfsumme gefunden wurde. Listing 10 numeriert, im Gegensatz zum SMON die Spalten mit 0 (statt mit 1) beginnend. Außerdem wurde die Adressentabelle in »LOWTAB« (Tabelle der Low-Bytes) und »HIGHTAB« (High-Bytes) zerlegt, was die Programmierung stark erleichtert.
Wir würden zwar Spalten von 1 an numerieren, für den Computer ist es aber besser, mit Spalte 8 zu beginnen. Wenn im X-Register die Spalte (0: VC 20,1: C 64, 2: PC 128, 3: AMIGA) steht, lesen die Zeilen 620/630 aus einer Tabelle die Adresse, ab der die ASCII-Darstellung des Prozessors zu finden ist. Weil jede der Tabellen »LOWTAB« und »HIGHTAB« gleich viele Elemente wie die Tabelle »PRUEFSUMMEN« hat, muß keine komplizierte Umwandlung über Multiplikation mit 2 oder ähnliches erfolgen wie beispielsweise beim SMON.
Auf eine akute Gefahr bei der Verwendung von Prüfsummen soll jetzt hingewiesen werden: die »Überschneidung von Prüfsummen«:
So wie unterschiedliche Basic-Zeilen beim Checksummer eine gleiche Prüfsumme haben können, sind Prüfsummen nie eindeutig.
Wenn Sie bei Listing 10 etwas herumprobieren, werden Sie sicher feststellen, daß auch eigentlich nicht vorgesehene Eingaben Wirkung zeigen. Dies liegt daran, daß diese Eingaben die gleiche Prüfsumme wie die Taste »VC 20«. »C 64«, »PC 128« oder »AMIGA« haben. Daher sollte man immer darauf achten, daß sich die vorgesehenen Eingaben nicht in ihren Prüfsummen überschneiden (das heißt, die gleichen Prüfsummen haben). Wenn man dies aber beachtet, so ist das Arbeiten mit Prüfsummen, vor allem bei kleineren Datenmengen, eine angenehme Sache.
e) Beispielprogramm für Tabellen
Wenden wir uns jetzt einem etwas größeren (aber keineswegs komplizierteren) Programm zu. Es heißt schlicht und einfach »TABELLEN-BEISPIEL«, womit schon einiges über die Funktion ausgesagt ist: ein reines Beispielprogramm, das nicht den Anspruch erhebt, etwa als Anwendersoftware nützlich zu sein. In Listing 11 finden Sie den kommentierten Quelltext.
Zuerst soll die Bedienung des Programms erläutert werden. Gestartet wird »TABELLEN-BEISPIEL« durch SYS 49152, worauf man sich in folgendem Menü befindet:
| ZAHL IN ZAHLWORT WANDELN | (0) |
| BILDSCHIRMFARBE | (1) |
| RESET AUSLOESEN | (2) |
| PROGRAMMENDE UEBER RTS | (3) |
| BITTE AUSWAEHLEN! |
Die Zahlen in Klammern sehen Sie nicht, diese zeigen nur die interne Numerierung der Menüpunkte an.
Der jeweils angewählte Menüpunkt (unmittelbar nach dem Start: 0) wird im Gegensatz zu den anderen revers hervorgehoben.
Der angewählte Menüpunkt kommt durch Drücken von F1,RETURN, »—« - oder »=«-Taste zur Ausführung.
Wollen Sie einen anderen Menüpunkt anwählen, drücken Sie einfach CRSR DOWN, »D«,F5 oder »+«, um den invertierten Bereich nach unten zu bewegen. Weiter nach oben gelangen Sie über CRSR UP, »U«, F3 oder »-«.
Wenn Sie von »3« aus nach unten wollen, geht es wieder bei »0« los; von »0« nach oben führt auf Punkt »3«.
Auf Punkt »0« (Ausgangseinstellung) kommen Sie über HOME,»0« oder Klammeraffe.
Sicher würden Sie Ihre Programme auch gerne mit einem solch komfortablen Menü aufwerten. Wenn Sie die Beschreibung des Quelltextes gut durchlesen, wird dies keine Schwierigkeiten bereiten.
Nun zu den einzelnen Menüpunkten.
»2« (Reset auslösen) springt in die RESET-Routine ab $FCE2. »3« (Programmende über RTS) bewirkt einen Rücksprung ins Basic. Wenn Sie aber »TABELLEN-BEISPIEL« vom Hypra-Ass aus gestartet haben, finden Sie sich im »AUTO-NUMBER«-Modus wieder. Dies ist weder ein Fehler von »TABELLEN-BEISPIEL« noch von Hypra-Ass, sondern liegt daran, daß beide Programme eine bestimmte Adresse verwenden, die Hypra-Ass dann als Aufforderung zur automatischen Zeilennumerierung wertet. Am besten starten Sie »TABELLEN-BEISPIEL« nur vom normalen Basic aus.
Punkt »0« bittet Sie um Eingabe einer Zahl von 0 bis 9 und gibt zur eingegebenen Zahl das Zahlwort aus. Beispiel: Eingabe »0«, Ausgabe »NULL«.
Danach müssen Sie eine Taste drücken, um ins Hauptmenü zu kommen.
Punkt »1« schließlich bietet die Möglichkeit, die Hintergrundfarbe besonders elegant einzustellen: Sie geben einfach die Farbe als Wort ein, zum Beispiel SCHWARZ.
Folgende Eingaben sind vorgesehen:
SCHWARZ, WEISS, ROT, TUERKIS, VIOLETT, GRUEN, BLAU, GELB, ORANGE, BRAUN, HELLROT, GRAU 1, GRAU 2, HELLGRUEN, HELLBLAU, GRAU 3
Aufgrund der Überschneidung von Prüfsummen zeigen jedoch auch andere Eingaben Wirkung, zum Beispiel:
SCH, HYPRAASS, PRINT, COMPUTER-GRAPHIK, TAGESSCHAU
Nun wollen wir uns mit dem Quelltext befassen.
Ab Zeile 10000 finden Sie die Tabellen. Und weil unser Programm ein Beispiel für die Verwendung von Tabellen sein soll, sind es derer recht viele. Die wichtigsten davon sind jedoch analog der internen Numerierung der Menüpunkte aufgebaut, da sie Daten für die Menüsteuerung beinhalten. Diese Tabellen sind auch mit 0 - 3 numeriert und grafisch in Bild 6 dargestellt.

Sehen wir uns wieder den Quelltext, beginnend mit der ersten Zeile, an.
Auf die Symboldefinitionen (210 - 260) folgt die Initialisierung der Hauptschleife (280 - 310). Diese Initialisierung löscht Bildschirm (280) und Tastaturpuffer (290 - 300). Außerdem wird der aktuelle (= derzeit invers dargestellte) Menüpunkt (immer in der Adresse »MPT« enthalten) auf 0 gesetzt (310). Zeile 310 ist also dafür verantwortlich, daß nach dem Start über SYS 49152 das Inversfeld ganz oben steht (auf Punkt 0).
Die Texte, die der Beschreibung der Menüpunkte dienen, werden in der Hauptschleife »HSCHLEIFE« (350 - 550) ausgegeben. Mit dieser wollen wir uns nun eingehend auseinandersetzen.
Zunächst wird die Tabelle »RVSTAB« gelöscht (350 - 400). Diese Tabelle enthält die Information, ob der erläuternde Text zu einem Menüpunkt invers ausgegeben wird. Wenn nein, so enthält das entsprechende Byte eine »0«, andernfalls eine »18« (= REVERS-ON-Code für Betriebssystem). Das entsprechende Byte aus »RVSTAB« braucht nur vor dem Menüpunkt-Text ausgegeben werden (470- 480). Die Zeilen 410- 430 sorgen dafür, daß das Byte in »RVSTAB«, welches sich auf den aktuellen Menüpunkt bezieht, den RVS-ON-Code erhält.
In der Hauptschleife muß das X-Register in »XSAVE« gesichert werden, weil die Routine »STROUT« den Inhalt des X-Registers ändert.
Mit »TASTE« (610) beginnt dann die Tastaturabfrage im Menü. Die Routine »GET« holt ein Zeichen von der Tastatur als ASCII-Code in den Akku. Wurde keine Taste gedrückt, erhält der Akku den Code 0. In diesem Fall wartet 620 auf eine neue Eingabe. Beachten Sie bitte, daß der Akku nach der Zeile 620 NIE den Wert 0 haben kann (dies wird sich bald als nützlich erweisen)!
Wurde nun eine Taste gedrückt, sucht »SCHLEIFE« (630 -680) in der Tabelle »TASTEN«, die im Quelltextab Zeile 10210 steht, nach dem eingegebenen Zeichen (wird es nicht gefunden, erfolgt in 690 der Sprung zur neuen Eingabe).
Diese Tabelle »TASTEN« enthält alle vorgesehenen Tastendrücke zur Menüsteuerung, die in 4er-Blockweise angeordnet sind (Bild 5). Nach der Suchschleife steht im X-Register die Position der gedrückten Taste innerhalb der Tabelle »TASTEN« (zum Beispiel 0 = F1 gedrückt, 4 = HOME gedrückt). Diese Position wird - ohne Berücksichtigung des Divisions-Restes - durch 4 dividiert (700 - 730), um festzuhalten, von welchem Tastenblock eine Taste gedrückt wurde.

Dadurch ist eindeutig bestimmt, welche Befehlsgruppe aufgerufen werden muß.
Steht nach 730 im X-Register 0, wurde eine der ersten vier in »TASTEN« enthaltenen Tasten gedrückt, die die Ausführung des aktuellen Menüpunktes veranlassen (Zeile 10210 und Bild 5). Ist X=1, so wurde eine Taste aus Zeile 10220 gedrückt. In 10220 stehtals letztes Byte eine 0. Diese dient, da für die Funktion »Inversfeld in HOME-Position« nur drei Tastendrücke vorgesehen wurden, zum Auffüllen auf vier Tasten. 0 kann hier bedenkenlos als Dummy (Füllbyte ohne wirkliche Bedeutung) stehen, da der Akku aufgrund von 620 nie den Wert 0 annehmen wird.
Beinhaltet X nach der Division durch 4 den Wert2, wird das Inversfeld nach unten bewegt, istX=3, dann nach oben. Dies können Sie sich an Bild 6 veranschaulichen.
An den Zeilen 740 - 870 sehen wir nun die Verwendung einer Sprungtabelle. Unsere Sprungtabelle ist »SP1LO/SP1HI«. »SP1LO« beinhaltet die Low-, »SP1HI« die High-Bytes der anzuspringenden Routinen. In den Vektor »SPRUNG« wird einfach die Zieladresse geschrieben (740 - 770).
Die Zuweisungszeile 790 errechnet die Rücksprungadresse des aufzurufenden Unterprogramms. Bei einem RTS soll nämlich zur »HSCHLEIFE« gesprungen werden.
Diese Rücksprungadresse »RUECKSPRUNG« wird auf den Stapel gelegt (830 - 860), zuletzt erfolgt der indirekte Sprung (870). Die über die soeben beschriebene Simulation von JSR (ind) angesprungenen Routinen finden Sie ab Zeile 900. Es wird einfach der aktuelle Menüpunkt »MPT« entsprechend dem Tastendruck geändert, dann wird zur »HSCHLEIFE« gesprungen, die auch die Tabelle »RVSTAB« entsprechend anpaßt.
»EXEC« (1090) holt die Rücksprungadresse vom Stapel (1090 - 1100), da diese Routine nicht als Unterprogramm behandelt werden soll.
Die Zeile 1110 holt den angeforderten Menüpunkt ins X-Register. Dann wird aus »SP2LO/SP2HI« die Adresse der zum Menüpunkt gehörenden Routine geholt und diese über einen gewöhnlichen indirekten Sprung aufgerufen (1160).
Als Routine zu »2« wird einfach die RESET-Routine des Betriebssystems angesprungen, für »3« eignet sich jeder RTS-Befehl, also auch der bei »ENDE« (920).
»ZAHLWORT«, die Routine zu 0, holt eine Zahl als ASCII-Code (1230) und wandelt sie in einen numerischen Wert um (1240 -1250), indem der ASCII-Code von 0 abgezogen wird. Das Ergebnis landet im X-Register (1260). Ob auch eine Zahl eingegeben wurde, prüfen die Zeilen 1310 - 1330. Bei »ZAHLWORT« (1350) wird das Resultat der Subtraktion in »XSAVE« gesichert, der Text »IN WORTEN« ausgegeben und das X-Register wieder geholt.
Die Tabelle »ZWLO/ZWHI« enthält die Adressen, ab denen die Texte der Zahlwörter als ASCII-Code stehen. Aus »ZWLO/ZWHI« wird dann diese Adresse geholt (1400 -1410) und der dort stehende Text ausgegeben (1420). Danach erwartet das Programm noch einen Tastendruck (1440-1450), bevor ins Hauptmenü verzweigt wird (1460).
Als letzte Routine wird »FARBE« besprochen (1500-1850): Hierzu istjedoch aufgrund derÄhnlichkeitzu Listing 10 nicht viel zu erläutern. Bei 1820 steht im X-Register der Code der eingegebenen Farbe (= Position der Prüfsumme innerhalb der Tabelle »PRUEFSUMMEN«). Dieser muß nur noch in die entsprechenden VIC-Register geschrieben werden (1820-1830). Ab Zeile 10000 stehen dann die Tabellen. Wenn Sie die Tabellen angesehen haben, sollten Sie durchaus noch einmal den Quelltext bis 10000 betrachten und die hier endende Beschreibung des Programms lesen. Denn wenn Sie das Programm »TABELLEN-BEISPIEL« ganz verstanden haben, sind Sie einen großen Schritt in der Assemblerprogrammierung weitergekommen I
Ich könnte mir übrigens vorstellen, daß Sie in Ihren eigenen Programmen jetzt auch eine Menüsteuerung wie die in »TABELLEN-BEISPIEL« einbauen; wie das geht, können Sie dem Programm »TABELLEN-BEISPIEL« entnehmen.
Eine Anmerkung ist wichtig: »TABELLEN-BEISPIEL« kann noch weiter verbessert werden. Sie werden sehen, daß viele Stellen noch optimiert werden können. Insbesondere der Speicherplatzbedarf kann verringert werden.
f) Weitere Anregungen zur Anwendung von Tabellen
Auch die bisherigen Erläuterungen und das Beispielprogramm können die Kreativität des Programmierers nicht ersetzen, sondern nur die Programmierung erleichtern. Aus diesem Grund möchte ich Ihnen noch einige Beispiele nennen, wie sich Tabellen sinnvoll verwerten lassen.
- Ein Anwenderprogramm, das aus Menüs und Untermenüs besteht, sollte in einer Tabelle die Adressen der Menüs/Untermenüs speichern.
- Spiele müssen oft viele Spritebewegungen, die immer gleich sind, durchführen. Es empfiehlt sich, die Spritebewegungen als Koordinaten in einer Tabelle abzulegen.
-Bei Software-Interfaces müssen viele Umrechnungen
erfolgen. Durch eine Umwandlungstabelle können diese stark beschleunigt werden. - Naturwissenschaftlich orientierte Programme müssen verschiedene Maße umrechnen. Die Umrechnungswerte können in einer Tabelle untergebracht werden.
Dies soll nur eine Anregung sein. Ich wüßte aber kein komplexes Programm, das sich nicht durch den gezielten Einsatz von Tabellen vereinfachen und beschleunigen ließe.
7. Die Initialisierung
»Initialisierung« nennt man eine Routine, die vor einem Programmteil (meist einer Schleife) steht und diese vorbereitet. Die Initialisierung wird nur einmal, eine Schleife aber mehrfach durchlaufen. Deshalb bringt es einen Geschwindigkeitszuwachs, wenn die Initialisierung der Schleife Arbeit abnimmt.
Ein Beispiel: Wenn ein Basic-Programm mit »RUN« gestartet wird, werden alle Variablen gelöscht, Files geschlossen und die Adressen, ab denen die Variablen abgelegt werden dürfen, errechnet. Dies ist die Initialisierung der Interpreterschleife. Dann wird Byte für Byte des Basic-Programms eingelesen und bearbeitet.
Muß im gerade übersetzten Befehl ein Sprung (GOTO 500 oder ähnliches) durchgeführt werden, kostet dies bekanntlich viel Zeit, wenn das Sprungziel am Ende eines langen Programms steht. Dies ist darauf zurückzuführen, daß der Interpreter, beginnend mit der ersten Zeile, das ganze Programm nach der Sprungzeile durchsucht, bis er sie gefunden hat.
Diese Berechnung der Adressen wird bei jedem »GOTO« oder »GOSUB« neu durchgeführt.
Viel besser und schneller wäre folgende Vorgehensweise: Bei »RUN« wird zunächst eine Tabelle angelegt, in der die Adressen aller Zeilen enthalten sind. Diese Tabelle könnte zum Beispiel als Array definiert werden. Folgt nun ein Sprung, kann aus der Tabelle die Adresse der Zeile im Speicher geholt werden.
Damit haben wir noch ein wesentliches Merkmal der Initialisierungsroutinen gefunden: Die Initialisierung kann Tabellen anlegen, die dann von der Hauptschleife verarbeitet werden.
Aber nicht nur Tabellen können generiert werden, auch die Berechnung von Flags ist sinnvoll. So merkt sich die »LOAD/VERIFY«-Routine ($FFD5), ob ein Verifizieren oder Laden gewünscht wird. Die Ladeschleife liest dann ein Zeichen von der Floppy oder der Datasette ein und entscheidet erst anschließend, ob das Byte im Speicher abgelegt oder mit dem Speicher verglichen werden soll.
Halten wir also fest, daß Initialisierungsroutinen Schleifen entlasten können. Näher werden wir uns damit beim Thema »Schleifen« beschäftigen.
8. Die Nutzung der Zeropage
In jedem Assembler-Lehrbuch werden die Vorteile der Zeropage-Adressierung gepriesen. Speicherplatzersparnis und hohe Verarbeitungsgeschwindigkeit sind nicht die einzigen Vorzüge; die indirekt-indizierte Adressierung kann nur auf Zeropage-Adressen zugreifen, nicht auf absolute 16-Bit-Adressen. Damit wird der Leser aber schon alleine gelassen. Er erfährt nicht, welche Adressen in der Zeropage für die Praxis geeignet sind. Das wird nun nachgeholt.
Fast die ganze Zeropage wird durch Basic-Interpreter und Betriebssystem belegt. Deshalb führen bestimmte Werte in Zeropage-Adressen oft zum Absturz oder sonstigem Fehlverhalten des Computers.
Wie dies im einzelnen aussieht, erfahren Sie in der Serie »Memory Map mit Wandervorschlägen«, die im 64’er Stammheft erscheint. Nicht nur in Zweifelsfällen stellt diese Serie das optimale Nachschlagewerk dar.
Ich möchte Ihnen nun zeigen, welche Adressen Sie als (Zwischen-)Speicher ohne Schwierigkeiten verwenden können, beziehungsweise was Sie bei Verwendung von Zeropage-Adressen beachten müssen.
a) Adressen, die problemlos verwendet werden können
Auf die Adressen $02 und $FB - $FE wird weder vom Basic-Interpreter noch vom Betriebssystem zugegriffen. Lediglich bei Initialisierung der Arbeitsspeicher (RESET) werden Sie auf 0 gesetzt.
Für die Praxis heißt das, daß Ihnen die genannten Adressen völlig zur Verfügung stehen.
b) Adressen, die in keiner Weise verwendet werden sollten
Von anderen Adressen hingegen müssen wir unsere Finger lassen. Diese haben entweder elementare Funktionen für Betriebssystem oder CPU, oder werden von beiden dauernd geändert, so daß die Datensicherheit in Frage gestellt ist. Genauer soll hier nicht unterschieden werden.
Belassen Sie die Adressen $00 und $01 unverändert, da sie (siehe Memory Map) für die CPU wichtige Informationen beinhalten und außerdem einige Bits nur durch externe Vorgänge geändert werden.
Das Betriebssystem und der Basic-Interpreter beanspruchen alle bislang ungenannten Adressen.
Von Bildschirmeditor und Tastaturabfrage werden die Adressen $C6 - $F6 beeinflußt. Die Adressen $90 - $C2 dienen der Ein-/Ausgabe-Steuerung mit Peripheriegeräten und der Verwaltung offener Files. Einzige Ausnahme: $A0 -$A2 (interne Uhr). Wenn ein Maschinenprogrammm in ein Basic-Programm eingebaut ist, sind die Adressen $03 - $56 sowie $73 - $8B tabu.
c) Bedingt einsetzbar
Der Vektor $C3/$C4 wird durch RUN/STOP-Restore, RESET oder LOAD beeinflußt. Ansonsten kann mit $C3/$C4 frei verfahren werden.
Ganz Vorsichtige können diesen Vektor auf seinen Ausgangswert $FD30 setzen, sobald das Programm die Adressen $C3/$C4 nicht mehr für eigene Zwecke benötigt.
d) Adressen, die unter Verzicht auf Kassettenbetrieb verwendet werden können
Die folgenden Adressen können verwendet werden, wenn nicht auf RS232 oder Datasette zugegriffen wird.
$9E/$9F, $A5-$A7, $A9-$AB, $B0-$B6, $F7-$FA
Bei anderen Adressen, die sich auf den RS232- oder Kassettenbetrieb beziehen, ist Vorsicht angebracht.
e) Geeignete Zwischenspeicher
Die Adressen $22-$2A und $57-$60 sind sogenannte »verschieden genutzte Arbeitsbereiche«. Sie werden vom Basic-Interpreter vor allem bei arithmetischen Operationen als Zwischenspeicher verwendet. Als solche Zwischenspeicher können wir sie auch verwenden. Sobald allerdings bestimmte Interpreterroutinen aufgerufen werden, können die Inhalte dieser Adressen verlorengehen. Eine längerfristige Aufbewahrung von Daten in diesen Adressen ist zwar nicht möglich, andererseits können wir aber durch Schreibzugriffe auf diese Adressen das Betriebssystem oder den Basic-Interpreter nicht stören.
Zu sagen wäre noch, daß die Adressen $57 - $60 den wichtigen Routinen BLTUC und UMULT (siehe »Assembler ist keine Alchimie«) als Zwischenspeicher dienen.
f) Zeropage kopieren
Zum Abschluß dieses Abschnittes über die Nutzung der Zeropage möchte ich Ihnen noch einen kleinen Trick verraten, der von einigen professionellen Programmen angewandt wird.
Wir sichern die Zeropage-Inhalte in einem anderen Bereich, zum Beispiel von $6F00 an.
Dann können wir viele Adressen in der Zeropage nutzen, sofern wir keine Interpreter- oder Betriebssystemroutine aufrufen. Danach schreiben wir die Zeropage wieder von der Kopie, zum Beispiel von $6F00, zurück und können wie gewöhnlich fortfahren.
Die Adressen 0 und 1 kopieren wir nicht, weil diese nach wie vor für solche Zwecke nutzlos sind. Ebenso könnten wir nur einzelne Bereiche kopieren (zum Beispiel die Zeiger für Basic-Programme$16 - $4A). Dann dürfen wiraberauch nur diesen Bereich verändern. Wenn wir nun den Bereich $02 - $FF kopieren, stehen uns folgende Adressen zur Verfügung: $03-$06, $14-$86, $71-$8A, $C3/$C4, $FB-$FF Diese Adressen können Sie nur so lange verwenden, bis eine Routine des Betriebssystems oder Basic-Interpreters aufgerufen wird. Davor muß die alte Zeropage zurückgeschrieben werden. Da Sie auf diese Weise viel Speicherplatz in der Zeropage gewonnen haben, ist es sogar möglich, eine Tabelle aus Geschwindigkeitsgründen in die Zeropage zu verlegen. Damit steigt auch der Wert der indiziert-indirekten Adressierung erheblich. Dennoch ist der Speicherplatz in der Zeropage begrenzt. Überlegen Sie sich also, auf welche Werte besonders schnell zugegriffen werden muß und schreiben Sie vorzugsweise diese in die Zeropage.
9. Schleifenprogrammierung
Zunächst befassen wir uns mit Schleifen, die maximal 256mal durchlaufen werden.
Typ a: Schleifen mit maximal 256 Durchläufen
Da 256 verschiedene Zahlen mit einem 8-Bit-Prozessor dargestellt werden können, verwendet man hier das X- (oder Y-) Register als Schleifenzähler. In Listing 12 sehen Sie die einfachste Form einer Schleife, die die Zeropage-Adressen $02 - $FF nach $6F00 kopiert.
,6000 A2 00 LDX #00 ,6002 B5 02 LDA 02,X ,6004 9D 00 6F STA 6F00,X ,6007 E8 INX ,6008 E0 FE CPX #FE ,600A D0 F6 BNE 6002
Da der Schleifenzähler X in Listing 12 INKREMENTIERT wird, haben wir es mit einer INKREMENTIERSCHLEIFE zu tun. Nach dem Inkrementieren (»6007 INX«) wird durch »6008 CPX # FE« überprüft, ob die Schleife beendet werden kann. Eine eingehendere Beschreibung des Programmablaufs erübrigt sich.
Für Schleifen des Typs a (maximal 256 Durchläufe) ist es aber meist vorteilhaft, eine DEKREMENTIERSCHLEIFE zu verwenden. Wie eine solche Schleife programmiert wird, sehen wir an Listing 13.
,6000 A2 FE LDX #FE ,6002 B5 01 LDA 01,X ,6004 9D FF 6E STA 6EFF,X ,6007 CA DEX ,6008 D0 F8 BNE 6002
Listing 13 unterscheidet sich in der Wirkung nicht von Listing 12, obwohl man dies nicht unbedingt auf den ersten Blick erkennt. Deshalb soll dieses Listing näher besprochen werden. In Zeile 6000 erhält das X-Register den Inhalt $FE. Durch »6002 LDA 01,X« wird damit das letzte Byte der Zeropage, nämlich $FF, zuerst gelesen und nach $7OFE geschrieben. Dann wird X dekrementiert. Ist X noch nicht 0, so wird die Schleife erneut durchlaufen.
Der niedrigste X-Wert innerhalb der Schleife ist folglich 1; aufgrund von »6002 LDA 01,X« ist $02 die niedrigste Zeropage-Adresse, die kopiert wird. In Listing 12 ist 0 der niedrigste X-Wert. Die niedrigste Adresse aufgrund von »6002 LDA 02,X« ist also auch $02 (stimmt auffällig). Warum $FF die höchste kopierte Zeropage-Adresse ist, können Sie nun selbst den Listings 12 und 13 entnehmen.
Listing 14 ist eine Dekrementierschleife, die die Kopie der Zeropage wieder von $6F00 nach $02 zurückholt.
,6000 A2 FE LDX #FE ,6002 BD FF 6E LDA 6EFF,X ,6005 95 01 STA 01,X ,6007 CA DEX ,6008 D0 F8 BNE 6002
Der Vorteil von Dekrementierschleifen beim Typ a ist, daß zum Erkennen der Abbruchbedingung (X=0) kein Vergleichsbefehl erforderlich ist, weil nach dem DEX-Befehl automatisch das Z-Flag gesetzt wird, wenn X Null wird.
Das Entfallen des Vergleichsbefehls »CPX #« bringt eine Ersparnis von 2 Byte Speicherplatz sowie insgesamt 508 Taktzyklen Rechenzeit. Dajedoch bei 6004 eine Seitenüberschreitung (eine Seite entspricht 256 Byte) vorliegt, schrumpft der Zeitgewinn auf 254 Taktzyklen (dies ließe sich aber vermeiden, indem wir die Zeropage nach $6F01 kopieren, womit durch »6004 STA $6F00,X« keine Seitenüberschreitung auftreten würde).
Nun wollen wir noch einen Sonderfall behandeln:
Dekrementierschleifen vom Typ a, bei denen der Ausgangswert für X < 129 ist.
In Listing 15 sehen Sie eine Schleife, die den Bereich $16 - $4A nach $6F00 kopiert, Listing 16 schreibt die Werte von $6F00 zurück nach $16. Selbstverständlich hätten wir das Problem auch so lösen können wie in Listing 13. Wir wollen aber noch eine andere Konstruktion von Dekrementierschleifen kennenlernen, die in diesem Sonderfall möglich ist. Besprechen wir also Listing 15.
,6000 A2 34 LDX #34 ,6002 B5 16 LDA 16,X ,6004 9D 00 6F STA 6F00,X ,6007 CA DEX ,6008 10 F8 BPL 6002
,6000 A2 34 LDX #34 ,6002 BD 00 6F LDA 6F00,X ,6005 95 16 STA 16,X ,6007 CA DEX ,6008 10 F8 BPL 6002
Bei 6000 wird ins X-Register die Zahl geladen, die man zu $16 addieren muß, um $4A zu erhalten. Dadurch wird zunächst bei 6002 die Adresse $4A gelesen und nach $6F34 geschrieben. Bei 6007 wird dekrementiert. Neu ist der Verzweigungsbefehl: es wird das N-Flag überprüft. Ist X = $FF, wird das N-Flag gesetzt und »6008 BPL 6002« beendet die Schleife. Der niedrigste Wert von X, der innerhalb der Schleife vorkommt, ist demnach $00.
Der BPL-Befehl funktioniert nur, wenn der Ausgangswert von X <129 ist. Andernfalls wäre nämlich nach dem Dekrementieren X>127 und damit das N-Flag gesetzt. Dies aber hätte zur Folge, daß die Schleife nur 1mal durchlaufen würde.
Zur soeben behandelten Schleifenkonstruktion sind noch zwei Dinge zu sagen; erstens, daß sie nur in diesem Sonderfall (X<129) möglich ist, und zweitens, daß sie nicht effektiver als eine Lösung wie in Listing 13 ist.
Allgemeine Gültigkeit hat aber folgende Regel für Schleifen vom Typ a:
An Listing 17 sehen wir ein Beispiel für den letzten Satz der Regel. Listing 17 kopiert die letzten 256 Speicherplätze des Stapels ($0100 - $01FF) und den Stapelzeiger nach $6F00 - $7000. Listing 18 schreibt den Stapel wieder zurück.
,6000 A2 FF LDX #FF ,6002 BD 00 01 LDA 0100,X ,6005 9D 00 6F STA 6F00,X ,6008 CA DEX ,6009 D0 F7 BNE 6002 ,600B AD 00 01 LDA 0100 ,600E 8D 00 6F STA 6F00 ,6011 BA TSX ,6012 8E 00 70 STX 7000
,6000 A2 FF LDX #FF ,6002 BD 00 6F LDA 6F00,X ,6005 9D 00 01 STA 0100,X ,6008 CA DEX ,6009 D0 F7 BNE 6002 ,600B AD 00 6F LDA 6F00 ,600E 8D 00 01 STA 0100 ,6011 AE 00 70 LDX 7000 ,6014 9A TXS
Die Dekrementierschleife (6000 - 600A) kopiert nun den Bereich $0101 - $O1FF, $0100 wird nicht übertragen. Dies geschieht in 600B - 600F. Eine andere Möglichkeit wäre ein zeitraubender CPX #FF-Befehl nach »6008 DEX«.
6011 - 6013 sichert schließlich noch das SP-Register.
Hier ist in der Tat eine Inkrementierschleife besser. Ändern wir Listing 17 also in Listing 17a:
| - | LDX #00 | ||
| - | LOOP | LDA 0100,X | |
| - | STA 6F00,X | ||
| - | INX | ;(!!) | |
| - | BNE LOOP | ||
| - | TSX | ||
| - | STX 7000 |
Analog ergibt sich Listing 18a:
| - | LDX #00 | ||
| - | LOOP | LDA 6F00,X | |
| - | STA 0100,X | ||
| - | INX | ;(!!) | |
| - | BNE LOOP | ||
| - | LDX 7000 | ||
| - | TXS |
In den Listings 17a und 18a habe ich diejenigen Befehle, die sich in der symbolischen Darstellung nicht von den Listings 17 und 18 unterscheiden, mit einem »-« markiert.
Typ b: Schleifen mit mehr als 256 Durchläufen
Während Schleifen des Typs a meist so schnell abgearbeitet werden, daß man es gar nicht bemerkt, dauern Typ-b-Schleifen oft eine oder mehrere Sekunden.
Deswegen wollen wir hier versuchen, den Zeitbedarf von Typ-b-Schleifen zu verringern.
Unsere erste Typ-b-Schleife (Listing 19) soll den Bereich von $3FD2 bis $475F invertieren (= EOR # FF-verknüpfen, aus jeder 1 wird eine 0 und umgekehrt). Da hierfür ein 8-Bit-Indexregister nicht ausreicht, benötigen wir einen 16-Bit-Zähler, nämlich$14/$15. Dieser soll immer die Adresse beinhalten, die invertiert wird. In diesen Zähler schreibt die Initialisierung der Schleife den Startwert $3FD2 (siehe $6000 -$6007).
,6000 A9 D2 LDA #D2 ,6002 85 14 STA 14 ,6004 A9 3F LDA #3F ,6006 85 15 STA 15 ,6008 A0 00 LDY #00 ,600A B1 14 LDA (14),Y ,600C 49 FF EOR #FF ,600E 91 14 STA (14),Y ,6010 E6 14 INC 14 ,6012 D0 02 BNE 6016 ,6014 E6 15 INC 15 ,6016 A5 14 LDA 14 ,6018 C9 60 CMP #60 ,601A A5 15 LDA 15 ,601C E9 47 SBC #47 ,601E 90 EA BCC 600A
Da es beim 6510 keine indirekte Adressierung für LDA/STA gibt, sbndern nur die indirekt-indizierte oder indiziert-indirekte, müssen wir auf eine dieser Adressierungen ausweichen und den Index auf 0 setzen (»6008 LDY #00«).
Bei $600A beginnt die Schleife: der Wert wird eingelesen, mit $FF EOR-verknüpft und zurückgeschrieben. Nun wird der 16-Bit-Zähler $14/$15 erhöht (6010 - 6015). Dann wird überprüft, ob die nächste Adresse schon mit der ersten Adresse nach der Endadresse ($475F), also $4760, übereinstimmt (siehe $6016 - $601D). Dieser 16-Bit-Vergleich wurde bereits im SMON vorgestellt. Bei $601E wird schließlich die Schleife beendet, falls die Abbruchbedingung (C=1) erfüllt ist.
Listing 20 ist eine Dekrementierschleife, die sich in der Wirkung nicht von Listing 19 unterscheidet. Da das Dekrementieren einer 16-Bit-Adresse beim 6510 langsamer und speicherplatzaufwendiger ist als das Inkrementieren, ist Listing 20 weniger effektiv als Listing 19.
,6000 A9 5F LDA #5F ,6002 85 14 STA 14 ,6004 A9 47 LDA #47 ,6006 85 15 STA 15 ,6008 A0 00 LDY #00 ,600A B1 14 LDA (14),Y ,600C 49 FF EOR #FF ,600E 91 14 STA (14),Y ,6010 A5 14 LDA 14 ,6012 D0 02 BNE 6016 ,6014 C6 15 DEC 15 ,6016 C6 14 DEC 14 ,6018 A5 14 LDA 14 ,601A C9 D2 CMP #D2 ,601C A5 15 LDA 15 ,601E E9 3F SBC #3F ,6020 B0 E8 BCS 600A
Grundsätzlich können Sie an den Listings 19 und 20 sehen, wie man eine Typ-b-Schleife programmiert. Diese arbeitet jedoch nicht besonders schnell. Der Grund ist, daß der Bereich von $3FD2 - $475F nicht restlos in ganze Seiten (256-Byte-Blöcke)aufgeteilt werden kann. Daher sollte man sich immer überlegen, ob die Schleifendurchlaufzahl nicht auf ganze 256-Byte-Blöcke »aufgerundet« werden kann. In unserem Fall würde dies heißen, daß mit einer schnelleren Schleife der exakt 8 x 256 Byte lange Bereich $3FD2 - $47D1 invertiert wird, anstelle des »ungeraden« Bereichs $3FD2 - $475F. An einfacheren Zahlen wollen wir nun eine solche Schleife für ganze Seiten programmieren. Der 32 x 256 Byte umfassende Bereich von $2000 bis $3FFF (einschließlich) soll invertiert werden. Mit einer solcher Routine könnte das gerade sichtbare Bild bei Hi-Eddi invertiert werden.
Die einfachste Form finden Sie in Listing 21. Zuerst wird die Anfangsadresse in $14/$15 abgelegt. Ins Y-Register kommt der Wert 0. Dann wird der Wert invertiert und das Y-Register, der Low-Zähler, erhöht. Ist der Wert noch nicht 0, wird die Schleife neu durchlaufen. Andernfalls wurde gerade eine Seite abgearbeitet. Der High-Zähler ($15) wird erhöht. Ist der Inhalt des High-Zählers = $40, wird die Schleife abgebrochen. Zu bemerken ist, daß während der Schleife die Adresse $14 unverändert 0 bleibt. Die Adresse, die invertiert wird, ergibt sich folgendermaßen:
(Y+Inhalt von $14)+256*(Inhalt von $15)
,6000 A9 00 LDA #00 ,6002 85 14 STA 14 ,6004 A9 20 LDA #20 ,6006 85 15 STA 15 ,6008 A0 00 LDY #00 ,600A B1 14 LDA (14),Y ,600C 49 FF EOR #FF ,600E 91 14 STA (14),Y ,6010 C8 INY ,6011 D0 F7 BNE 600A ,6013 E6 15 INC 15 ,6015 A5 15 LDA 15 ,6017 C9 40 CMP #40 ,6019 D0 EF BNE 600A
Da wir auf die Adresse über das Prozessor-Register Y Einfluß nehmen können und die Adresse $14 nicht verändert werden muß, ist die Verarbeitungsgeschwindigkeit gegenüber der »Normalform« (Listing 20) gestiegen. Das High-Byte müssen wir aber weiterhin in $15 belassen. Neu führen wir den High-Zähler X ein. Im X-Register merken wir uns, wieviele Seiten invertiert werden. Diesen Wert verwenden wir als Dekrementierzähler. In unserem Fall werden $20 Seiten invertiert. Weil $20 zufälligerweise auch das High-Byte der Anfangsadresse ($2000) ist, wird dieser Wert in Listing 22 nur einmal (6005) in den Akku geladen und dann bei 6009 ins X-Register übertragen.
,6000 A9 00 LDA #00 ,6002 85 14 STA 14 ,6004 A8 TAY ,6005 A9 20 LDA #20 ,6007 85 15 STA 15 ,6009 AA TAX ,600A B1 14 LDA (14),Y ,600C 49 FF EOR #FF ,600E 91 14 STA (14),Y ,6010 C8 INY ,6011 D0 F7 BNE 600A ,6013 E6 15 INC 15 ,6015 CA DEX ,6016 D0 F2 BNE 600A
Beachten Sie bitte, daß in Listing 22 die Befehle »6004 TAY« und »6009 TAX« nur bei den Werten dieses Beispiels verwendet werden können. In der Regel sind eigene »LDX # «- oder »LDY #«-Befehle erforderlich. Wenn wir zum Beispiel den Bereich $3FD2 - $47D1 invertieren wollen, muß die Initialisierung so aussehen:
| LDA #D2 | Low-Byte der ersten Adresse | |
| STA 14 | ||
| LDY #00 | Index-Register | |
| LDA #3F | High-Byte der ersten Adresse | |
| STA 15 | ||
| LDX #08 | High-Zähler | |
| … Schleife wie ab 600C in Listing 22 | ||
Damit hätten wir eine Schleife, die den Bereich # 3FD2 -$475F (siehe Listings 19 und 20) invertiert und wesentlich schnellerals die Listings 19 und 20 arbeitet. Dawiraber »aufgerundet« haben, wird zusätzlich der Bereich $4760 - $47D1 invertiert, obwohlwirdasgarnichtwollen. Esgibtnun mehrere Möglichkeiten, dies zu verhindern:
- Wir verwenden die Schleife aus Listing 19, müssen aber eine deutlich höhere Arbeitsdauer hinnehmen.
- Wir verwenden die Schleife aus Listing 22 mit obiger Initialisierung. Dann invertiert eine Typ-a-Schleife den Restbereich $4760 - $47D1 ein weiteres Mal. Damit wären - eine Besonderheit der EOR # FF-Verknüpfung - im Restbereich die alten Inhalte wiederhergestellt. Diese Lösung eignet sich aber (fast) nur bei dieser logischen Verknüpfung und hilft bei den meisten anderen Typ-b-Schleifen nicht weiter.
- Dies dürfte wohl die beste Lösung sein: Wir schreiben eine »gemischte« Schleife, die aus einer Typ-a-Schleife und einer Typ-b-Schleife besteht. Dieses Verfahren ist immer (!) möglich und wird von der BLTUC-Routine ($A3BF) des Basic-Interpreters angewandt. Diese Verschiebe-Routine zerlegt den Bereich, der verschoben werden soll, in einen Bereich der aus 256-Byte-Blöcken besteht und in einen Restbereich. Beide Bereiche werden dann getrennt verschoben.
Folgendermaßen sieht die optimale Invertierroutine für den Bereich $3FD2 - $475F aus:
a) Der exakt 7 Seiten umfassende Bereich 3FD2 - $46D1 wird mit einer Typ-b-Schleife wie in Listing 22 komplementiert.
b) Der Restbereich $46D2 - $475F wird mit einer Typ-a-Schleife wie in Listing 13 komplementiert.
Wir haben nun viele verschiedene Schleifenkonstruktionen in Theorie und Praxis behandelt. Was uns noch fehlt, sind Formeln, nach denen Sie die einzelnen Parameter (zum Beispiel den Startwert für X in einer Dekrementier-Schleife vom Typ a) errechnen können. Als Zusammenfassung finden Sie in Form von Listing 23 ein Hypra-Ass-Assemblerlisting zu mehreren Schleifenkonstruktionen. An den Quelltext-Ausdrücken können Sie sehen, wie einzelne Parameter errechnet werden können.
70 -.BA $C000 80 -.LI 1,3,0 90 -; 100 -; ******************************* 110 -; * QUELLTEXTE (HYPRA-ASS) * 120 -; * ====================== * 130 -; * * 140 -; * FUER VERSCHIEDENE SCHLEIFEN * 150 -; * * 160 -; * 28.08.85 BY FLORIAN MUELLER * 170 -; * * 180 -; ******************************* 190 -; 200 -; 210 -; QUELLTEXT ZU LISTING 1 220 -; ====================== 230 -; 240 -.EQ ANFANGSADRESSE = $02 250 -.EQ ENDADRESSE = $FF 260 -.EQ ZIELBEREICH = $6F00 270 -; 280 - LDX #0 290 -SCHLEIFE1 LDA ANFANGSADRESSE,X 300 - STA ZIELBEREICH,X 310 - INX 320 - CPX #(ENDADRESSE+1-ANFANGSADRESSE) 330 - BNE SCHLEIFE1 340 -; 350 -; 360 -; QUELLTEXT ZU LISTING 2 370 -; ====================== 380 -; 390 -.EQ ANFANGSADRESSE = $02 400 -.EQ ENDADRESSE = $FF 410 -.EQ ZIELBEREICH = $6F00 420 -; 430 - LDX #(ENDADRESSE+1-ANFANGSADRESSE) 440 -SCHLEIFE2 LDA ANFANGSADRESSE-1,X 450 - STA ZIELBEREICH-1,X 460 - DEX ; DEKREMENTIERBEFEHL 470 - BNE SCHLEIFE2 480 -; 490 -; 500 -; QUELLTEXT ZU LISTING 4 510 -; ====================== 520 -; 530 -.EQ ANFANGSADRESSE = $16 540 -.EQ ENDADRESSE = $4A 550 -.EQ ZIELBEREICH = $6F00 560 -; 570 - LDX #(ENDADRESSE-ANFANGSADRESSE) 580 -SCHLEIFE3 LDA ANFANGSADRESSE,X 590 - STA ZIELBEREICH,X 600 - DEX 610 - BPL SCHLEIFE3 ; PRUEFT N-FLAG 620 -; 630 -; 640 -; QUELLTEXT ZU LISTING 8 650 -; ====================== 660 -; 670 -.EQ ANFANGSADRESSE = $3FD2 680 -.EQ ENDADRESSE = $475F 690 -.EQ ZAEHLER = $14 700 -; 710 - LDA #<(ANFANGSADRESSE) 720 - STA ZAEHLER 730 - LDA #>(ANFANGSADRESSE) 740 - STA ZAEHLER+1 750 - LDY #0 760 -SCHLEIFE4 LDA (ZAEHLER),Y 770 - EOR #$FF 780 - STA (ZAEHLER),Y 790 - INC ZAEHLER 800 - BNE WEITER 810 - INC ZAEHLER+1 820 -WEITER LDA ZAEHLER 830 - CMP #<(ENDADRESSE+1) 840 - LDA ZAEHLER+1 850 - SBC #>(ENDADRESSE+1) 860 - BCC SCHLEIFE4 870 -; 880 -; 890 -; QUELLTEXT ZU LISTING 10 900 -; ======================= 910 -; 920 -.EQ ANFANGSADRESSE = $2000 930 -.EQ ENDADRESSE = $3FFF 940 -.EQ ZAEHLER = $14 950 -; 960 - LDA #<(ANFANGSADRESSE) 970 - STA ZAEHLER 980 - LDA #>(ANFANGSADRESSE) 990 - STA ZAEHLER+1 1000 - LDY #0 1010 -SCHLEIFE5 LDA (ZAEHLER),Y 1020 - EOR #$FF 1030 - STA (ZAEHLER),Y 1040 - INY 1050 - BNE SCHLEIFE5 1060 - INC ZAEHLER+1 1070 - LDA ZAEHLER+1 1080 - CMP #>(ENDADRESSE+1) 1090 - BNE SCHLEIFE5 1100 -; 1110 -; 1120 -; QUELLTEXT ZU EINER SCHLEIFE, 1130 -; DIE DEN BEREICH $3FD2-$47D1 1140 -; KOMPLEMENTIERT 1150 -; 1160 -.EQ ANFANGSADRESSE = $3FD2 1170 -.EQ ENDADRESSE = $47D1 1180 -.EQ ZAEHLER = $14 1190 -; 1200 - LDA #<(ANFANGSADRESSE) 1210 - STA ZAEHLER 1220 - LDA #>(ANFANGSADRESSE) 1230 - STA ZAEHLER+1 1240 - LDX #>(ENDADRESSE+1-ANFANGSADRESSE) 1250 - LDY #0 1260 -SCHLEIFE6 LDA (ZAEHLER),Y 1270 - EOR #$FF 1280 - STA (ZAEHLER),Y 1290 - INY 1300 - BNE SCHLEIFE6 1310 - INC ZAEHLER+1 1320 - DEX 1330 - BNE SCHLEIFE6 1340 -; 1350 -; ENDE VON LISTING 12
Merke: Sofern es der Programmablauf zuläßt, sollten Sie Inkrementierschleifen verwenden.
Bei Verschiebeschleifen ist aber oft eine Dekrementierschleife erforderlich.
Noch etwas zum Schleifen-Inhalt: Wenn mehrere Schleifen einen gleichen Innenteil haben (zum Beispiel einen Invertierbefehl), definieren Sie diesen unbedingt als Makro und nicht als Unterprogramm! JSRs sollten Sie nur beim Aufruf von ROM-Routinen verwenden.
Damit wäre das Thema »Schleifen« erst einmal abgeschlossen. Im nächsten Abschnitt(überSelbstmodifikation) werden wir uns aber wieder mit Schleifen auseinandersetzen.
10. Selbstmodifikation
Bevor wir uns mit dieser Programmiertechnik beschäftigen, die zwar nicht strukturiert, aber sehr trickreich ist, soll der Begriff geklärt werden.
Unter Modifikation versteht man »eine Änderung, Anpassung«. Wenn Sie bei einem Spiel einen der vielen POKE-Befehle, die im 64’er schon vorgestellt wurden, eingeben, so wird dadurch das Spiel »modifiziert«. Die Änderung ist zum Beispiel eine Erhöhung der Spielfigurenanzahl.
Selbstmodifikation bedeutet, daß ein Programm sich selbst programmgesteuert verändert. Dies wäre der Fall, wenn im Spielprogramm eine Passage stünde, die den POKE ausführt.
Wenn Sie sich für die Selbstmodifikation von Basic-Programmen interessieren, finden Sie in der Zeitschrift »Happy-Computer« (Ausgabe 8/85) unter der Überschrift »Lernen Sie Ihren Commodore 64 kennen« alles, was Sie wissen müssen. Auf simulierten Direktmodus wurde im 64’er schon mehrfach eingegangen, unter anderem in der »Memory Map mit Wandervorschlägen«.
Wir werden uns an dieser Stelle ausschließlich mit der Selbstmodifikation von Maschinenprogrammen befassen. Als erstes Beispiel nehmen wir Listing 24.
,6000 A0 00 LDY #00 ,6002 B9 00 20 LDA 2000,Y ,6005 49 FF EOR #FF ,6007 99 00 20 STA 2000,Y ,600A C8 INY ,600B D0 F5 BNE 6002 ,600D EE 04 60 INC 6004 ,6010 EE 09 60 INC 6009 ,6013 AD 09 60 LDA 6009 ,6016 C9 40 CMP #40 ,6018 D0 E8 BNE 6002
Es handelt sich um eine selbstmodifizierende Schleife, die den Bereich $2000 - $3FFF komplementiert.
TRACEn Sie doch einmal Listing 24 mit dem SMON und vergleichen Sie die disassemblierten Befehle mit den ursprünglichen Werten, die Sie in Listing 24 finden. Sie werden erkennen, daß die Befehle »6002 LDA 2000,Y« und »6007 STA 2000,Y« aufgrund der INC-Befehle immer auf andere Adressen zugreifen. Besagte INC-Befehle erhöhen jeweils das High-Byte des Operanden. Ist dieses schon $40, so wird die Schleife beendet. In Listing 25 sehen Sie, wie unsere Schleife aus Listing 24 aussieht, wenn sie fertig durchlaufen wurde. Ein weiterer Start bewirkt, daß das Programm sich früher oder später selbst invertiert und darum abstürzt.
,6000 A0 00 LDY #00 ,6002 B9 00 40 LDA 4000,Y ,6005 49 FF EOR #FF ,6007 99 00 40 STA 4000,Y ,600A C8 INY ,600B D0 F5 BNE 6002 ,600D EE 04 60 INC 6004 ,6010 EE 09 60 INC 6009 ,6013 AD 09 60 LDA 6009 ,6016 C9 40 CMP #40 ,6018 D0 E8 BNE 6002
Was nämlich unserem Listing 24 fehlt, damit es mehr als einmal arbeitet, ist eine Initialisierung, diejedesmal den Ausgangswert ($2000) in die LDA/STA-Befehle einsetzt. In Listing 26 sehen Sie eine solche Initialisierung (6000 -600F). Die Adresse $FFFF (bei 6012 und 6017) ist ein Dummy-Wert, das heißt er dient nur zum vorläufigen Ausfüllen von Adressen und hat keine programmtechnische Bedeutung. Der Dummy-Wert wird ohnehin von der Initialisierung überschrieben; wir hätten also statt $FFFF auch $040C oder andere verwenden können. Wichtig ist nur, daß »LDA DummyY« 3 Byte belegt.
,6000 A9 00 LDA #00 ,6002 8D 13 60 STA 6013 ,6005 8D 18 60 STA 6018 ,6008 A9 20 LDA #20 ,600A 8D 14 60 STA 6014 ,600D 8D 19 60 STA 6019 ,6010 A0 00 LDY #00 ,6012 B9 FF FF LDA FFFF,Y ,6015 49 FF EOR #FF ,6017 99 FF FF STA FFFF,Y ,601A C8 INY ,601B D0 F5 BNE 6012 ,601D EE 14 60 INC 6014 ,6020 EE 19 60 INC 6019 ,6023 AD 19 60 LDA 6019 ,6026 C9 40 CMP #40 ,6028 D0 E8 BNE 6012
Ein besonderer Vorteil der Selbstmodifikation ist es, daß selbstmodifizierende Schleifen keine Zähler in der Zeropage benötigen, weil der Zähler praktisch im Programm selbst liegt. In puncto Geschwindigkeit sind selbstmodifizierende Schleifen den herkömmlichen aber oft unterlegen.
Ein weiterer Vorteil von ihnen ist aber, daß man außer mit weniger Zeropage-Speicherplätzen auch mit weniger Registern auskommen kann (sofern man hier Einsparungen vornehmen will). Listing 27 beispielsweise invertiert den Bereich $3FD2 - $475F. X- und Y-Register sowie die Zeropage bleiben unverändert, lediglich der Akkumulator fungiert als Arbeitsregister.
,6000 A9 D2 LDA #D2 ,6002 8D 11 60 STA 6011 ,6005 8D 16 60 STA 6016 ,6008 A9 3F LDA #3F ,600A 8D 12 60 STA 6012 ,600D 8D 17 60 STA 6017 ,6010 AD 00 00 LDA 0000 ,6013 49 FF EOR #FF ,6015 8D 00 00 STA 0000 ,6018 EE 11 60 INC 6011 ,601B EE 16 60 INC 6016 ,601E D0 06 BNE 6026 ,6020 EE 12 60 INC 6012 ,6023 EE 17 60 INC 6017 ,6026 AD 11 60 LDA 6011 ,6029 C9 60 CMP #60 ,602B AD 12 60 LDA 6012 ,602E E9 47 SBC #47 ,6030 90 DE BCC 6010
Listing 28 kopiert den Basic-Interpreter ($A000 - $BFFF) ins RAM an gleicher Adresse, wobei nur das X-Register verwendet wird (!).
,6000 A2 00 LDX #00 ,6002 8E 11 60 STX 6011 ,6005 8E 14 60 STX 6014 ,6008 A2 A0 LDX #A0 ,600A 8E 12 60 STX 6012 ,600D 8E 15 60 STX 6015 ,6010 AE 00 00 LDX 0000 ,6013 8E 00 00 STX 0000 ,6016 EE 11 60 INC 6011 ,6019 EE 14 60 INC 6014 ,601C D0 F2 BNE 6010 ,601E EE 12 60 INC 6012 ,6021 EE 15 60 INC 6015 ,6024 AE 12 60 LDX 6012 ,6027 E0 C0 CPX #C0 ,6029 D0 E5 BNE 6010
Nun wollen wir sehen, wie man bei der Entwicklung selbstmodifizierender Programme unter Zuhilfenahme eines guten Assemblers (Hypra-Ass) vorgehen muß.
Zunächst einmal müssen diejenigen Stellen, an denen Modifikationen vorgenommen werden, mit Label definiert werden. Von diesen Label aus können die Stellen im Speicher die geändert werden sollen, leicht berechnet werden.
| Befehlscode | = LABEL + 0 = | LABEL |
| Low-Operand | = | LABEL + 1 |
| High-Operand | = | LABEL + 2 |
Bei 2-Byte-Befehlen wird der Parameter wie der Low-Operand eines 3-Byte-Befehls errechnet.
Als Beispiel finden Sie in Form von Listing 29 einen Quelltext (Assembler: Hypra-Ass) für Listing 28. Während in Listing 28 der Ausgangswert bei 6010 »LDX 0000« und bei 6013 »STX 0000« ist, wurde im Quelltext $FFFF verwendet (270, 280), um den Assembler zu zwingen, den Dummy-Wert als 16-Bit-Adresse abzulegen (und nicht als Zeropage-Adresse, wodurch der Befehl nur 2 statt 3 Byte belegen würde).
80 -.BA $6000 90 -.LI 1,3,0 100 -; 110 -; HYPRA-ASS-QUELLTEXT ZU EINER 120 -; SELBSTMODIFIZIERENDEN SCHLEIFE 130 -; (ARBEITET WIE LISTING 5) 140 -; 150 -; 1985 BY FLORIAN MUELLER 160 -; 170 -; 180 -.GL START = $A000 190 -.GL ENDE = $BFFF 200 -; 210 - LDX #<(START) 220 - STX MOD1+1 230 - STX MOD2+1 240 - LDX #>(START) 250 - STX MOD1+2 260 - STX MOD2+2 270 -MOD1 LDX $FFFF 280 -MOD2 STX $FFFF 290 - INC MOD1+1 300 - INC MOD2+1 310 - BNE MOD1 320 - INC MOD1+2 330 - INC MOD2+2 340 - LDX MOD1+2 350 - CPX #>(ENDE+1) 360 - BNE MOD1
Die Stellen, die modifiziert werden, wurden mit »MOD1« und »MOD2« definiert. MOD1 ist zugleich der Schleifenbeginn.
Nachdem Sie jetzt den Eingang gefunden haben, möchte ich einige Anregungen liefern, wie Sie die Vorteile der Selbstmodifikation nutzen können. Wir werden hier die Anwendung nach den verschiedenen Adressierungsarten unterteilen.
a) Anwendung auf absolute Adressierung
Bei der Stapelmanipulation haben wir schon ein Verfahren kennengelernt, den Befehl JSR (indirekt), der im normalen 6510-Befehlssatz nicht existiert, zu simulieren.
Folgendermaßen kann über Selbstmodifikation ein Unterprogramm ab ADRESSE aufgerufen werden.
| LDA #<ADRESSE | ||
| STA SPRUNGBEFEHL+1 | ; Low-Operand | |
| LDA #>ADRESSE | ||
| STA SPRUNGBEFEHL+2 | ; High-Operand | |
| SPRUNGBEFEHL | ||
| JSR $FFFF | ; $FFFF=Dummy |
Genauso kann man mit dem JMP-Befehl verfahren. Sogar bei den Schieber-, Dekrementier- und Inkrementierbefehlen, die im Gegensatz zu JMP die indirekte Adressierung nicht haben, ist auf diese Weise eine Simulation der indirekten Adressierung möglich.
Wird eine Sprungtabelle per Selbstmodifikation verarbeitet, müssen die Sprungadressen in der Tabelle nicht (!) dekrementiert werden.
b) Anwendung auf Immediate-Befehle
Oft müssen Werte, die berechnet werden, auf dem Stapel oder im Speicher abgelegt und dann, wenn sie gebraucht werden, wieder aufgenommen werden.
Ein Beispiel hierfür ist der »Basic-Start-Generator« (64’er, 7/85, Seite 74). Bei Erwähnung dieses Programms taucht natürlich die Frage auf, ob es sich hier noch um ein selbstmodifizierendes Programm handelt oder ob der »Basic-Start-Generator« nicht eher zu den Programmgeneratoren zählt. Diese Frage ist voll berechtigt. Deshalb wollen wir darauf kurz eingehen.
Der »Basic-Start-Generator« ist eindeutig den Programmgeneratoren zuzuordnen, da der generierte Programmteil nie angesprungen wird und somit ein eigenständiges Programm darstellt. Das Programm modifiziert also nicht sich selbst, sondern vielmehr ein zweites Programm, welches dann vom Benutzer gespeichert werden kann.
Die Programmierung ist aber bei Programmgeneratoren nicht anders als bei selbstmodifizierenden Programmen. Auf den Unterschied Programmgeneration/Selbstmodifikation werden wir an späterer Stelle näher eingehen.
Zunächst wollen wir aber ein praktisches Beispiel für die Anwendung der Modifikation von Immediate-Befehlen behandeln. Oft steht man vor dem Problem, ein Register zu sichern und später wieder zu holen. Im Falle des Akkumulators sieht das so aus:
| PHA | ; Akku sichern |
| … | ; weiteres Programm |
| PLA | ; Akku wieder holen |
Beim X-Register wird’s schon ungünstiger:
| TXA | ; X-Register in Akku |
| PHA | ; Akku sichern |
| ……….. | ; weiteres Programm |
| PLA | ; Akku wieder holen |
| TAX | ; Akku ins X-Register |
Hier wird also zusätzlich der Akku beeinflußt. Wenn dies vermieden werden muß, wird folgender Weg gewählt:
| STX $02 | ; $02 = Zwischenspeicher |
| … | ; weiteres Programm |
| LDX $02 | ; X wieder holen |
Für die Sicherung des X-Registers gibt es aber noch eine weitere Lösung, die den X-Wert im Programm ablegt und dadurch nicht den Stapel oder einen Zwischenspeicher außerhalb des Programms benötigt.
| STX GETX+1 | ; X direkt in Immediate-Befehl schreiben | |
| … | ; weiteres Programm | |
| GETX | LDX #$00 | ; $00 = Dummy-Wert |
Obiges Beispiel kann sehr leicht auf Akkumulator oder Y-Register umgeschrieben werden.
Folgendermaßen kann das X-Register mit dem Akkumulator verglichen werden:
| STX VGL+1 | ; in Vergleichsbefehl ablegen | |
| (………… | ; evtl. weitere Programme) | |
| VGL | CMP #$00 | ; $00 = Dummy |
Als letztes Beispiel für die Anwendung auf Immediate-Befehle soll das Y-Register zum Akkumulator addiert werden:
| STY ADD+1 | ; in Arithmetikbefehl ablegen | |
| (……………… | ; evtl, weiteres Programm) | |
| CLC | ; Carry vor Addition | |
| ADD | ADC #$FF | ; $FF = Dummy |
Die Anwendungsmöglichkeiten sind hier unbegrenzt.
c) Anwendung auf komplette Befehle
Bisher haben wir nur die Parameter einzelner Befehle modifiziert. Es ist selbstverständlich auch möglich, die Befehlscodes oder die kompletten Befehle zu modifizieren.
Wenn nur der Befehlscode geändert wird (zum Beispiel ein ORA #- in einen EOR #-Befehl) bleiben die Parameter erhalten. Es könnte ferner ein impliziter Befehl (SEI,CLI,CLD, DEX,INX…) geändert werden, um beispielsweise zwischen In- und Dekrementieren umzuschalten. Außerdem könnte bei einem BRANCH-Befehl die Sprungbedingung (CS,CC,VS, VC,NE,EQ) geändert werden. Aus BCS könnte also leicht BCC werden.
Weil man hier die Opcodes der Befehle kennen muß, empfehle ich das erste 64’er Extra (Ausgabe 9/85) oder die Tabelle am Ende dieser Ausgabe.
Nun lösen wir noch das häufig auftretende Problem, wie die Ausführung eines Unterprogramms verhindert wird. Dazu werden wir drei Lösungen (I - III) entwickeln.
I. Die Adresse FLAG wird auf 0 gesetzt, wenn das Unterprogramm ausgeführt werden soll; auf einen anderen Wert, wenn es nicht ausgeführt werden soll.
| LDA #0 | ; Flag für Ausführung | |
| STA FLAG | ; Flag setzen | |
| (……… | ; evtl, weiteres Programm) | |
| LDA FLAG | ; Flag testen | |
| BNE NEIN | ; Flag < > 0, also nicht ausführen | |
| JSR UNTERPROGRAMM | ; Aufruf weiteres Programm | |
| NEIN | ….. |
Das Flag könnte auch am Beginn des Unterprogramms abgefragt und dann (wenn FLAG < > 0) das Unterprogramm verlassen werden.
II. Als ersten Befehl des Unterprogramms verwenden wir NOP:
| UP | NOP | ; Beginn des Unterprogramms |
| …….. | ; Fortsetzung des Unterprogramms |
So wird die Ausführung des Unterprogramms gestattet:
| LDA #$EA | ; Opcode für NOP | |
| STA UP | ; an Anfang des Unterprogramms schreiben |
Und so wird sie verhindert:
| LDA #$60 | ; Opcode für RTS | |
| STA UP | ; an Anfang des Unterprogramms schreiben |
Wer noch einen NOP-Befehl und damit 1 Byte sparen möchte, kann den NOP-Befehl entfallen lassen. Dann muß auch der Opcode $EA beim Erlauben des Unterprogramms in den Opcode des ersten Byte im Unterprogramm geändert werden. Weil dies ziemlich mühselig ist, ziehe ich die ursprüngliche Lösung II trotz des um 1 Byte erhöhten Speicherbedarfs vor.
III. Das beste Verfahren. Wir schalten den JSR-Befehl aus, indem wir ihn in einen BIT-Befehl abändern.
| AUFRUF | JSR Unterprogramm |
JSR ausschalten:
| LDA #$2C | ; Opcode für BIT | |
| STA AUFRUF |
JSR wieder erlauben:
| LDA #$20 | ; Opcode für JSR | |
| STA AUFRUF |
Der JSR-Opcode kann auch mit $0C überschrieben werden. $0C ist ein illegaler Opcode für ein 3-Byte-NOP und arbeitet mit allen mir bekannten Versionen des C 64. Ob er ebenfalls auf dem C 128 läuft, konnte ich noch nicht prüfen.
Im übrigen können mit dem soeben beschriebenen Verfahren auch andere Befehle ausgeschaltet werden, zum Beispiel JMP, LDA, STA und so weiter. Wenn aber der JSR-Opcode mit $2C (BIT) überschrieben wird, ist darauf zu achten, daß bei der Ausführung des BIT-Befehls die Prozessorflags gesetzt werden.
Sicherlich gibt es noch mehr Problemlösungen als I - III, aber III dürfte wohl kaum zu übertreffen sein.
d) Anwendung auf mehrere Befehle
Selbstverständlich können ganze Befehlsfolgen, also größere Programmteile gegeneinander ausgetauscht werden. Zu beachten ist nur, daß die Routinen, die gegeneinander ausgetauscht werden, auch in dem Bereich, in den sie vom Programm aus geschrieben werden, lauffähig sind. Dies ist vor allem dann gegeben, wenn nur die relative Adressierung verwendet wird und dadurch die Routine im Speicher frei verschoben werden kann.
e) Anwendung auf Tabellen
Dieser Anwendungsfall würde auch zum Abschnitt über »Tabellen« passen.
Wir bleiben hier bei der Theorie, denn die Umsetzung in ein Programm ist nicht mehr schwer. Vielmehr soll Ihre Kreativität nicht durch Unmengen von Beispielen gehemmt werden.
Zunächst wollen wir uns ein wenig mit dem SMON befassen. Wenn Sie den Disk-Monitor einschalten, kopiert das Programm einen Floppy-Befehl («U1 ..«) vom Ende des SMON in einen Bereichzwischen $02A0 und $02FF. Dieser Lesebefehl wird nach Bedarf modifiziert, zum Beispiel wird beim Schreiben der »U1«- in einen »U2«-Befehl umgewandelt oder die Angabe des einzulesenden Blocks wird geändert. Dies wäre ein typisches Anwendungsbeispiel für Selbstmodifikation, wenn der Lesebefehl nicht erst in einen Bereich außerhalb des Programms kopiert würde (worin ich keinen Sinn sehe), sondern am Ende des SMON (etwa bei $CFF0) bliebe und dort modifiziert würde.
Im Hi-Eddi liegt eine Tabelle, die die High-Byte der Bit-Map-Anfangsadressen beinhaltet. Diese Tabelle wird von Hi-Eddi bei jedem Bildwechsel umgerechnet.
Nach den vorausgegangenen zwei Beispielen an Spitzenprogrammen aus dem 64’er möchte ich noch andere Anwendungsbeispiele nennen.
Besonders flexible Programme erlauben Eingriffe des Anwenders in die Befehls- oder Text-Tabellen. So können Bildschirmmasken editiert oder Eingabemasken erstellt werden.
Ein solches Programm braucht sich nach den Modifikationen nur selbstabzuspeichern. Weil hier unter Umständen ein erheblicher Teil des Programmschutzes verlorengeht, werden dann lediglich die Tabellen gespeichert.
Ein Adventure-Generator modifiziert in der Regel auch nur die Tabellen eines fertigen Adventureprogramms, das eigentliche Programm bleibt unverändert. In diesen Tabellen sind die einzelnen Spielsituationen enthalten.
Bei diesen (theoretischen) Fällen wollen wir es belassen. Letztendlich muß ja der Programmierer entscheiden, inwieweit er die Selbstmodifikation auf Tabellen anwenden kann.
f) Das Beispielprogramm »Loader-Maker 64«
Wie aus dem Namen des Beispielprogramms schon zu entnehmen ist, handelt es sich um einen Programmgenerator. Da - wie gesagt - die Programmierung wie bei selbstmodifizierenden Programmen ist, habe ich bewußt einen Programmgenerator als Beispiel gewählt.
Als Listing 31 finden Sie ein MSE-Listing, falls Sie »Loader-Maker 64« bequem abtippen wollen und an der Anwendung des Programms interessiert sind. Deshalb zunächst eine Kurzbeschreibung für Anwender.
PROGRAMM : LISTING 31 0801 0A38 ----------------------------------- 0801 : 0B 08 C1 07 9E 32 30 36 0A 0809 : 31 00 00 00 A2 00 86 9D BA 0811 : A2 49 BD 1F 08 9D 3C 03 0F 0819 : CA 10 F7 4C 3C 03 A9 01 F7 0821 : A8 A2 00 20 BA FF A9 00 71 0829 : A2 5C A0 03 20 BD FF A9 C5 0831 : 00 20 D5 FF B0 03 4C 00 0B 0839 : 00 A2 1D 6C 00 03 00 00 77 0841 : 00 00 00 00 00 00 00 00 42 0849 : 00 00 00 00 00 00 86 2D BE 0851 : 84 2E 20 44 E5 A2 03 86 09 0859 : C6 BD 83 03 9D 77 02 CA 72 0861 : 10 F7 4C 74 A4 52 D5 0D 5D 0869 : 20 44 E5 A9 21 A0 09 20 D5 0871 : 1E AB 20 FD AE 20 8A AD 9E 0879 : 20 F7 B7 A6 14 A5 15 8E 37 0881 : 38 08 8D 39 08 20 CD BD 7C 0889 : A2 0F A9 00 9D 3F 08 CA A7 0891 : 10 FA 8D 0E 08 A9 03 8D 38 0899 : 36 08 A9 A2 8D 22 08 A9 EF 08A1 : 42 A0 09 20 1E AB A2 00 44 08A9 : 20 CF FF C9 0D F0 08 9D 9E 08B1 : 3F 08 E8 E0 10 D0 F1 8E B7 08B9 : 28 08 A9 50 A0 09 20 1E 69 08C1 : AB 20 CF FF 38 E9 30 8D 1F 08C9 : 23 08 D0 0A A9 A6 8D 22 B0 08D1 : 08 A9 BA 8D 23 08 A9 74 10 08D9 : A0 09 20 1E AB 20 1B 0A 06 08E1 : F0 0A A9 6C A0 03 8D 38 97 08E9 : 08 8C 39 08 A9 88 A0 09 FA 08F1 : 20 1E AB 20 1B 0A D0 05 5F 08F9 : A9 80 8D 0E 08 A9 9A A0 81 0901 : 09 20 1E AB 20 1B 0A F0 FC 0909 : 05 A9 00 8D 36 08 A9 B1 42 0911 : A0 09 20 1E AB A9 69 85 BA 0919 : 2D A9 08 85 2E 4C 74 A4 2E 0921 : 4C 4F 41 44 45 52 2D 4D 24 0929 : 41 4B 45 52 20 36 34 0D 4A 0931 : 0D 53 54 41 52 54 41 44 7A 0939 : 52 45 53 53 45 20 3A 20 EC 0941 : 00 0D 0D 46 49 4C 45 4E 7D 0949 : 41 4D 45 20 3A 20 00 0D 45 0951 : 0D 47 45 52 41 45 54 45 B8 0959 : 4E 52 2E 20 28 31 2D 39 93 0961 : 3B 30 3D 55 45 42 45 52 CE 0969 : 4E 45 48 4D 45 4E 29 20 C1 0971 : 3A 20 00 0D 0D 4D 41 53 44 0979 : 43 48 49 4E 45 4E 50 52 A9 0981 : 4F 47 52 41 4D 4D 00 0D 8A 0989 : 0D 53 59 53 54 45 4D 4D 40 0991 : 45 4C 44 55 4E 47 45 4E 89 0999 : 00 0D 0D 4C 4F 41 44 20 3D 09A1 : 45 52 52 4F 52 20 20 41 B7 09A9 : 55 53 47 45 42 45 4E 00 AA 09B1 : 0D 0D 12 2A 2A 2A 20 4C 1C 09B9 : 4F 41 44 45 52 20 47 45 30 09C1 : 4E 45 52 49 45 52 54 20 E8 09C9 : 2A 2A 2A 0D 0D 4D 49 54 3E 09D1 : 20 27 53 41 56 45 27 20 EE 09D9 : 53 50 45 49 43 48 45 52 FF 09E1 : 4E 2C 20 4D 49 54 20 27 FD 09E9 : 52 55 4E 27 20 53 54 41 CF 09F1 : 52 54 45 4E 00 0D 0D 12 49 09F9 : 2A 2A 2A 20 50 52 4F 47 2A 0A01 : 52 41 4D 4D 45 4E 44 45 53 0A09 : 20 21 20 2A 2A 2A 0D 0D 49 0A11 : 00 20 28 4A 2F 4E 29 3F FD 0A19 : 20 00 A9 12 A0 0A 20 1E FD 0A21 : AB 20 CF FF C9 5F D0 0C C3 0A29 : 68 68 A9 F6 A0 09 20 1E 1E 0A31 : AB 4C 74 A4 C9 4A 60 92
»Loader-Maker« ermöglicht es Ihnen, zu einem Programm ein (Maschinensprache-) Ladeprogramm zu generieren, welches normal geladen und mit »RUN« gestartet wird, worauf es das nachzuladende Programm nachlädt und startet.
Nach dem Laden von »Loader-Maker« wird dieses Programm durch SYS 2154,START gestartet. START ist eine Variable und wird durch die Startadresse des nachzuladenden Programms ausgedrückt. Soll ein Basic-Programm nachgeladen werden, hat diese Adresse keine Bedeutung (einfach SYS2154,0 eingeben). Bei einem Maschinenprogramm handelt es sich hier um die Adresse, mit der das Programm über »SYS« gestartet wird (49152 beim SMON $C000).
Das Programm meldet sich mit »Loader-Maker 64« und gibt die Startadresse aus. Dazu können Sie den Filenamen eingeben.
Bei allen weiteren Eingaben (Gerätenummer, von der geladen werden soll; Maschinenprogramm j/n; Systemmeldungen wie »SEARCHING FOR« ausgeben j/n; LOAD ERROR bei Ladefehler ausgeben j/n) können Sie das Programm durch Eingabe des Linkspfeils abbrechen. Sind alle Eingaben gemacht worden, kommt die Meldung »LOADER GENERIERT« und der Lader kann mit »SAVE« gespeichert werden.
Wenn das nachzuladende Programm von der Adresse geladen werden soll, von der auch das Ladeprogramm selbst eingelesen wurde, ist als Gerätenummer nur 0 einzugeben.
Befassen wir uns nun mit dem Programm, dessen Quelltext Sie als Listing 30 finden.
100 -.BA $0801 110 -.OB "LOADER-MAKER 64,P,W" 120 -; 130 -; 140 -; ***************************** 150 -; * * 160 -; * L O A D E R - M A K E R * 170 -; * * 180 -; ***************************** 190 -; * * 200 -; * EIN PROGRAMMGENERATOR * 210 -; * * 220 -; * VON FLORIAN MUELLER * 230 -; * * 240 -; ***************************** 250 -; 260 -; 270 -; 280 -.GL BASIN = $FFCF 290 -.GL SETPAR = $FFBA 300 -.GL SETNAM = $FFBD 310 -.GL LOAD = $FFD5 320 -.GL READY = $A474 330 -.GL NUMOUT = $BDCD 340 -.GL TASTPF = 631 ; TASTATURPUFFER 350 -.GL ANZAHL = 198 ; ENTHAELT ANZAHL 360 -; DER ZEICHEN IM 370 -; TASTATURPUFFER 380 -.GL KASSPF = 828 ; KASSETTENPUFFER 390 -; 400 -; 410 -.MA PRINT (TEXT) 420 - LDA #<(TEXT) ; MAKRO 430 - LDY #>(TEXT) ; FUER 440 - JSR $AB1E ; TEXTAUSGABE 450 -.RT 460 -; 470 -; 480 -; 490 -; 500 -.WO LINK+1 ; LINKPOINTER 510 -.WO 1985 ; ZEILENNUMMER 520 -.BY $9E ; TOKEN FUER "SYS" 530 - .TX "2061" 540 -LINK .BY 0,0,0 ; ENDMARKIERUNG 550 -; DER BASIC-ZEILE 560 -; 570 -SYSTEM LDX #0 ; FLAG FUER SYSTEM- 580 - STX $9D ; MELDUNGEN SETZEN 590 -; 600 - LDX #$49 ; DEKR.-ZAEHLER 610 -SCHLEIFE1 LDA ABLAGE,X ; LADEROUTINE 620 - STA KASSPF,X ; VON ABLAGE IN 630 - DEX ; DEN BEREICH 640 - BPL SCHLEIFE1 ; KOPIEREN, IN 650 -; DEM SIE LAEUFT 660 - JMP KASSPF ; & STARTEN 670 -; 680 -; 690 -; ES FOLGT DIE LADEROUTINE, DIE HIER 700 -; AN FALSCHER STELLE ABGELEGT IST UND 710 -; VON DER "SCHLEIFE1" (600-640) IN 720 -; DEN ORIGINALBEREICH GESCHRIEBEN WIRD. 730 -; 740 -ABLAGE LDA #1 ; FILENUMMER #1 750 - TAY ; SEKUNDAERADRESSE #1 760 -GERAETENR LDX #0 ; GERAETEADRESSE #? 770 - JSR SETPAR ; PARAMETER SETZEN 780 -; 790 -LAENGE LDA #0 ; LAENGE DES FILENAMEN 800 - LDX #<($35C) ; ADRESSE DES 810 - LDY #>($35C) ; FILENAMEN: $035C 820 - JSR SETNAM ; NAMEN SETZEN 830 -; 840 - LDA #0 ; FLAG FUER "LADEN" 850 - JSR LOAD 860 -; 870 -FEHLER BCS LOADERROR ; LADEFEHLER? 880 -START JMP 0 ; ZUR STARTADRESSE 890 -LOADERROR LDX #$1D ; "LOAD ERROR" 900 - JMP ($300) ; AUSGEBEN 910 -; 920 -NAME .BY 0,0,0,0 ; 16 BYTES 930 - .BY 0,0,0,0 ; FUER FILENAMEN 940 - .BY 0,0,0,0 ; RESERVIEREN 950 - .BY 0,0,0,0 960 -; 970 -BASIC STX $2D ; POINTER FUER 980 - STY $2E ; PROGRAMMENDE SETZEN 990 - JSR $E544 ; = PRINT CHR$(147) 1000 - LDX #3 ; 3 BYTES IN 1010 - STX ANZAHL ; TASTATURPUFFER 1020 -; 1030 -SCHLEIFE2 LDA $0383,X ; AUS DER TABELLE 1040 - STA TASTPF,X ; IN ZEILE 1100 1050 - DEX ; KOPIEREN 1060 - BPL SCHLEIFE2 1070 -; 1080 - JMP READY ; WARMSTART 1090 -; 1100 -.BY "R",$D5,13 ; "R",SHIFT U,RETURN 1110 -; 1120 -; HIER ENDET DER PROGRAMMTEIL, 1130 -; DER MODIFIZIERT WIRD. 1140 -; ES FOLGT DIE MODIFIKATIONSROUTINE: 1150 -; 1160 -MDFIKATOR JSR $E544 ; = PRINT CHR$(147) 1170 -...PRINT (TEXT1) 1180 -; STARTADRESSE HOLEN 1190 -; 1200 - JSR $AEFD ; PRUEFT AUF KOMMA 1210 - JSR $AD8A ; HOLT PARAMETER 1220 - JSR $B7F7 ; NACH $14/$15 1230 -; 1240 - LDX $14 ; STARTADRESSE 1250 - LDA $15 ; HOLEN, 1260 - STX START+1 ; IM PROGRAMM 1270 - STA START+2 ; ABLEGEN UND 1280 - JSR NUMOUT ; UND AUSGEBEN 1290 -; 1300 -; 1310 -; NUN WIRD NOCH DER ZU MODIFIZIERENDE 1320 -; PROGRAMMTEIL IN DEN AUSGANGSZUSTAND 1330 -; GEBRACHT: 1340 -; 1350 - LDX #15 ; NAMEN MIT NULL-BYTES 1360 - LDA #0 ; BELEGEN 1370 -SCHLEIFE3 STA NAME,X ; DURCH EINE 1380 - DEX ; DEKREMENTIER- 1390 - BPL SCHLEIFE3 ; SCHLEIFE 1400 -; 1410 - STA SYSTEM+1 ; KEINE SYSTEMMELDUNGEN 1420 -; 1430 - LDA #3 ; SPRUNGWEITE = 3 1440 - STA FEHLER+1 1450 -; 1460 - LDA #$A2 ; OPCODE FUER "LDX #" 1470 - STA GERAETENR 1480 -; 1490 -; 1500 -; AN DIESER STELLE IST DAS "GERUEST" 1510 -; (DER ZU MODIFIZIERENDE TEIL) 1520 -; IM AUSGANGSZUSTAND 1530 -; 1540 -; 1550 -; EINGABE DES FILENAMEN 1560 -; ===================== 1570 -; 1580 -...PRINT (TEXT2) 1590 - LDX #0 ; ZAEHLER AUF 0 1600 -SCHLEIFE4 JSR BASIN 1610 - CMP #13 ; ENDE DER EINGABE? 1620 - BEQ WEITER1 ; JA=>WEITER 1630 - STA NAME,X ; BYTE ABLEGEN 1640 - INX 1650 - CPX #16 ; 16 ZEICHEN MAX. 1660 - BNE SCHLEIFE4 ; NAECHSTES ZEICHEN 1670 -; 1680 -; WENN DIESE STELLE DURCHLAUFEN WIRD, 1690 -; HAT DAS X-REGISTER DEN WERT 16. 1700 -; 1710 -; BEI "WEITER1" HINGEGEN KANN ES AUFGRUND 1720 -; DES BRANCH-BEFEHLS "BEQ WEITER1" 1730 -; UNTERSCHIEDLICHE WERTE HABEN. 1740 -; 1750 -WEITER1 STX LAENGE+1 1760 -; 1770 -; 1780 -; EINGABE DER GERAETEADRESSE 1790 -; ========================== 1800 -; 1810 -...PRINT (TEXT3) 1820 - JSR BASIN ; HOLT ZEICHEN 1830 - SEC ; VOR SUBTRAKTION 1840 - SBC #"0" ; IM AKKU STEHT JETZT 1850 -; DIE ZAHL 1860 -; 1870 - STA GERAETENR+1; ABLEGEN 1880 - BNE WEITER2 ; GERAET<>0 : WEITER 1890 -; DA ALS GERAETENUMMER 0 EINGEGEBEN 1900 -; WURDE, MUSS DER GESAMTE BEFEHL 1910 -; "LDX #GERAET" IN "LDX $BA" 1920 -; UMGEWAENDELT WERDEN, DAMIT DAS 1930 -; NACHLADEN VON DEM GERAET ERFOLGT, 1940 -; VON DEM DER LADER EINGELESEN WIRD. 1950 -; 1960 - LDA #$A6 ; OPCODE FUER "LDX ZP" 1970 - STA GERAETENR 1980 - LDA #$BA ; "LDX $BA" 1990 - STA GERAETENR+1; GENERIEREN 2000 -; 2010 -; 2020 -; MASCHINENPROGRAMM (J/N)? 2030 -; ======================== 2040 -; 2050 -WEITER2 ... PRINT(TEXT4) 2060 - JSR JANEIN ; (JA/NEIN)? 2070 - BEQ WEITER3 ; JA=>WEITER 2080 - LDA #$6C ; SPRUNG AUF $036C 2090 - LDY #$03 ; VERBIEGEN 2100 - STA START+1 ; BEI $36C STEHT 2110 - STY START+2 ; EINE ROUTINE, 2120 -; DIE DEN "RUN"- 2130 -; BEFEHL SIMULIERT 2140 -; 2150 -; 2160 -; SYSTEMMELDUNGEN (J/N)? 2170 -; ====================== 2180 -; 2190 -WEITER3 ... PRINT(TEXT5) 2200 - JSR JANEIN ; (JA/NEIN)? 2210 - BNE WEITER4 ; NEIN=>WEITER 2220 - LDA #$80 ; FLAG FUER 2230 - STA SYSTEM+1 ; SYSTEMMELDUNGEN 2240 -; 2250 -; 2260 -; LOAD ERROR AUSGEBEN (J/N) 2270 -; ========================== 2280 -; 2290 -; 2300 -WEITER4 ... PRINT(TEXT6) 2310 - JSR JANEIN ; (JA/NEIN)? 2320 - BEQ WEITER5 ; NEIN=>WEITER 2330 - LDA #0 ; FEHLERMELDUNGEN 2340 - STA FEHLER+1 ; UNTERDRUECKEN 2350 -; 2360 -; 2370 -; PROGRAMMENDE 2380 -; ============ 2390 -; 2400 -; 2410 -WEITER5 ... PRINT(TEXT7) 2420 -; 2430 -; VEKTOR FUER BASIC-ENDE SETZEN 2440 -; ============================= 2450 -; 2460 -; 2470 - LDA #<(MDFIKATOR) 2480 - STA $2D ; LOW-BYTE 2490 - LDA #>(MDFIKATOR) 2500 - STA $2E ; HIGH-BYTE 2510 - JMP READY ; SPRUNG INS BASIC 2520 -; 2530 -; 10000 -; 10010 -; ASCII-TABELLEN 10020 -; ============== 10030 -; 10040 -; 10050 -TEXT1 .TX "LOADER-MAKER 64" 10060 - .BY 13,13 10070 - .TX "STARTADRESSE : " 10080 - .BY 0 10090 -; 10100 -TEXT2 .BY 13,13 10110 - .TX "FILENAME : " 10120 - .BY 0 10130 -; 10140 -TEXT3 .BY 13,13 10150 - .TX "GERAETENR. (1-9;0=UEBERNEHMEN) : " 10160 - .BY 0 10170 -; 10180 -TEXT4 .BY 13,13 10190 - .TX "MASCHINENPROGRAMM" 10200 - .BY 0 10210 -; 10220 -TEXT5 .BY 13,13 10230 - .TX "SYSTEMMELDUNGEN" 10240 - .BY 0 10250 -; 10260 -TEXT6 .BY 13,13 10270 - .TX "LOAD ERROR AUSGEBEN" 10280 - .BY 0 10290 -; 10300 -TEXT7 .BY 13,13,18 10310 - .TX "*** LOADER GENERIERT ***" 10320 - .BY 13,13 10330 - .TX "MIT 'SAVE' SPEICHERN," 10340 - .TX " MIT 'RUN' STARTEN" 10350 - .BY 0 10360 -; 10370 -TEXT8 .BY 13,13,18 10380 - .TX "*** PROGRAMMENDE ! ***" 10390 - .BY 13,13,0 10400 -; 10410 -TEXT9 .TX " (J/N)? " 10420 - .BY 0 10430 -; 10440 -; 20000 -; 20010 -; UNTERPROGRAMM FUER "J/N?" 20020 -; ========================= 20030 -; 20040 -; 20050 -JANEIN ... PRINT(TEXT9) 20060 - JSR BASIN ; EINGABE HOLEN 20070 - CMP #"_" 20080 - BNE JANEIN1 20090 - PLA ; SIEHE STAPEL- 20100 - PLA ; MANIPULATION 20110 -...PRINT(TEXT8) 20120 - JMP READY ; SPRUNG INS BASIC 20130 -JANEIN1 CMP #"J" ; VERGLEICH MIT "J" 20140 - RTS ; RUECKKEHR VOM 20150 -; UNTERPROGRAMM 20160 -.EN
Die Zeilen bis 990 stellen das Ladeprogramm in unmodifizierter Form dar und enthalten viele Dummywerte, wie zum Beispiel die (unsinnige) Startadresse 0 in Zeile 820.
Mit 1000 beginnt die Modifikationsroutine. Nach 1120 wurde die Startadresse eingelesen, die ja per SYS übergeben wurde, und wird wieder mit dem Titel ausgegeben. 1100/1110 schreiben die Startadresse hinter den JMP-Befehl in Zeile 820.
1150 - 1350 bringen das (noch unmodifizierte) Gerüst in den Ausgangszustand, der dann nach Bedarf geändert wird.
1400 - 1550 holen den Filenamen, legen ihn bei NAME (850) ab, berechnen gleich die Länge des Filenamens und legen diese bei LAENGE (750) ab.
1600 - 1720 holen die Geräteadresse. Da diese im ASCII-Fbrmat vorliegt, muß der ASCII-Code von 0 abgezogen werden (1640/1650). Wurde 0 eingegeben, wird der LDX # DEVICE-Befehl (730) in »LDX $BA« geändert. Die Adresse $BA enthältjeweils die Adresse, von der das letzte Programm geladen wurde.
1750 - 1850 fragen, ob das nachzuladende Programm mit der per SYS übermittelten Startadresse gestartet wird (Eingabe »j«). Wurde »n« eingegeben, muß das Programm über den Basic-Befehl RUN eingegeben werden. Auf eine entsprechende Routine (870 - 980) wird die Startadresse gestellt (1810 - 1840).
1900 - 1970 ermöglichen die Einstellung, ob »SEARCHING..«, »LOADING« etc. ausgegeben werden sollen.
Soll im Falle eines Ladefehlers das Programm nicht gestartet und stattdessen »LOAD ERROR..« ausgegeben werden, wird dies bei 2000 - 2090 festgelegt. Wird die Fehlerausgabe unterdrückt, muß der BCS-Befehl (810) unschädlich gemacht werden. Dies geschieht einfach dadurch, daß die Sprungweite auf 0 gesetzt wird (2070/2080).
Am Programmende wird noch eine Meldung ausgegeben (2140 - 2160) und der Vektor für das Ende des Basic-Programms neu gesetzt, damit das generierte Ladeprogramm mit »SAVE« gespeichert werden kann.
10000 - 10310 enthalten nur die Text-Tabellen.
Von 15000 bis zur letzten Zeile (15170) steht ein Unterprogramm, daß bei jeder J/N-Entscheidung über »JSR J,N« aufgerufen wird.
Es gibt den Text »(J/N)?« aus (15030 - 15050) und holt eine Eingabe. Ist diese »J«, so ist nach dem Verlassen des Unterprogramms (1517) das Zero-Flag gesetzt (andernfalls < nicht).
Wurde der Linkspfeil eingegeben, wird das Programm abgebrochen und eine entsprechende Meldung ausgegeben (15100- 15150).
Wie wir nun gesehen haben, handelfes sich bei »Loader-Maker« um einen Programmgenerator. Mit zwei kleinen Änderungen wird erjedoch zum selbstmodifizierenden Ladeprogramm. Wir müssen nur die beiden »JMP READY.«-Befehle (2240/15150) in »JMP SYSTEM« umwandeln, wodurch am Programmende der generierte Lader angesprungen würde. Schon hätten wir ein selbstmodifizierendes Ladeprogramm.
Um Ihnen noch die Anwendung des Loader-Maker zu erleichtern, hier zwei Eingabebeispiele:
| Startadresse | 49152 |
| Filename | SMON $C000 |
| Geräteadresse | 0 |
| Maschinenprogramm | j |
| Systemmeldungen | j |
| LOAD ERROR ausgeben | j |
| Startadresse | 0 (bedeutungslos) |
| Filename | HI-EDDI |
| Geräteadresse | 8 |
| Maschinenprogramm | n |
| Systemmeldungen | n |
| LOAD ERROR ausgeben | j |
g) Verbesserungen an »Tabellen-Beispiel«
Zum Abschluß des Themas »Selbstmodifikation« wollen wir noch kleine Verbesserungen am Programm »Tabellen-Beispiel« erwähnen. Ich werde hier eher Anregungen geben als fertige Änderungsvorschläge.
Zunächst soll die Adresse XSÄVE (zum Sichern des X-Registers in Schleifen) überflüssig werden. So könnte es nun gesichert werden:
| XSV | STX GETX | |
| … | ||
| GETX | LDX #$00 | 0=Dummy; hier wird X wieder aufgenommen. |
Auch die Sprungtabelle läßt sich - viel einfacher, finde ich - anders handhaben:
| LDA J?LO,X | JMLO oder JELO | |
| STA SPRO+1 | ||
| LDA J?HI,X | JMHI oder JEHI | |
| STA SPRG+2 | ||
| SPRG | JMP 0000 |
In den Tabellen JMLO/JMHI und JELO/JEHI (Low- und High-Bytes der Sprungadressen) dürfen die Adressen aber nicht dekrementiert werden.
Wird ein JSR (IND)-Befehl simuliert, muß nach wie vor die Rücksprungadresse auf den Stapel gelegt werden. Dies würde entfallen, wenn die Rücksprungadresse direkt auf »SPRG JMP 0000« folgen und der JMP-Befehl bei SPRG in JSR umgewandelt würde.
Damit soll das Thema »Selbstmodifikation« abgeschlossen sein. Die vorgestellten Programmiertechniken bieten fast unbegrenzte Möglichkeiten, hier konnte ich nur einen kleinen Überblickgeben, welcher aber für fortgeschrittene Programmierer ausreicht.
11. Mehr über relative Adressierung
So wie wir schon die Tücken der Zeropage-Adressierung zumindest teilweise beseitigen konnten, wollen wir uns mit der in vergleichbarer Weise leistungsstarken Relativ-Adressierung auseinandersetzen.
a) So vermeidet man JMP
Oft muß eine Stelle im Programm angesprungen werden, ohne daß erst eine Bedingung geprüft wird. Diese Stelle ist nicht selten weniger als 128 Byte vom Sprungbefehl entfernt, könnte also relativ adressiert werden.
Dennoch ist es in vielen Fällen möglich, einen Branch-Befehl - obwohl diese Befehle eine Bedingung (C=0..) prüfen - zu verwenden.
Beispiel:
7050 BNE 7040
7052 JMP 708A
Kann ersetzt werden durch:
7050 BNE 7040
7052 BEQ 708A
da bei 7052 in jedem Fall das Z-Flag = 0 ist (dafür sorgt der Abfang-Befehl BNE) und somit immer verzweigt wird.
Man könnte den BEQ-Befehl als »Pseudo-Verzweigungsbefehl« bezeichnen, da die Bedingung gar nicht überprüft werden müßte (sie ist sowieso erfüllt).
Der Branch-Befehl übertrifft den JMP-Befehl deutlich an Effektivität, da ein Byte weniger verbraucht wird.
Im übrigen ist auch bei
7050 BVS 7040
7052 CLV
der CLV-Befehl überflüssig, solange vor7052 der Befehl von 7050 verarbeitet wird.
b) Zugriff auf Befehle in »Umgebung«
Unter »Umgebung« wollen wir den Bereich um einen Programmteil verstehen, der über relative Adressierung angesprochen werden kann. Da in diesem oft ähnliche Befehlsfolgen stehen wie im anderen Programm, läßt sich hier durch gezielten Zugriff auf die »Umgebung« der Speicherplatzbedarf senken.
Beispielsweise stehen an vielen Stellen im Programm RTS-Befehle. Diese werden, wenn ein Unterprogramm verlassen werden soll, manchmal durch einen Branch-Befehl angesprungen.
| X1 | RTS | ; Ende eines im Speicher vorausgehenden Unterprogramms |
| UP | …… | ; Unterprogramm |
| TEST | BEQ X2 | ; Unterprogramm verlassen, falls Z=0 |
| ……… | ; andernfalls weiteres Programm | |
| X2 | RTS ; Ende des Unterprogramms |
Wenn X1 von TEST aus relativ adressiert werden kann, können wir folgendermaßen ein Byte sparen:
| X1 | RTS | |
| UP | … | |
| TEST | BEQ X1 | ; nach X1 springen, wo auch ein RTS steht |
| X2 | RTS | ; wird nicht mehr benötigt |
Noch ein Beispiel aus dem Basic-Interpreter. Bei Adresse $AF08 stehen zwei Befehle, die einen SYNTAX ERROR erzeugen.
Nun gibt es im Basic-Interpreter unzählige Stellen, an denen ein SYNTAX ERROR aufgerufen werden muß. Deshalb steht dort nur »JMP $AF08«. Diese Stellen werden bei Bedarf relativ adressiert, so daß nicht an jeder Stelle, an der ein SYNTAX ERROR aufgerufen wird, der Befehl »JMP $AF08« stehen muß.
Zur Übung könnten Sie noch versuchen, im Programm Tabellen-Beispiel (Listing 11) die Menüroutine (insbesondere die Routinen HOME, DOWN, UP, EXEC), in der beispielsweise wiederholt STX MPT steht, durch Zugriff auf »Umgebung« zu optimieren. Besonders hilfreich dürfte es sein, zunächst statt Branch-Befehlen JMPs einzusetzen und dann zu überlegen, inwieweit die JMPs durch Branches ersetzt werden können, weil zum Beispiel nach »LDX # 0« das Z-Flag immer gesetzt ist etc.
12. Puffer-Technik
In der Computerei fällt der Begriff »Puffer« sehr häufig. Beim C 64 gehören der Kassetten- und der Tastaturpuffer gemeinhin zu den bekanntesten Puffern. Statt »Puffer« kann man auch Zwischenspeicher sagen. Puffer dienen nämlich immer als Zwischenspeicher.
Zunächst wollen wir klären, was zu einem Puffer gehört,
a) Was benötigt ein Puffer?
– Pufferspeicher
Selbstverständlich muß ein Puffer einen bestimmten Speicherbereich belegen, in dem die Werte zwischengespeichert werden.
Ebenso muß die maximale Puffergröße festgelegt werden, damit geprüft werden kann, ob sich der Puffer schon angefüllt hat. Beim Kassettenzugriff werden vorerst alle Byte, die auf die Kassette sollen, im Puffer (ab $033C) zwischengespeichert. Ist dieser Puffer voll, würde er beim nächsten Byte, das er aufnehmen soll, überlaufen (das heißt, die maximale Puffergröße überschreiten). Deshalb wird dann Byte für Byte der Puffer entleert, indem die Bytes auf Kassette geschrieben werden. Jedes Byte, das auf Kassette geschrieben wurde, belegt keinen Speicher mehr im Puffer, so daß der Puffer wieder aufnahmefähig ist.
Damit das Programm, das den Puffer verwaltet, auch weiß, aus welcher Adresse im Puffer es sich das nächste Byte holen soll beziehungsweise wo im Puffer das nächste Byte abgelegt werden soll, gibt es noch einen
– Pufferzeiger
Auf englisch heißt er »BUFFER-POINTER«, woher auch die Abkürzung »B-P« beim Floppy-Befehl zur Manipulation des Pufferzeigers stammt.
Dieser Pufferzeiger kann mit dem Stapelzeiger verglichen werden. Auf keinen Fall ist er mit dem
– Puffervektor
zu verwechseln, der die Startadresse des Pufferspeichers beinhaltet. Ein Puffervektor ist nicht unbedingt erforderlich, erhöht aber die Flexibilität.
Damit wären die Fachausdrücke im Zusammenhang mit Puffern geklärt.
b) Wann verwendet man Puffer?
Puffer dienen in der Regel als Zwischenspeicher, wie zum Beispiel der Basic-Eingabepuffer (ab $0200).
Im Fall des Tastatur- oder Diskettenpuffers aber sind die Puffer als Verbindungsstelle zwischen zwei parallel arbeitenden Programmen beziehungsweise Peripheriegeräten vorgesehen (interruptgesteuerte Tastaturabfrage/Hauptprogramm im Computer, DOS/Betriebssystem des Computers).
Die Puffer sind in diesen Fällen ein Bereich, auf den zwei (quasi-) parallel arbeitende Programme zugreifen.
Bei Computern, die ein wirklich starkes Multitasking bieten (wie der Commodore Amiga) finden Puffer weitaus mehr Verwendung als beim C 64, der nur einen quasiparallelen Ablauf ermöglicht.
Daher werden bei ihm Puffer hauptsächlich im I/O-Bereich verwendet, zum Beispiel bei Druckern, Datasette, Floppy, Tastatur etc. (I/O = lnput/Output = Eingabe/Ausgabe).
13. Pass-Technik
a) Begriffserläuterung
Der Begriff »Pass« wurde schon mehrfach im 64’er erläutert (unter anderem Ausgabe 7/85, Seite 51).
Am einfachsten kann der Begriff als »Schritt beim Programmmablauf« verstanden werden. Mit »Schritt« ist hier nicht ein einzelner Befehl, sondern ein größerer Block im Programm gemeint.
Wenn ein Programm in 3 Passes (Durchläufen) arbeitet, heißt dies, daß 3 Schleifen hintereinander abgearbeitet werden, die alle eine Teilaufgabe erfüllen, die in Verbindung mit den anderen Passes erst eine größere Aufgabe (zum Beispiel eine Assemblierung) ausfüllen kann. Jeder einzelne Pass führt eine bestimmte Tätigkeit aus, die für das Funktionieren der darauffolgenden Passes unbedingt erforderlich ist. Pass 1 wirkt also wie eine Initialisierung von Pass 2 etc.
Komplexe Programme in Schritte (Passes) zu gliedern, gehört zu den Grundregeln des strukturierten Programmierens.
b) Beispiele von Anwendungen der Pass-Technik
Besonders umfangreiche Programme wie Assembler (Hypra-Ass), Compiler (Austro-Speed) und Interpreter (Comal) sind immer in mehrere Passes eingeteilt.
So erfolgt bei den meisten Assemblern im ersten Pass ein Syntax-Check und das Anlegen der Symbol-Tabelle. Erst im zweiten Pass wird der Objektcode generiert, wobei die bereits erstellte Symboltabelle benötigt wird.
14. Diverse Tips zur optimalen Speichernutzung
Mit übermäßig viel RAM ist der C 64 bestimmt nicht gesegnet. Bei vielen Anwendungen (zum Beispiel Datenverarbeitung) braucht man auch das letzte Byte.
Sie werden nun mehrere Tips erhalten, wie man den wenigen vorhandenen Speicher möglichst sparsam verwenden kann.
Zu den speicherplatzaufwendigsten Einrichtungen gehören die Puffer. Der Kassettenpuffer beispielsweise belegt den RAM-Bereich $033C - $03FB, auf den man somit oft verzichten muß.
Hier wollen wir einfach den Kassettenpuffer in den Bildschirmspeicher (ab $0400 in Normaleinstellung) verlegen.
LDA #<$400
LDY #>$400
STA $B2
STY $B3
Da der Bildschirm beim Kassettenbetrieb ohnehin abgeschaltet wird, fällt dies nicht auf. Nach dem Kassettenbetrieb sollte man aber den Bildschirm unverzüglich löschen.
Ebenso kann man andere Puffer, für die es einen Vektor gibt, problemlos nach $400verlegen, sofern sie nicht größer als 1000 Byte sind.
Ein Problem für sich stellt das RAM ab $E000 (also unter dem Betriebssystem!) dar. Diesen Speicher kann man nur durch Bank-Switching nutzen, wobei man noch auf das Betriebssystem verzichten muß, solange der $E000-Bereich auf RAM geschaltet ist.
Hier können wir uns zunutze machen, daß der VIC auch ohne Ändern des Prozessor-Ports (Adresse $0001) auf diesen RAM-Bereichzugreifen kann. FürGrafikbilderodereinen geänderten Zeichensatz ist der$E000-Bereich bestens geeignet.
Oft wird der $E000-Bereich zur Ablage verschiedener Daten verwendet, auf die nicht andauernd zugegriffen werden muß.
Man könnte aber auch das Betriebssystem ins RAM ab $E000 kopieren und diejenigen Bereiche, in denen nicht benötigte Routinen stehen (zum Beispiel für Kassettenbetrieb) einfach überschreiben. Dies ist dann sinnvoll, wenn nur ein paar Byte im $E000-Bereich gebraucht werden. Außerdem ist eine gute Kenntnis des C 64-ROMs erforderlich.
Nun wollen wir noch besprechen, wie der Speicherplatzbedarf eines Programms niedriggehalten werden kann. Dazu wurde im Laufe des Kurses schon einiges gesagt (Unterprogramme statt Makros verwenden etc.).
Jedes Programm benötigt eine Menge Flags. Meist belegt ein Flag genau 1 Byte, für dessen Inhalt es oft nur zwei mögliche Werte gibt: einen für »JA« und einen für »NEIN«.
Für diese primitive Unterscheidungsform genügt aber auch 1/8 Byte, also ein Bit.
Wenn Sie sich das 64’er Extra in der Ausgabe 10/85 ansehen, werden Sie feststellen, daß fast jedes VIC-Register mehrere Funktionen hat, weil jedem Bit eine eigene Bedeutung zukommt. Würde der VIC hier statt auf Bits auf Bytes zugreifen müssen, wäre er
- langsamer und
- würde der Speicherplatzaufwand für die Register sich vervielfachen.
Man sollte also bei Flags jedem Bit eine Bedeutung geben und nur die Bits prüfen:
BIT FLAG
Danach ist das N-Flag gesetzt, falls das 7. Bit im FLAG gesetzt ist, und das V-Flag, falls das 6. Bit gesetzt ist. Die übrigen Flags erhält man über das Z-Flag im Prozessor-Status-Register mit Hilfe des Akkus. Angenommen, man möchte testen, ob Bit 0 im Flag gesetzt ist oder nicht, dann macht das folgendes Programm:
LDA #01
BIT Flag
BNE ??? ; (Bit gesetzt)
.
.
.
; (Bit nicht gesetzt)
Der Bit-Befehl ANDet den Inhalt des Akkus mit dem Inhalt der Speicherzelle »Flag«. Möchte man Bit 1 testen, so ist der Befehl LDA #01 zu ersetzen durch LDA #02 und so weiter.
Durch Selbstmodifikation können Flags bekanntlich vermieden werden. Aber auch sonst bietet die Selbstmodifikation die Möglichkeit, Speicherplatz zu sparen: die Steuerung einer Sprungtabelle belegt mit Selbstmodifikation weniger Speicher als ohne.
Auch die »Wegwerfmethode« ist sehr vorteilhaft. Programmteile werden einmal abgearbeitet und dann (zum Beispiel durch Nachladen) überschrieben.
Damit hätten wir unseren Kurs abgeschlossen. Ich hoffe, daß er Ihnen etwas Spaß gemacht hat und Sie einige interessante Informationen herausholen konnten. Sie sollten sich jedoch darüber im klaren sein, daß einige der hier vorgestellten Methoden die Lesbarkeit eines Assembler-Listings einschränken können. Also, verzichten Sie, wenn nicht unbedingt notwendig, auf allzu trickreiche Programmierung. Falls Sie noch Fragen oder Probleme haben (vielleicht erst wegen diesem Artikel), dann schreiben Sie doch einfach.
(Florian Müller/tr)