8.
Datei-I/O
Sie
kennen sich nun mit dem SID und dem VIC aus, und auch die CIA-Chips sind kein Geheimnis
mehr für Sie. Leider fehlt noch eine wichtige Sache, nämlich der Umgang mit
Dateien- darüber wissen Sie im Endeffekt noch relativ wenig. Vielleicht haben
Sie schon mit einigen Kernal-Funktionen rumgespielt,
und es sogar fertiggebracht, Ausgaben auf Ihren Drucker umzuleiten. Vielleicht
konnten Sie sogar sämtliche Tastatureingaben in eine Datei umleiten, und
dadurch eine Art Retro-Logfile programmieren. Wie aber eine Datei wirklich aufgebaut
ist, und was Ihre Floppy auf einer Diskette ablegt, wenn Sie SAVE
aufrufen, blieb Ihnen bis jetzt verborgen. Dies wollen wir jetzt nachholen,
indem wir uns eingehend mit der 1541-Floppy beschäftigen.
8.1 Grundlagen der
1541-Floppy
Die
Floppy-Station am C64 ist im Endeffekt ein eigener Computer, der auch ein ganz
eigenes Betriebssystem besitzt. Genau wie der C64 auch, enthält die 1541 einen
Prozessor (um genau zu sein den 6502, eine Variante des 6510), zwei CIA-Chips,
16 kB RAM, und ein Betriebssystem-ROM. Anders jedoch, als der C64, besitzt die
1541 keinen Bildschirmausgang, sondern muss die Daten über die serielle
Schnittstelle übertragen. Dazu verwenden Sie ein spezielles Kabel mit einem
DIN-Stecker, der an der Rückseite des C64 angeschlossen wird. Der C64
kommuniziert nun mit der Floppy per serieller Schnittstelle über ein spezielles
Protokoll, das Commodore als IEC-Protokoll
bzw. Commodore-Bus bezeichnet hat.
Dieser Commodore-Bus ist nun sehr
einfach aufgebaut: Es gibt für sämtliche Geräte nur eine einzige Datenleitung,
die sich alle Geräte teilen müssen, und auch die Daten werden seriell
übertragen, das heißt Bit für Bit.
Was
bedeutet dies nun für die Kommunikation mit der oder den Floppy-Stationen? Ganz
einfach: Es darf immer nur ein einziges Gerät senden, alle anderen Teilnehmer
am Bus können nur zuhören. Und da alle Geräte an einem Bus hängen, hören auch
alle Geräte immer dasselbe. Dies führt natürlich zunächst zu einer starken
Verwirrung, denn wer ist gemeint, wenn z.B. der C64 gerade Daten über den Bus
sendet? Gilt der gerade ausgegebene Text dem Drucker, oder soll doch lieber die
Floppy (eventuell sogar eine von mehreren Floppys) den Text in einer Datei
sichern? Um diese Verwirrung aufzulösen, gibt es Geräteadressen. Die
Geräteadressen sind fest vorgegeben, und können z.B. durch Dip-Schalter an der
Gehäuserückseite von Drucker oder Floppy eingestellt werden (wenn Sie mehrere
Floppys haben, müssen Sie dies sogar tun, denn sonst gibt es Chaos auf dem
Bus). Bevor nun ein Sender die eigentlichen Daten übermittelt, sendet er
zunächst ein Byte mit einer Geräteadresse. Da sämtliche Geräte mithören,
weiß der Empfänger auch anschließend, dass genau er gemeint ist. Was ist aber,
wenn der C64 der Empfänger ist, und dieser z.B. Bilddaten von einem Scanner
anfordern möchte? Für diese Zwecke gibt es zusätzlich zur ATN-Leitung (diese
zeigt ja nur die Bereitschaft eines Gerätes an, zuzuhören) spezielle
Kommando-Bytes, nämlich LISTEN und TALK. Diese Kommandos sind
immer vom Standpunkt des Gerätes aus zu sehen, das das
entsprechende Byte empfängt. Deshalb bedeutet LISTEN, dass der andere
Teilnehmer, der LISTEN empfängt, zuhören soll (Datenempfang), und TALK,
dass der andere Teilnehmer Daten senden soll. Nun besitzt die Floppy jedoch
auch ein Betriebssystem, das heißt, dass zusätzlich zu TALK und LISTEN
ganz spezielle Floppy-Kommandos übermittelt werden müssen, und dabei auch
Fehler auftreten können. Da die Floppy deshalb einen Datenkanal, einen
Fehlerkanal, und einen Kommandokanal haben muss, wird die Geräteadresse noch
einmal in zwei Teile aufgespalten, nämlich in die Primäradresse und die Sekundäradresse
(oft auch als Subkanal bezeichnet). Die
Sekundäradresse wird zusätzlich zur Geräteadresse übermittelt, und gibt eine
Art Übertragungskanal an. Im Fall der 1541 ist der Datenkanal mit der Sekundäradresse
1, der Fehlerkanal mit der Sekundäradresse 2, und der Kommandokanal
mit der Sekundäradresse 15 verknüpft.
Wenn Sie
nun ein Kommando an die 1541 senden wollen, wie z.B. “S:TEST“, dann
können Sie dies natürlich über die Kernal-Funktionen
erreichen: Sie müssen dann mit OPEN die Sekundäradresse 15 und die
Geräteadresse 8 an eine logische Dateinummer binden, anschließend LISTEN
aufrufen, und zum Schluss das Kommando übermitteln, das Sie vorher als Zeiger
auf einen ASCII-String im Speicher abgelegt haben. Sie können aber auch
alternativ den OPEN-Befehl von BASIC für diese Zwecke benutzen, indem
Sie die folgenden zwei Zeilen eingeben:
OPEN 15,8,15,"S:TEST"
CLOSE 15
Wie Sie
sehen, rufen die BASIC-Funktionen OPEN und CLOSE die
entsprechenden Kernal-Funktionen automatisch auf, und
verwenden auch die Parameter in der korrekten Reihenfolge. Sie müssen also für
den Umgang mit Dateien nur zusätzliche Floppy-Kommandos lernen.
8.1.1
Der Aufbau von Disketten
Obwohl
Ihnen BASIC eine Menge Arbeit abnimmt, ist es trotzdem nicht ganz unwichtig,
etwas über den Aufbau von Disketten zu wissen. Jede Diskette ist mit einer
Magnetschicht überzogen. Das heißt, Ihre Daten werden von der 1541 als
Magnetfelder abgelegt. Diese Magnetfelder werden sehr schnell vom
Schreib/Lesekopf der 1541 erzeugt, während die Diskette um ihre Achse rotiert.
Die Ausrichtung der Magnetfelder nach oben oder unten bestimmt hier, welches
Bit auf die Diskette geschrieben wird, und das Betriebssystem der 1541 nimmt
die Bits vorher über die serielle Schnittstelle entgegen. Sie müssen sich
selbst nicht um die Magnetisierung und um die Steuerung des Schreib/Lesekopfes
kümmern, dies übernimmt das Betriebssystem der 1541. Was Sie allerdings wissen
sollten, ist, dass die Daten auf der Diskette in konzentrischen Ringen, den
sogenannten Spuren (englisch Tracks) um das Zentrum herum
angeordnet sind. Es gibt auf einer Diskette von außen nach innen gesehen 35
Spuren (bei manchen kopiergeschützten Spielen auch 38 oder 40), und diese
Spuren sind noch einmal in Sektoren (englisch Sector)
unterteilt. In den Sektoren stehen nun die Daten in Form von Blöcken.
Die Blöcke
sind nun die Stellen, in denen die Daten stehen. Jeder Block kann 256 Bytes
aufnehmen. Die Anzahl der Sektoren pro Spur ist allerdings unterschiedlich, da
die Spuren zum Zentrum der Diskette hin kleiner werden. Deshalb können die
Spuren bis zur Spur 17 noch 21 Sektoren, die Spuren 31-35 aber nur noch 17
Sektoren aufnehmen. Dies führt am Ende zu
(17*21)+(7*19)+(6*18)+(5*17)=683 Blocks
die auf
einer Diskette maximal nutzbar sind. Da aber Spur 18 das Inhaltsverzeichnis
(Disk Content bzw. Directory) enthält, sind auf einer frisch
formatierten Diskette nur
683-19=664
Blocks
für
Dateien nutzbar. Neue Disketten können nun in zwei verschiedenen Formen im
Internet bestellt werden (z.B. bei Amazon, aber auch bei einigen Retro-Shops),
nämlich vorformatiert und unformatiert. Vorformatierte Disketten sind entweder
leer, oder aber sie enthalten zumindest auf der ersten Diskette in der Packung
zusätzliche Floppy-Tools. Unformatierte Disketten dagegen sind nur beschichtet
worden, und können deswegen nicht gelesen werden. Sie müssen solche Disketten
im Zweifelsfall mit folgendem BASIC-Kommando formatieren:
OPEN 15,8,15,"N:[Diskettenname],[ID]":CLOSE 15
Jede Diskette
bekommt immer einen Namen mit maximal 16 Zeichen Länge, und eine ID-Nummer
von 2 Zeichen Länge zugeteilt. Die 1541 muss diese Zuordnungen mit dem N-Kommando,
gefolgt von einem Doppelpunkt und den anschließenden Parametern, auf die
Diskette schreiben. Das Kommando N ist die Abkürzung von „new“
(neu), weshalb dann im Anschluss auch sämtliche Spuren und Sektoren neu
geschrieben werden. Erst, wenn dies geschehen ist, enthalten die Spuren und
Sektoren die korrekten Synchronisationsbits, sodass die 1541 Daten auf die
Diskette schreiben kann. Das N-Kommando führt stets zwei Schritte aus. Im
ersten Schritt werden die Synchronisationsbits erstellt. Im zweiten Schritt
wird die Spur 18 mit einem leeren Directory beschrieben. Ebenfalls wichtig ist
in diesem Fall auch das Anlegen der BAM (block availability map).
Die BAM ist eine Tabelle, in der für jeden Block angegeben wird, ob er noch
frei ist, und welchen Status er besitzt. Ohne BAM können Sie also keine neuen
Dateien anlegen. Die BAM befindet sich direkt vor dem Directory, als in Sektor
0 der Spur 18 und ist wie folgt aufgebaut (1541-Version mit 35 Spuren, Quelle https://www.c64-wiki.de/wiki/BAM):
|
0 |
Spurnummer für
Directory 18 |
$12 |
|
1 |
Startsektor für
Directory 1 |
$01 |
|
2 |
Formatkennzeichen |
"A" bei
1541/1570/1571 |
|
3 |
Flag für doppelseitige
Disketten |
$00 = einseitige Disk
(1541), $80 = doppelseitige
Disk (nur 1571) |
|
4 |
Anzahl der freien
Blöcke von Spur 1 |
|
|
5 |
Belegung für Sektor
0-7 |
Bit=0: Sektor belegt,
Bit=1: Sektor frei |
|
6 |
Belegung für Sektor
8-16 |
Bit=0: Sektor belegt,
Bit=1: Sektor frei |
|
7 |
Belegung für Sektor
17-20 (Sektoren 21-23 nicht vorhanden) |
Bit=0: Sektor belegt
Bit=1: Sektor frei |
|
8-143 |
Bedeutung wie Byte
4-7, aber für Spuren 2-35 |
|
|
144-159 |
Diskettenname, der
bei der Formatierung angegeben wurde, aufgefüllt mit "Shift Space"
160 ($A0) |
|
|
160-161 |
jeweils "Shift Space" |
160 ($A0) |
|
162-163 |
Diskettenidentifikation
(ID), die bei der Formatierung angegeben wurde |
|
|
164 |
"Shift
Space" |
160 ($A0) |
|
165 |
DOS-Version mit der
gearbeitet wird |
2 = CBM DOS V2.6
(spätere Versionen werden nicht aktualisiert) |
|
166 |
Kopie von Byte 2 |
bei 1541:
"A" bei 8050: "C" |
|
167-170 |
jeweils "Shift Space" |
160 ($A0) |
|
171-179 |
Modus |
$00=1541 $A0=1571 |
|
180-220 |
unbenutzt |
0 |
|
221-237 |
Bei 1541 unbenutzt,
bei 1571 Anzahl der freien Blöcke für Spur 36-52 |
|
|
238 |
Bei 1541 unbenutzt,
bei 1571 Anzahl der freien Blöcke für Spur 53 |
|
|
239-244 |
Bei 1541 unbenutzt,
bei 1571 Anzahl der freien Blöcke für Spur 54-59 |
|
|
245-250 |
Bei 1541 unbenutzt,
bei 1571 Anzahl der freien Blöcke für Spur 60-65 |
|
|
251-255 |
Bei 1541 unbenutzt,
bei 1571 Anzahl der freien Blöcke für Spur 66-70 |
|
Die BAM
speichert also für jeden freien Sektor ein Bit mit dem Wert 1, und wenn ein
Sektor von einer neuen Datei belegt wird, dann wird das entsprechende Bit in
der BAM zu 0 gesetzt. In der BAM wird aber auch der Diskettenname und die ID
abgelegt, sowie zusätzliche wichtige Informationen, die das Diskettenformat
angeben. Die BAM ist deshalb essentiell für das Funktionieren der Diskette-
wenn die BAM beschädigt wird, sind eventuell sämtliche Dateien verloren.
8.1.2
Der Aufbau von Dateien
Dateien sind also erst einmal
nichts anderes, als eine Sammlung von Blöcken. Damit auf diese Blöcke
korrekt zugegriffen werden kann, muss erst einmal ein Dateiname an OPEN
übergeben werden. Dieser Dateiname wird nun auf Spur 18 gesucht, und
wenn der Name gefunden wird, wird der Startblock und die Spur des
Startblocks bestimmt. Hierzu besitzt jede Datei einen Eintrag in der Directory auf Spur 18, der den Namen, die Größe, und
den Startblock angibt. Allerdings geschieht direkt nach dem Öffnen einer Datei
noch überhaupt nichts, und die Datei wird auch noch nicht in den Speicher
geladen. Das Laden erledigt das Kernal-ROM oder BASIC
durch zusätzliche Floppy-Kommandos. Zum Glück werden diese Kommandos gut von
BASIC versteckt, und Sie müssen auch hier wieder nur OPEN in der richtigen
Weise verwenden.
Für
Dateien gibt es also einen übergeordneten Aufbau aus Blöcken, und einen
internen Aufbau aus Daten-Bytes. Der wichtigste Block ist hier natürlich der Startblock,
der immer durch eine Spurnummer und eine Sektornummer
(in dieser Reihenfolge) angegeben wird. Angenommen, Ihre Datei beginnt auf Spur
1 in Sektor 1. Dann wird erst einmal dieser Block in einen Puffer im Speicher
geladen (in diesem Fall wird übrigens der Kassettenpuffer auch für die Floppy
benutzt). In diesem Block geben nun die ersten zwei Bytes Spur und Sektor (in
dieser Reihenfolge) des nächsten zu ladenden Blocks an (der sogenannte Jump To Link, oder einfach nur Link), und die
folgenden zwei Bytes die Startadresse, an die die Datei geladen werden soll in
Form von Lo- und Hi-Byte (in dieser Reihenfolge). Für BASIC-Programme
stehen in den ersten zwei Bytes der Datei immer die Werte 1 und 8
(Adresse 2049), für ausführbare Maschinenprogramme immer die
Werte für die Startadresse. Allerdings sind beim C64 nur diese beiden
Dinge fest vorgegeben, und Sie können auch z.B. für nachladbare Spiele-Level
Ihre Daten nach Ihren eigenen Vorstellungen in einer Datei ablegen.
Die Daten
selbst können nach Öffnen einer Datei mit dem PRINT#-Kommando auf die
Diskette geschrieben werden. BASIC versetzt die 1541 durch dieses Kommando in
den LISTEN-Modus, und Sie können durch Angabe eines entsprechenden
Parameters Daten in eine Datei schreiben. PRINT# schreibt normalerweise
Zeichenketten in derselben Weise in Dateien, in der Sie auch mit PRINT
Zeichen auf den Bildschirm schreiben. Das bedeutet, dass wenn Sie eine Datei
mit der Nummer 1 geöffnet haben, Sie mit
PRINT#1,"HALLO LEUTE!"
Diesen
Text 1:1 (inklusive Zeilenumbrüchen) in die Datei mit
der Dateinummer 1 schreiben, anstatt ihn auf dem Bildschirm auszugeben. Leider
können Sie in dieser Weise keine einzelnen Bytes mit einem bestimmten Wert in
eine Datei schreiben, z.B. die Startadresse $C000 Ihres Maschinenprogramms. Hierzu
müssen Sie die CHR$()-Funktion
benutzen. Wenn Sie z.B. die Startadresse 49152 in einer leeren Datei ablegen
wollen, müssen Sie die Werte 0 und 192 in der folgenden Weise in die Datei
schreiben:
OPEN 1,8,1,"TESTFILE"
PRINT#1,CHR$(0);
PRINT#1,CHR$(192);
Vergessen
Sie hier auf keinen Fall das Semikolon! Wenn Sie das Semikolon vergessen, dann
schreibt PRINT# die Bytes 0,13,192 und 13 in die Datei, weil der Wert 13
das ASCII-Zeichen für den Zeilenumbruch ist. Sie müssen dieses Verhalten also
(wie bei dem normalen PRINT-Befehl) durch ein Semikolon am Ende der
Zeile unterdrücken. Angenommen, Ihr Maschinenprogramm endet an der Adresse
50000, und Sie wollen nun die Bytes Ihres Maschinenprogramms ebenfalls in die
Datei übertragen. Dies erreichen Sie nun mit der folgenden Zeile:
FOR I=49152 TO 50000:PRINT#1,CHR$(PEEK(I));:NEXT I
Auch hier
darf das Semikolon vor dem NEXT-Befehl nicht fehlen. Wenn Sie alle Daten übertragen haben, dann
müssen Sie natürlich den letzten unfertigen Block, der sich am Ende noch im
Kassettenpuffer befindet, auf die Diskette übertragen, damit Sie wirklich vollständige
Dateien erhalten. Diese Aufgabe erledigt aber zum Glück der folgende CLOSE-Befehl
automatisch:
CLOSE 1
8.1.3
Die verschiedenen Dateitypen
Ihr C64 erstellt
normalerweise Programmdateien, wenn Sie OPEN die Standardparameter
übergeben. Es gibt jedoch die folgenden vier Dateitypen:
· Programmdateien (PRG):
Dies sind Dateien mit einer Startadresse, also entweder BASIC-Programme oder
Maschinenprogramme
· Benutzerdateien (USR):
Dies sind Dateien mit keiner fest definierten Struktur ohne Startadresse, die
auch nicht mit LOAD geladen werden können
· Sequentielle Dateien
(SEQ): Dies sind Dateien, die aus einer Sequenz von Datenblöcken einer festen
Größe bestehen. Die Größe der Datenblöcke ist 256 Bytes, die Daten werden also
immer an der Größe von Sektoren ausgerichtet. Sequenzielle Dateien können nicht
mit LOAD geladen werden.
· Relative Dateien (REL):
Dies sind Dateien, die einen wahlfreien Zugriff auf einzelne Bytes ermöglichen
(random access files). Relative Dateien können nicht mit LOAD geladen
werden, sondern erfordern spezielle Tools (z.B. Datenbank-Software).
Wenn Sie
eine Datei erstellen wollen, die keine Programmdatei ist, müssen Sie andere
Parameter an OPEN übergeben. Auch die Anweisungen, die Sie an INPUT#
und PRINT# übergeben müssen, unterscheiden sich von dem Vorgehen bei
Programmdateien. In den meisten Büchern werden die Dateitypen USR, SEQ und REL
nicht sehr ausführlich behandelt, ich möchte dies aber trotzdem tun, weil Sie
mit den verschiedenen Dateitypen sehr viele interessante Dinge realisieren
können. So können Sie z.B. mit sequentiellen Dateien Datenbanken aufbauen, oder
aber Ihre Spiele-Level in einer sehr kurzen Zeit nachladen (vorausgesetzt, die
Level besitzen stets die gleiche Größe). Ferner können Sie auch erweiterbare
Dateien erstellen, an die Sie Daten einfach anhängen können, ohne immer wieder
die gesamte Datei neu erstellen zu müssen. Ich werde Ihnen nun die einzelnen
Dateitypen kurz vorstellen, und auch beschreiben.
USR-Dateien
USR ist die Abkürzung von
„user file“, also von
benutzerdefinierten Dateien. Deshalb können USR-Dateien beliebige Bytes
enthalten, z.B. Spiele-Level, Songdaten, Hintergrundbilder, etc. Auf Daten in
USR-Dateien kann jedoch nicht wahlfrei zugegriffen werden, deshalb müssen diese
immer komplett in den Speicher geladen werden. Der Vorteil von USR-Dateien ist
jedoch, dass diese nicht mit LOAD geladen werden können, da hier die
Bytes für die Startadresse nicht benutzt werden. Sie können also in diesem Fall
nicht versehentlich einen Spiele-Level laden, sondern sehen sofort, dass Sie
einen separaten Loader benutzen müssen. USR-Dateien
können mit den Standard-Befehlen OPEN, PRINT#, GET# und INPUT#
erstellt, beschrieben und ausgelesen werden. Allerdings muss nach dem
Dateinamen im OPEN-Befehl der Parameter “,U“
angehängt werden.
PRG-Dateien
PRG ist die Abkürzung von
„program file“, also einer
ausführbaren Datei. Im Gegensatz zur USR-Datei bestimmen die ersten beiden
Bytes in der Datei, an welche Adresse die Daten mit LOAD geladen werden
sollen. Hierbei wird allerdings nicht wirklich zwischen Maschinenprogrammen und
BASIC-Programmen unterschieden, sondern es werden nur die Bytes in der Datei ab
dem dritten Byte in den Speicher kopiert. Deshalb wird auch bei BASIC-Programmen
eine Startadresse angegeben, nämlich 2049. Allerdings unterscheidet LOAD
intern zwischen BASIC-Programmen und Maschinenprogrammen, und verwendet die
Startadresse 2049, wenn Sie ein Programm mit LOAD“[Dateinamen]“,8
anstatt mit LOAD“[Dateiname]“,8,1 laden. Die Sekundäradresse ist hier
übrigens dieselbe, nämlich 1. Deshalb können Sie ein BASIC-Programm auch mit LOAD“[Dateiname]“,8,1
laden, aber ein Maschinenprogramm nicht mit LOAD“[Dateiname]“,8.
Beachten
Sie hier unbedingt, dass viele Programme, vor Allem Spiele, ihre eigenen Loader verwenden, und die Daten auch im Speicher hin und
her schieben. Nicht zuletzt dienen diese Maßnahmen dem Kopierschutz.
SEQ-Dateien
Sequenzielle
Dateien
verwenden die Sekundäradresse 2 und bestehen aus einer Sequenz von Blöcken, das
heißt, wenn sich ein Block ändert, dann muss die gesamte Datei neu geschrieben
werden (Ausnahme: Anhängen von Blöcken direkt am Ende). Sequenzielle Dateien
werden also für Daten benutzt, die sich selten oder nur einmalig vor dem
Beenden eines Programms ändern, wie z.B. Spielstände oder
Konfigurationseinstellungen. Sequenzielle Dateien werden mit OPEN wie
folgt erstellt (die Floppy ist hier Gerät 8):
OPEN 1,8,2,"[Dateiname],W"
Auf der
1541 werden Dateien werden stets neu geschrieben, wenn sie zum Schreiben
geöffnet werden, deshalb führt eine Verwendung des Dateinamens einer
existierenden Datei zu der Meldung „62, FILE EXISTS“ auf dem Floppy-Fehlerkanal
(siehe auch dort). Wenn Datenblöcke aus einer sequenziellen Datei gelesen
werden sollen, muss diese durch den folgenden OPEN-Befehl zum Lesen
geöffnet werden:
OPEN 1,8,2,"[Dateiname],R"
Ist die
Datei nicht vorhanden, wird die Fehlermeldung
?FILE NOT FOUND ERROR
von BASIC
erzeugt, und auf dem Floppy-Fehlerkanal (siehe auch dort) wird die Meldung „63,
FILE NOT FOUND“ ausgegeben. Sequenzielle Dateien verwalten die Daten sektorweise, jedoch müssen einzelne Bytes mit den
Standard-BASIC-Befehlen PRINT#, GET# und INPUT#
geschrieben bzw. gelesen werden. Dies führt dann dazu, dass mit PRINT#
in einen Block nur 254 Bytes geschrieben werden können (der Link zu dem
nächsten Block wird hier automatisch erstellt), ein Auslesen der Daten mit INPUT#
liest aber 256 Bytes pro Block ein (also auch die Daten des Links zum nächsten
Block).
Eine
Besonderheit bei sequenziellen Dateien ist, dass an diese in einer einfachen
Weise Daten angehängt werden können. Dies leistet der folgende OPEN-Befehl
(A=append):
OPEN 1,8,2,"[Dateiname],A"
Das Append-Kommando
erstellt stets einen neuen Datenblock am Ende der Datei, und es können auch nur
neue Daten angehängt, nicht aber alte Daten verändert werden. Fehlerhaft
arbeiten dagegen die folgenden Ersetzungs-Kommandos:
OPEN 1,8,2,"@:[Dateiname],W"
OPEN 1,8,2,"@0:[Dateiname],W"
Manchmal
wird die Datei hier fehlerhaft ersetzt, z.B. wird der letzte Block nicht
korrekt mitgeschrieben (sog. Replace-Bug). Verwenden
Sie stattdessen lieber die folgenden Zeilen:
OPEN 15,8,15,"S:[Dateiname]":CLOSE 15
OPEN 1,8,2,"[Dateiname],W"
Beachten
Sie beim Benutzen des Append-Modus stets, dass alle Schreiboperationen, die neu angelegte Blöcke nicht vollständig füllen, dazu
führen, dass die Blöcke bis zu Kernal-Version 2.0 und/oder
der 1541 bis V 1.2 mit Datenmüll, und nicht mit Null-Bytes aufgefüllt werden.
Da Sie nicht wissen, welche Kernal-Version der
Endbenutzer hat, sollten Sie das Padding (also das Auffüllen der Datenblöcke
mit Null-Bytes) per Hand erledigen.
REL-Dateien
REL-Dateien benutzen wie
sequenzielle Dateien die Sekundäradresse 2, ermöglichen aber im Gegensatz zur
sequenziellen Datei einen wahlfreien Zugriff auf die Daten. Das heißt, es
können beliebige Datenblöcke (auch in der Mitte) geändert werden, ohne die
gesamte Datei neu schreiben zu müssen. Diese Methode des Zugriffs wird auch als
Random Access bezeichnet. Um einen wahlfreien Zugriff zu gewährleisten,
ist es bei relativen Dateien erlaubt, unterschiedlich große Datenblöcke zu
verwenden. Im Fall der relativen Dateien wird deswegen auch von Datensätzen
bzw. Records gesprochen.
Relative
Dateien sind jedoch anders strukturiert, als Programmdateien oder sequenzielle
Dateien. Da nämlich Dateien normalerweise eine verkettete Liste von Blöcken
sind, müssen die Informationen für den wahlfreien Zugriff in separaten Sektoren
(Side Sectors) abgelegt werden, und innerhalb der
relativen Datei separat angesprochen werden. Leider sind diese
Zusatzinformationen unterschiedlich bei unterschiedlichen Laufwerken (z.B. bei
einer 1571 anders, als bei einer 1541). Deshalb werden relative Dateien nur selten
verwendet. Um eine relative Datei zu erstellen, wird folgendes Kommando
verwendet:
OPEN 1,8,2,"[Dateiname],L,[Länge des
Datensätze]"
Der
Dateiname ist eine normale ASCII-Zeichenkette, dem aber stets “,L,“ folgen muss. Die Längeninformation der
Datensätze muss jedoch in ein Byte passen, das heißt, dass auch bei einer
relativen Datei keine Datensätze gespeichert werden können, die nicht mehr in
einen Sektor passen. Ferner bedeutet dies, dass Sie von BASIC aus z.B.
folgendes Kommando benutzen müssen, um die relative Datei TEST mit einer
Datensatzlänge von 200 Bytes anzulegen:
OPEN 1,8,2,"TEST,L,"+CHR$(200)
Im
Gegensatz zu sequenziellen Dateien müssen für den Zugriff auf einen bestimmten
Datensatz zusätzliche Kommando-Bytes über die Sekundäradresse 15 gesendet
werden. Das wichtigste Kommando ist in diesem Fall das folgende
Positionierungs-Kommando:
"P"+CHR$([Sekundäradresse])+CHR$([Lo-Byte der Datensatznummer])+CHR$(Hi-Byte der
Datensatznummer)+CHR$([Byteposition])
Um also
z.B. auf den Anfang des 1000. Datensatzes zuzugreifen zu können, muss die Zahl
1000 erst einmal in den Hexadezimalwert $3E8 gewandelt werden. Das Lo-Byte ist
also $E8 (232 dezimal), das Hi-Byte 3 (3 dezimal). Das entsprechende Kommando
wird nun wie folgt übermittelt:
OPEN 15,8,15
PRINT#15,"P"+CHR$(2)+CHR$(232)+CHR$(3)+CHR$(0)
Bei
relativen Dateien kann nun der Inhalt eines Datensatzes direkt nach der
Positionierung entweder ausgelesen, oder aber verändert werden. Das heißt, um
z.B. einen neuen Text in den 1000. Datensatz zu schreiben, können Sie einfach PRINT#
benutzen:
PRINT#1,"HALLO, DIES IST DER 1000. DATENSATZ"
PRINT# verwendet in diesem Fall
als Kanal die Sekundäradresse, die Sie im letzten Positionierungsbefehl
verwendet haben. Diese ist im Standardfall 2, kann aber im Gegensatz zu
sequenziellen Dateien für jeden Zugriff neu gesetzt werden. Das bedeutet
natürlich, dass Sie auch mehrere relative Dateien gleichzeitig verwenden
können, ich würde Ihnen dies jedoch nicht raten, da dadurch Ihre Programme sehr
langsam werden, und auch Ihre Floppy durch die dauernde Neupositionierung des
Schreib/Lesekopfes Schaden nehmen kann. Auf dem C64 zusammen mit der 1541
können Sie aber sowieso nicht viele Kanäle gleichzeitig öffnen, denn Kanal 3
ist zum Lesen von Daten, und Kanal 15 für die Übermittlung von Kommandos und
Fehlern vorgesehen. Sie können also nur noch Kanal 1 zusätzlich benutzen, weil
dieser bei relativen Dateien nicht für die Erstellung ausführbarer Dateien
benutzt wird. Kanal 4 ist normalerweise für Drucker vorgesehen, und wenn Sie
keinen Scanner besitzen, können Sie noch Kanal 5, 6 und 7 für relative Dateien
benutzen.
8.2 Floppy-Kommandos
Quelle: The anatomy of the 1541 disk drive
First Publishing LTD.
ISBN 0-948015-012
Seite 86 ff.
Nun
können Sie sehr gut mit Dateien umgehen, und beliebige Daten so in diesen
ablegen, dass Sie immer die optimale Zugriffszeit erhalten (was bei der langsamen 1541 nicht unwichtig ist). Sie können zwar nun
Ihre Spiele-Level oder Kundendaten auf einer Diskette ablegen, aber immer noch
nicht die Struktur der Diskette selbst verwalten. Dies ist so, weil Sie nicht
auf die einzelnen Blöcke zugreifen können, sondern nur auf die Dateien. Sie
können sich z.B. nicht die Directory anzeigen lassen,
ohne Ihr laufendes Programm dadurch zu löschen, oder aber die aktuelle
Blockbelegung in der BAM ansehen. Sie benötigen also eine Referenz sämtlicher
Floppy-Kommandos. Diese Referenz folgt nun. Wundern Sie sich aber nicht
darüber, dass die Kommandos englische Namen haben, dies ist oft der Fall auf
dem C64.
8.2.1 Block-Read-Kommando
(B-R)
Das Block-Read-Kommando
verwendet den Kommandokanal (Sekundäradresse 15), und einen zusätzlichen
Kanal für einen Datenpuffer. Hierbei ist der Standardkanal für den
Datenpuffer die Sekundäradresse 2, es können aber auch andere Sekundäradressen
benutzt werden, sofern diese frei sind. Die folgenden BASIC-Zeilen öffnen
zuerst einen Datenpuffer-Kanal mit dem #-Kommando, und senden
anschließend das B-R-Kommando:
OPEN 2,8,2,"#"
OPEN 1,8,15
PRINT#1,"B-R [Kanalnummer] [Drive] [Spur] [Sektor]"
(bitte
die eckigen Klammern nicht mit eingeben, diese sind nur Platzhalter)
Es ist
hier zu beachten, dass reine Floppy-Kommandos die Befehle als ASCII-Text
entgegennehmen und die einzelnen Parameter durch Leerzeichen (und nicht durch
Kommata) getrennt werden müssen. Außerdem hat der Parameter [Drive] hier
bei dem Laufwerk an der Primäradresse 8 den Wert 0 und nicht den Wert 8. Ferner
liest das B-R-Kommando die ersten zwei Bytes des Blocks nicht mit ein.
Dies ist normalerweise nicht tragisch, da die eigentlichen Daten erst ab dem 3.
Byte anfangen. Wenn Sie jedoch den gesamten Block einlesen müssen, um z.B. den
Link-Block bestimmen zu können, müssen Sie das B-R-Kommando durch das U1-Kommando
ersetzen.
Beispiel:
Einlesen des ersten Directory-Blocks in das Array B
10 DIM B(256)
20 OPEN 2,8,2,"#"
30 OPEN 1,8,15
40 PRINT#1,"U1 2 0 18
1"
50 FOR I=0 TO 255
60 GET#2,A$
70 IF LEN(A$)>0 THEN
B(I)=ASC(A$): GOTO 90
80 B(I)=0
90 NEXT I
100 CLOSE 1: CLOSE 2
GET# liest einzelne Bytes
aus einer Datei oder einem Puffer, wenn dieser vorher mit dem #-Kommando
an eine Sekundäradresse gebunden wurde. GET# kann jedoch, genau wie GET
für die Tastatur, das eingelesene Byte nur an einen String weitergeben. Deshalb
muss in dem letzten Beispiel die ASC()-Funktion
benutzt werden, um wirklich Byte-Werte in das Array B zu schreiben. Da CHR$(0)
einem leeren String entspricht, benötigen Sie noch eine zusätzliche IF-Abfrage,
die den Wert 0 in das Array B schreibt, wenn A$ leer sein sollte.
8.2.2
Das Block-Pointer-Kommando (B-P)
Das BP-Kommando
folgt normalerweise direkt dem B-R-Kommando, und wird dazu benutzt, an
eine bestimmte Byte-Position im Puffer für den zuletzt eingelesenen Block zu
springen. Angenommen, Sie haben einen Block in den Puffer eingelesen, der an
die Sekundäradresse 2 gebunden ist, und wollen nun zum 100. Byte springen. In
diesem Fall müssen Sie die folgende BASIC-Zeile verwenden:
PRINT#1,"B-P [Sekundäradresse] [Position]"
Ergibt
PRINT#1,"B-P 2 99"
Auch hier
müssen die Kommandos als Text gesendet werden, und die Parameter müssen durch
Leereichen voneinander getrennt werden. Zu beachten ist, dass die
Positionsangaben von Bytes in Puffern bei 0 beginnen, das 100. Byte hat also
die Positionsnummer 99.
Das
B-P-Kommando wird nicht oft benutzt, da in den meisten Fällen genug Speicher
frei ist, um 256 Bytes in einem Array oder String abzulegen. Ausnahmen kann es
dann geben, wenn z.B. nur der Disketten-Name oder andere Einträge in der BAM
angezeigt werden sollen, die Sie wirklich interessieren. In diesem Fall kann
dann der Sektor 0 der Spur 18 eingelesen, und anschließend ein B-P-Kommando
benutzt werden.
8.2.3
Das Block-Write-Kommando (B-W)
B-W ist die Abkürzung von
„block write“, das heißt, dass B-W den Inhalt
des aktuellen Puffers in einen bestimmten Sektor auf die Diskette schreibt.
Damit ist B-W das wohl gefährlichste Floppy-Kommando, denn der
entsprechende Block, den Sie auch hier wieder durch die Spur und den Sektor
angeben, wird kommentarlos überschrieben. Wenn sich dann an dieser Stelle ein
Block einer Datei befindet, dann wird die Datei an dieser Stelle beschädigt.
Wenn Sie versehentlich die BAM mit einem eventuell nicht initialisierten Puffer
überschreiben, wird sogar Ihre gesamte Diskette unbrauchbar, und die Daten
können dann auch nicht so einfach wieder hergestellt werden. Es gibt zwar für
einen solchen Fall Notfall-Tipps, wie z.B. das sofortige Ausführen eines Validate-Kommandos (siehe auch dort), die korrekte BAM wird
aber nicht in allen Fällen wiederhergestellt.
Um das B-W-Kommando
zu verwenden, muss auf jeden Fall vorher ein Puffer initialisiert (das heißt an
eine Sekundäradresse gebunden) und anschließend korrekt gefüllt werden, sei es
mit Daten aus einem früheren Block, oder aber mit Daten für einen neuen Block.
Das B-W-Kommando funktioniert ansonsten fast wie B-R:
OPEN 2,8,2,"#"
Anschließend
wird der Puffer mit PRINT# gefüllt, und wie folgt geschrieben:
PRINT#1,"B-W [Kanalnummer] [Drive] [Spur] [Sektor]"
B-W schreibt die Daten ab
dem aktuellen Puffer-Zeiger sofort auf die Diskette, und wenn der Puffer
überläuft, dann wird automatisch der nächste Puffer verwendet. Die Floppy
besitzt intern vier Puffer, in die abwechselnd Daten
geschrieben werden. Um einen ganzen Sektor zu schreiben, ohne vorher B-P
zu benutzen, wird häufig B-W durch U2 ersetzt. U2 setzt im
Gegensatz zu B-W zunächst den Puffer-Zeiger auf 0 zurück.
Beispiel:
Löschen der BAM
10 OPEN 2,8,2,"#"
20 OPEN 1,8,15
30 FOR I=0 TO 255
40 PRINT#2,CHR$(0);
50 NEXT I
60 PRINT#1,"U2 2 0 18 0"
70 CLOSE 1: CLOSE 2
Das
letzte Beispiel ist ein Notfallprogramm, das benutzt werden kann, wenn sich
eine Diskette aus irgendeinem Grund nicht mehr formatieren lässt. Sie können
dann die BAM mit Null-Bytes überschreiben, und anschließend versuchen, die
Diskette erneut zu formatieren. Ihre Floppy behandelt dann die Diskette als
fabrikneu, und erstellt auch sämtliche Synchronisationsbits neu. Wenn natürlich
die Diskettenoberfläche beschädigt ist, dann hat das vorige Beispiel keine
Wirkung.
Manche
Kopierschütze für Spiele benutzen übrigens Spurnummern über 35, um dort z.B.
Daten für einen Schlüssel oder eine Lizenznummer abzulegen. Viele
Kopierprogramme kopieren die Spuren 36-40 nicht mit, sodass dann die Kopie
nicht läuft. Sie sollten es möglichst vermeiden, Spurnummern über 35 zu
benutzen, denn besonders bei der 1541 der ersten Version kann sich dadurch der
Lesekopf verklemmen. Diesen müssen Sie dann im Zweifelsfall per Hand wieder
zurückschieben, was dann aber die Mechanik beschädigen kann. Ich rate Ihnen an
dieser Stelle, auf die Ausführung von Programmen zu verzichten, die Spurnummern
über 35 verwenden, da diese die Floppy zerstören können.
8.2.4 Block-Allocate-Kommando
(B-A)
Allocate ist die englische
Bezeichnung für die Zuweisung von Speicher, in diesem Fall ist dies die
Zuweisung eines belegten Blocks in der BAM. Sie können also bestimmte Sektoren
auf einer Diskette von vornherein als belegt markieren, wodurch diese z.B.
nicht mehr beim Erstellen von neuen Dateien verwendet werden. Auch hier gibt es
zahlreiche Kopierschütze für Spiele, die zusätzlich zu den Dateien noch andere
Informationen auf der Diskette ablegen, und deshalb nicht mehr durch ein
einfaches File-Copy-Programm auf eine zweite Diskette übertragen werden können.
Um einen bestimmten Block in der BAM als belegt zu kennzeichnen, können
folgende BASIC-Zeilen verwendet werden:
OPEN 1,8,15
PRINT#1,"B-A [Drive]
[Track] [Sektor]"
Auch hier
muss das Kommando in Textform übergeben werden, und für [Drive] muss 0 für die
erste 1541 am Bus eingesetzt werden. Um z.B. einen bestimmten Block (z.B. den
10. Sektor der Spur 10) als belegt zu kennzeichnen, müssen Sie deshalb folgende
BASIC-Zeilen benutzen:
T=10: S=10: REM T=track,
S=sector
OPEN 1,8,15
PRINT#1,"B-A 0
"+STR$(T)+" "+STR$(S)
Wahlweise
kann die letzte Zeile auch so lauten:
PRINT#1,"B-A 0";T;S
PRINT# wandelt dann die
Variablen T und S automatisch in Strings um, und trennt diese bei
der Ausgabe auch durch Leerzeichen voneinander.
8.2.5 Block-Free-Kommando
(B-F)
Free ist die englische
Bezeichnung für die Freigabe von vorher zugewiesenem Speicher, in diesem Fall
ist dies die Aufhebung der Zuweisung eines belegten Blocks in der BAM. Sie
können also bestimmte Sektoren auf einer Diskette als frei markieren, wodurch
diese dann beim Erstellen von neuen Dateien wieder verwendet werden. Um einen
bestimmten Block in der BAM als frei zu kennzeichnen, können folgende
BASIC-Zeilen benutzt werden:
OPEN 1,8,15
PRINT#1,"B-F [Drive]
[Track] [Sektor]"
Auch hier
muss das Kommando in Textform übergeben werden, und für [Drive] muss 0 für die
erste 1541 am Bus eingesetzt werden. Um z.B. einen bestimmten Block (z.B. den
10. Sektor der Spur 10) als frei zu kennzeichnen, müssen Sie deshalb folgende
BASIC-Zeilen benutzen:
T=10: S=10: REM T=track,
S=sector
OPEN 1,8,15
PRINT#1,"B-F 0
"+STR$(T)+" "+STR$(S)
Wahlweise
kann die letzte Zeile auch so lauten:
PRINT#1,"B-F 0";T;S
PRINT# wandelt dann die
Variablen T und S automatisch in Strings um und trennt diese dann bei der
Ausgabe auch durch Leerzeichen voneinander.
8.2.6
Block-Execute-Kommando (B-E)
Execute ist die englische
Bezeichnung für die Ausführung von im Speicher befindlichen Programmen. In diesem
Fall ist dies ein 6502-Maschinenprogramm, das Sie zuvor in einen Puffer geladen
haben. Allerdings wird dieses Programm auf der 1541 ausgeführt, und nicht auf
dem C64, deshalb wird B-E auch ausschließlich für DOS-Erweiterungen
benutzt. Das Erstellen einer DOS-Erweiterung erfordert tiefgreifendes Wissen
über die Interna der Floppy, und kann natürlich an dieser Stelle nicht
behandelt werden. Das Gleiche gilt für die Kommandos M-R (memory read), M-W (memory write), M-E (memory execute).
8.2.7
New-Kommando (N)
Mit dem N-Kommando
wird eine komplett neue Diskettenstruktur erstellt, man spricht in diesem Fall
auch von Formatieren. Die 1541 formatiert von Haus aus Disketten immer
komplett, das heißt, dass zunächst der Schreib/Lesekopf kalibriert und auf Spur
0 positioniert wird (was man dann auch hören kann), und anschließend 35 leere
Spuren geschrieben werden. In einem zweiten Durchlauf wird dann eine leere BAM
erstellt, in die dann auch der Diskettenname und die ID eingetragen werden.
Spur 18 wird durch das N-Kommando komplett gelöscht, das heißt, dass die Directory anschließend leer ist. Im Gegensatz zum
Scratch-Befehl (siehe auch dort) kann das N-Kommando nicht wieder rückgängig
gemacht werden. Um eine Diskette zu formatieren, können Sie folgende
BASIC-Zeilen benutzen:
OPEN 15,8,15, "N:[Diskettenname],[ID]"
CLOSE 15
Beachten
Sie hier, dass die Parameter durch Kommata getrennt werden, anders als bei den
Block-Kommandos. Der Diskettenname kann aus bis zu 16 Zeichen bestehen, die ID
nur aus zwei Zeichen (zusätzliche Zeichen werden ignoriert). Was Sie an dieser
Stelle als ID eintragen, bleibt Ihnen überlassen, meistens wird jedoch für den
ID einfach die Zahl 64 benutzt.
8.2.8 Replace-Kommando (@)
Das @-Kommando
wird meistens zusammen mit SAVE verwendet, um eine Datei durch eine
neuere Variante zu ersetzen. Bei sequenziellen Dateien gibt es ein separates Replace-Kommando für einzelne Datenblocks (siehe auch
dort). @ schreibt eine Datei immer komplett neu (überschreibt also die
alte Datei). Ein Beispiel für die Benutzung des @-Kommandos ist z.B.:
SAVE "@:TESTPROGRAM",8
Die
1541-Varianten vor der Version 3.0 besitzen jedoch ein fehlerhaftes DOS, das
das @-Kommando fehlerhaft ausführt. In Einzelfällen werden hier die
alten Dateien nicht vollständig ersetzt, oder erst gar nicht gespeichert (in
diesem Fall blinkt die Floppy und hängt sich auf). Im Zweifelsfall sollten Sie
auf das @-Kommando verzichten, und die entsprechende Datei erst mit dem
Scratch-Kommando (siehe auch dort) löschen und anschließend neu abspeichern.
8.2.9
Scratch-Kommando (S)
Beim C64
spricht man nicht von „delete“ (löschen), sondern von
„scratch“ (kratzen), wenn man eine Datei entfernen
will. Dies kommt wahrscheinlich daher, dass D (DOR=data orientation register) ein internes Register der 1541 ist, das
festlegt, ob die Daten nach innen oder nach außen fließen sollen, also ob die
1541 Daten empfangen oder senden soll. Der Buchstabe D ist also schon
belegt, und kann nicht mehr für Kommandos benutzt werden (wie gesagt, dies ist
nur meine eigene Vermutung). Da der Löschvorgang auf der 1541 in der Tat
Geräusche erzeugen kann, spricht man hier wahrscheinlich von „scratch“. Um eine Datei mit dem S-Kommando zu
löschen, können die folgenden BASIC-Zeilen benutzt werden:
OPEN 15,8,15,"S:[Dateiname]"
CLOSE 15
Beachten
Sie, dass erst nach dem Senden von CLOSE die Datei wirklich komplett
entfernt wird, und dass ein vergessenes CLOSE zu verstümmelten Dateien
führen kann, die Sie dann auch nicht erneut überschreiben können. Scratch
löscht jedoch eine Datei nicht komplett, sondern gibt nur die Dateiblöcke in
der BAM wieder frei. Außerdem wird der entsprechende Directory-Eintrag nicht entfernt,
sondern der Dateityp wird auf DEL (deleted file) gesetzt. Wenn Sie also eine Datei versehentlich
gelöscht haben, kann diese mit entsprechenden Programmen, wie z.B. dem
Diskretter, wieder hergestellt werden (dieses befindet sich natürlich auch auf
der Beispiel-Diskette). Das Non Plus Ultra ist
allerdings das Tool „Disk Maintenance“, das wirklich
alles kann, sogar Assembler-Code oder Sprite-Blöcke anzeigen. Natürlich können
Sie damit auch Sektoren verändern, und allerhand anderen Unsinn anstellen.
Leider ist „Disk Maintenance“ nicht mehr sehr gut im Internet zu bekommen, und
auch die rechtliche Frage, ob ich Ihnen ein D64-Image zum Download anbieten
darf, ist bis jetzt nicht geklärt.
8.2.10
Validate-Kommando (V)
Beim C64
spricht man nicht von Scannen, wie beim PC, sondern von „validate“
(evaluieren), wenn man das Dateisystem aufräumen will. Dies kommt daher, dass
der Buchstabe S schon durch den Scratch-Befehl belegt ist. Validate sucht nach Sektoren, die nicht mehr von
Dateien benutzt werden, und gibt diese dann in der BAM frei. Dies kann lange
dauern, der Vorteil ist aber unter Umständen, dass nach Ausführen des V-Kommandos
wieder mehr Speicher auf der Diskette verfügbar ist. Der Nachteil ist leider,
dass vorher gelöschte Dateien unter Umständen nicht mehr mit Diskrettern
wiederhergestellt werden können, nachdem Validate
ausgeführt wurde. Um die Diskette aufzuräumen, und unbenutzte Blöcke wieder
freizugeben, können die folgenden BASIC-Zeilen benutzt werden:
OPEN
15,8,15,"V"
CLOSE
15
8.3 Beispielprogramme
8.3.1
Automatisches Erstellen von DATA-Zeilen
Vor
allem, wenn Sie umfangreiche Assemblerprogramme mit zahlreichen Unterprogrammen
und Daten-Tabellen erstellen, kann es schnell geschehen, dass Sie die OP-Codes,
in die Ihr Programm übersetzt wird, nicht mehr alle auf dem Bildschirm ausgeben
können. Natürlich wird dann auch die Übernahme des Maschinenprogramms in DATA-Zeilen
eine sehr aufwendige Prozedur, weil Sie z.B. die OP-Codes per FOR-Schleife
in Hunderter-Blöcken ausgeben, und anschließend in DATA-Zeilen
übertragen müssen. Oft werden Sie sich hierbei mehrmals vertun, bis Sie endlich
die richtigen Werte in Ihr BASIC-Programm übertragen haben. Das nächste Listing
erstellt nun eine neue Datei, in die die richtigen DATA-Zeilen für ein
bestimmtes Maschinenprogramm automatisch eingetragen werden.
Zu diesem
Zweck wird zunächst mit OPEN eine neue Datei angelegt, in die
anschließend mit PRINT# ein BASIC-Programm hineingeschrieben wird. Dies
ist gar nicht so schwer, wie Sie denken. Ein BASIC-Programm ist nämlich im
Endeffekt nur eine verkettete Liste von BASIC-Zeilen. Jede Zeile beginnt
zunächst mit einem Zeiger (in der Form Lo/Hi) auf die Adresse der nächsten
BASIC-Zeile. Anschließend folgt die Zeilennummer in binärer Form. Das heißt,
dass eine Zeile auch hier durch ein Lo- und ein Hi- Byte definiert wird, in dem
eine 16-Bit-Zahl gespeichert wird, die die aktuelle Zeilennummer angibt. Die
BASIC-Zeile selbst besteht immer aus ASCII-Zeichen und Tokens. ASCII-Zeichen
werden bei Variablen, in PRINT-Anweisungen und bei Zahlenwerten benutzt,
Tokens bei Zuweisungen, Funktionen und BASIC-Befehlen. Tokens erkennen
Sie daran, dass bei dem entsprechenden Token-Zeichen das oberste Bit 1
ist, der Wert eines Token-Zeichens kann also nicht unter 128 sein. Im Falle der
ASCII-Zeichen, die für die Variablen und Klammern benutzt werden, liegen die
ASCII-Werte stets zwischen 33 (Ausrufezeichen) und 122 (kleines z).
Sonderzeichen, die aber nur bei einer PRINT-Anweisung innerhalb zweier
Anführungszeichen benutzt werden dürfen, haben ASCII-Werte zwischen 1 und 255.
Deshalb kann auch das Null-Byte als End-Marker einer BASIC-Zeile benutzt
werden, weil es in der eigentlichen Programmzeile nicht vorkommen kann.
Wenn Sie
nun ein Programm erstellen wollen, das DATA-Zeilen enthält, müssen Sie
erst einmal eine Datei mit OPEN anlegen. Anschließend müssen Sie für
jede Zeile die folgenden Bytes mit PRINT# in die Datei schreiben:
·
Lo-Byte
der Adresse der nächsten BASIC-Zeile
·
Hi-Byte
der Adresse der nächsten BASIC-Zeile
·
Lo-Byte
der 16-Bit-Zeilennummer
·
Hi-Byte
der 16-Bit-Zeilennummer
·
Die
Zeile selbst als Byte-Werte zwischen 1 und 255
·
Abschließendes
Null-Byte
Die
BASIC-Zeile selbst muss in diesem Fall immer mit einem DATA-Token
(Byte-Wert 131) beginnen, anschließend folgen die einzelnen Byte-Werte Ihres
Maschinenprogramms (bzw. einem Teil davon) als ASCII-Text, getrennt durch
Kommata. Bei längeren Maschinenprogrammen müssen Sie also mehrere DATA-Zeilen
generieren, die auch nicht zu voll werden dürfen. Da alle Theorie grau ist,
schauen Sie sich nun das folgende Listing an:
DATAGEN
10 PRINT"[SHIFT+CLR/HOME]DATEINAME";:INPUT N$
20 PRINT"ERSTE ZEILE";:INPUT SZ
30 PRINT"SCHRITTWEITE";:INPUT S
40 PRINT"STARTADRESSE";:INPUT SA
50 PRINT"ENDADRESSE";:INPUT EA
60 OPEN 1,8,1,N$
70 PRINT#1,CHR$(1);:PRINT#1,CHR$(8);
80 A$="":AD=2049
90 K=0:L=SZ:I=SA
100 IF K<>0 THEN GOTO
140
110 HI=INT(L/256):LO=L-(256*HI)
120
A$=A$+CHR$(LO)+CHR$(HI)+CHR$(131)+" "
130 L=L+S
140 BY=PEEK(I)
150 A$=A$+MID$(STR$(BY),2,LEN(STR$(BY))-1)
160 I=I+1
170 IF I>EA THEN 200
180 K=K+1
190 IF K<15 THEN
A$=A$+",":GOTO 140
200 AD=AD+1+LEN(A$)
210 HI=INT(AD/256):LO=AD-(256*HI)
220 PRINT#1,CHR$(LO)+CHR$(HI);
230 PRINT#1,A$;:A$=""
240 PRINT#1,CHR$(0);
250 IF I<=EA THEN K=0:GOTO 100
260 PRINT#1,CHR$(0);CHR$(0);
270 CLOSE 1
In Zeile
10-50 werden erst einmal die Daten Ihres Maschinenprogramms mit INPUT
von der Tastatur eingelesen. Hierzu gehören der Dateiname der zu erzeugenden
BASIC-Datei (N$), die Zeilennummer der ersten DATA-Zeile (SZ),
die Schrittweite für die nächste BASIC-Zeile (S), sowie die
Start-Adresse SA und die Endadresse EA Ihres Maschinenprogramms
(dies sollte natürlich vorher mit einem Assembler-Monitor wie Hypra-Ass erzeugt worden sein). In Zeile 60 wird
dann eine Datei mit dem Namen N$ angelegt. Für die neue Datei wird die
Sekundäradresse 1 benutzt, was im Endeffekt bedeutet, dass Sie ein
Maschinenprogramm in die Datei schreiben. Warum tun Sie dies an dieser Stelle?
Die Antwort ist, dass sich ein BASIC-Programm nicht wesentlich von einem
Maschinenprogramm unterscheidet, außer dass die Startadresse stets 2049 ist
(diese steht dann auch in den ersten zwei Bytes der Datei). Zeile 70
schreibt nun zunächst genau diese zwei Bytes für die Startadresse in die am
Anfang leere Datei (Lo-Byte=7, Hi-Byte=8).
In Zeile
80 wird anschließend diese Startadresse (also 2049) in der Variablen AD
abgelegt, sowie der leere String A$ erzeugt (dieser enthält später die
eigentliche BASIC-Zeile). Zusätzlich werden in Zeile 90 einige
Hilfsvariablen definiert, nämlich ein Zähler K (Nummer des DATA-Bytes in
der aktuellen BASIC-Zeile), L (aktuelle BASIC-Zeile, beginnend bei SZ),
und ein Zähler I (aktuelle Adresse, beginnend bei SA).
Nun
beginnt der eigentliche Algorithmus, mit dem aus dem Maschinenprogramm an den
Adressen SA bis EA ein BASIC-Programm abgeleitet wird. Zunächst
prüft der Algorithmus in Zeile 100, ob eine neue BASIC-Zeile angelegt
werden soll. Dies ist immer dann der Fall, wenn der Zähler K entweder
gerade initialisiert wurde, oder aber am Ende einer Zeile (die maximal 15 Werte
enthalten darf) auf 0 zurückgesetzt wurde. Am Anfang ist K immer 0,
deshalb werden die Zeilen 110-130 auch nicht übersprungen. Stattdessen
wird die Zeilennummer in ein Lo- und ein Hi-Byte zerlegt, und zusammen mit
einem DATA-Token und einem Leerzeichen in den String A$
geschrieben. Dies geschieht mit der CHR$()-Funktion und der Verkettung von
Strings durch den Operator + Byte für Byte, und es werden hier auch
keine Zeilenumbrüche zwischen den einzelnen Bytes eingefügt.
Wenn K
nicht 0 ist, und Sie sich nicht am Anfang einer neuen BASIC-Zeile befinden,
werden die Zeilen 110-130 übersprungen, und es wird auch nicht Zeile
130 ausgeführt, die die nächste Zeilennummer durch Addition von S (Nummerierungs-Schrittweite) zu L (aktuelle
Zeilennummer) ermittelt. Stattdessen wird zunächst das nächste Byte Ihres
Maschinenprogramms an der Adresse I in die Variable BY eingelesen
(BY ist die Abkürzung für Byte). Nun müssen Sie BY in einen Text
umwandeln, und da Sie inzwischen perfekt BASIC können, fällt Ihnen natürlich
sofort die STR$()-Funktion
ein. Leider fügt STR$() am Anfang des ASCII-Textes, der die Zahl
darstellt, immer ein Leereichen ein, und Sie können dies auch nicht ändern.
Deshalb müssen Sie in Zeile 150 folgendes Konstrukt verwenden: Sie
übergeben STR$(BY) und die Länge von STR$(BY) vermindert um 1 an
die MID$()-Funktion,
und lassen MID$() zusätzlich an der Startposition 2 beginnen. Nun hängen
Sie diesen Rückgabe-String an die aktuelle BASIC-Zeile (die sich in A$
befindet) an, und erhalten dadurch das korrekte Ausgabeformat. Ehe aber ein
zusätzliches Komma an A$ angehängt wird, das die DATA-Werte
voneinander trennt, müssen zunächst noch zwei Dinge geprüft werden. Hierzu wird
in Zeile 160 zunächst der Adresszeiger I um 1 erhöht. Wenn dieser
in Zeile 170 den Wert EA überschreitet, dann haben Sie alle Bytes
Ihres Maschinenprogramms korrekt eingelesen, und der Hauptalgorithmus wird mit GOTO
200 beendet. Wenn jedoch noch nicht alle Bytes Ihres Maschinenprogramms
eingelesen wurden, wird stattdessen der Zähler K um 1 erhöht, der
angibt, wie viele DATA-Bytes Sie bis jetzt in die aktuelle Zeile
geschrieben haben. Nur, wenn K noch nicht den Wert 15 hat, wird ein
Komma an das aktuelle DATA-Byte angehängt, ansonsten wird der Befehl GOTO
140 nicht ausgeführt, und die nun volle BASIC-Zeile wird in Zeile 200-250
in die Datei zurückgeschrieben.
Da Ihr
BASIC-Programm eine verkettete Liste von BASIC-Zeilen ist, müssen Sie nun
zunächst einen Zeiger auf den Beginn der nächsten Zeile in die Datei schreiben,
bevor Sie die eigentliche Zeile in der Datei ablegen können. Zu diesem Zweck
muss in Zeile 200 zunächst zu AD (das ist der Zeiger auf den
Beginn der aktuellen Zeile) um die Länge der BASIC-Zeile, die in A$
steht, addiert werden. Zusätzlich muss aber noch der Wert 1 addiert werden, da
jede BASIC-Zeile immer mit einem Null-Byte endet (man spricht in diesem Fall
auch von einem null-terminierten String). Den neuen Zeiger AD
zerlegen Sie anschließend in Zeile 210 und 220 in ein Lo- und Hi-Byte
und schreiben diese Byte-Werte in die Datei. Sie müssen an dieser Stelle der PRINT#-Anweisung
eine Zeichenkette übergeben, und müssen auch durch ein abschließendes Semikolon
am Ende von Zeile 220 explizit darauf achten, dass PRINT# keine
Zeilenumbruch-Zeichen erzeugt (dies wäre fatal und würde zu einem fehlerhaften
Listing führen). Nach dem Ablegen des Zeigers auf die nächste BASIC-Zeile wird
anschließend A$ in die Datei geschrieben. Auch hier müssen Sie in Zeile
230 darauf achten, die PRINT#-Anweisung mit einem Semikolon
abzuschließen, und in Zeile 240 stattdessen ein Null-Byte anhängen. Erst
an dieser Stelle wurde die aktuelle BASIC-Zeile erfolgreich in der Datei
abgelegt, und der Puffer A$ kann geleert werden.
Nachdem
Sie eine BASIC-Zeile erfolgreich abgeschlossen, und diese auch erfolgreich in
der Datei abgelegt haben, können zwei Szenarien auftreten. Das erste Szenario,
das in Zeile 250 abgefragt wird, ist, dass Sie noch nicht alle Bytes
Ihres Maschinenprogramms verarbeitet haben. In diesem Fall ist I<=EA,
und es erfolgt ein Rücksprung zu Zeile 100 (der Hauptalgorithmus wird
also erneut ausgeführt). Ist jedoch I>EA, wird die Datei geschlossen
und das Programm beendet. Allerdings müssen in Zeile 260 vorher noch zwei
Null-Bytes in die Datei geschrieben werden, bevor diese geschossen werden kann.
Wenn nämlich der Befehl LIST am Anfang einer BASIC-Zeile auf einen
Null-Zeiger trifft (Lo- und Hi-Byte sind 0), wird dies als Ende des Listings
gewertet. Wenn Sie also die zwei Null-Bytes am Ende der Datei nicht
mitschreiben, führt dies zu einem fehlerhaften BASIC-Listing, das auch durch LOAD
fehlerhaft in den Speicher geladen wird.
8.3.2
Speicherabbilder erstellen
Speicherabbilder
(sog. memory dumps) gehören
zu den mächtigsten Werkzeugen überhaupt, vor Allem, wenn man die
Speicherabbilder in korrekter Weise auf eine Diskette schreibt. Zahlreiche
Spiele konnten dadurch geknackt werden, dass man diese erst startete, und
anschließend den richtigen Speicherbereich auf eine Diskette schrieb, nachdem
man den Resetschalter betätigt hatte. Die Tatsache, dass man diese geknackten
Programme mit „,8,1“ laden musste, war dabei eine unbedeutende Nebensache. Sie
können memory dumps in
einfacher Weise selbst dadurch erzeugen, dass Sie ein kleines Maschinenprogramm
an die Adressen 53176-53249 laden. Dieses kleine Programm habe ich einmal aus
der 64-er abgetippt, und seitdem immer wieder verwendet. Ich habe dieses
Programm unter dem Namen SAVER ja schon einmal als BASIC-Listing
angegeben (nämlich in Kapitel 1 dieses Kurses), nun können Sie dieses wahre
Goldstück wieder hervorholen.
Angenommen,
Sie wollen den Inhalt des Bildschirms in eine Datei speichern. Laden sie dazu
das Programm SAVER und geben RUN ein. SAVER erstellt nun
eine Maschinenroutine, die an der Adresse 53176 beginnt, und die Sie mit den
folgenden Parametern aufrufen können:
SYS 53176 [Dateiname] oder
SYS 53176 [Dateiname],[Startadresse][Endadresse]
Mit der
ersten BASIC-Zeile können Sie ein Maschinenprogramm bzw. Speicherabbild in den
Speicher laden, mit der zweiten BASIC-Zeile können Sie ein Speicherabbild auf
die Diskette schreiben, wobei Sie hierbei die Startadresse und die Endadresse
angeben müssen. SAVER hat aber noch einen großen Vorteil: Er kann
problemlos zusammen mit BASIC benutzt werden, weil SYS 53176 das
laufende Programm nicht unterbricht. Im nächsten Beispiel-Listing wird genau
dies getan: Am Anfang des Listings wird der Saver
selbst in den Speicher geschrieben, und anschließend springt das Programm in
die Zeile 100, an der das eigentliche BASIC-Listing beginnt. Sehen Sie
sich dies nun an.
22-SCREENCOPY
10 FOR I=53176 TO 53176+73: READ A: POKE I,A:NEXT I
100
PRINT"[SHIFT+CLR/HOME]DIESES PROGRAMM SICHERT DEN
INHALT"
110
PRINT"DES BILDSCHIRMS IN EINE DATEI IHRER"
120
PRINT"WAHL."
130 PRINT
140
PRINT"GEBEN SIE ZUNAECHST EINEN DATEINAMEN"
150
PRINT"EIN. ANSCHLIESSEND GEBEN SIE DEN TEXT"
160
PRINT"EIN. AM ENDE DRUECKEN SIE [CTRL+RVS ON]ENTER[CTRL+RVS
OFF]UND"
170
PRINT"LASSEN DEN TEXT IN DIE DATEI SPEICHERN."
180 PRINT"DATEINAME";:INPUT
N$
190 PRINT"[SHIFT+CLR/HOME]";
200 OPEN 1,0:INPUT#1,A$
210 CLOSE 1
220 OPEN 15,8,15,"S:"+N$:CLOSE
15
230 SYS 53176 N$,1024,2023
240
PRINT"[SHIFT+CLR/HOME]DIE DATEI "+N$;
250
PRINT" WURDE ERSTELLT."
260 END
10000 REM
*** SAVER ***
10010 DATA 32,87,226,162,8,134,186,32,121
10020 DATA 0,240,44,32,253,174,32,138,173
10030 DATA
32,247,183,72,32,121,0,240,21,104,132,193,133,194,32,253,174,32
10040 DATA
138,173,32,247,183,132,174,133,175,76,237,245,104,132,195,133
10050 DATA 196,160,0,44,160,1,132,185,169
10060
DATA 0,76,165,244,60,54,52,39,69,82,62,0,0
Damit der
Benutzer (und auch Sie selbst) auch nach längerer Zeit noch etwas mit dem
letzten Listing anfangen kann, wird in Zeile 100-170 ein kleiner
Hilfetext angezeigt, der Ihnen erklärt, was das Programm überhaupt tut. In Zeile
180 werden Sie anschließend dazu aufgefordert, einen Dateinamen anzugeben,
in der am Ende der Bildschirminhalt abgelegt wird. Nach Eingabe eines
Dateinamens wird der Bildschirm gelöscht, damit der Hilfetext nicht mehr die
Eingabe eines eigenen Textes stört.
Sie
könnten nun hergehen, und einen eigenen Editor programmieren, der sämtliche
Cursor-Tasten abfragt, und entsprechende Aktionen ausführt, wenn Sie z.B.
keinen Buchstaben eingeben, sondern eine der Cursor-Steuerungs-Tasten drücken.
Sie müssten natürlich in diesem Fall auch eine eigene Cursor-Steuer-Routine
programmieren, die stets den alten Cursor entfernt, und entsprechend an eine
neue Stelle setzt, nachdem Sie ein Zeichen eingegeben haben. Sie können diese
Dinge jedoch umgehen, wenn Sie den BASIC-Editor in den Eingabe-Modus
versetzen. Hierzu müssen Sie nur die Datei mit der Dateinummer 1 an den I/O-Kanal
0 (also die Tastatur) binden. Anschließend lesen Sie einen
beliebigen Dummy-String (z.B. A$) mit INPUT# aus der Datei mit
der Dateinummer 1 ein (Zeile 200). Wenn Sie dann irgendwann die RETURN-Taste
drücken, dann wird die zuletzt eingegebene Zeile an A$ übergeben, und Zeile
210 (CLOSE 1) ausgeführt. Durch diesen Trick können Sie einen
beliebigen Text auf den Bildschirm schreiben, Sie dürfen nur die RETURN-Taste
nicht während der Texteingabe benutzen, denn diese beendet das Programm.
Wenn Sie
am Ende RETURN drücken, wird erst einmal die Datei mit dem Namen, den
Sie zuvor vergeben haben, mit dem Scratch-Befehl gelöscht (falls diese Datei
noch nicht existiert, geschieht einfach nichts). Anschließend wird in Zeile
230 der Saver aufgerufen, der die Datei mit dem
Namen N$ neu erstellt, und in diese Datei ein Speicherabbild des
Bildschirmspeichers schreibt. Anschließend wird das Programm mit der
entsprechenden Meldung beendet, dass die Datei mit dem Namen N$ erstellt
wurde. Wie Sie an dieser Stelle sehen, kann der Saver
auch komplexe Parameter verwenden, inklusive Variablen und Strings.
8.3.3
Einfaches Verschlüsseln von Dateien
Wenn Sie
Ihre Programme und Texte vor neugierigen Blicken schützen wollen, dann müssen
Sie die entsprechenden Dateien verschlüsseln. Es gibt zahlreiche
Verschlüsselungsverfahren, wahrscheinlich mehr, als Sie sich vorstellen können.
Professionelle Verschlüsselungsmethoden sind auch mehr oder weniger komplexe
Verfahren, die an dieser Stelle natürlich nicht besprochen werden können (wenn
Sie solche Verfahren dann doch programmieren wollen, müssen Sie sich ein gutes
Buch über Kryptographie besorgen).
Trotzdem
gibt es einfache, effiziente Verfahren, die ein Laie nicht so ohne weiteres
knacken kann (wie gesagt, ein professioneller Krypto-Analytiker kann dies dann
doch). In dem nächsten Beispiel wird ein solches Verfahren vorgestellt.
Natürlich handelt es sich bei diesem Verfahren wieder um einen Algorithmus, in
diesem Fall verwendet der Algorithmus eine Rotations-Chiffre. Die
bekannteste Rotations-Chiffriermaschine ist wahrscheinlich die
Enigma, die mit mechanischen Walzen arbeitet. Es gibt aber auch Software-Lösungen,
die unterschiedliche Rotations-Algorithmen benutzen. Die hier vorgestellte
Rotations-Chiffre arbeitet viel einfacher, als die Enigma,
nämlich wie ein Glücksspielautomat (einarmiger Bandit).
Angenommen,
eine Drehwalze eines einarmigen Banditen zeigt eine Zitrone an. Wenn Sie nun
diese Walze um eine zufällige Anzahl an Schritten nach vorne drehen, dann
erscheint anschließend ein ganz anderes Bild, z.B. eine Sonne (hurra, zumindest
eine Sonne haben wir schon!). Wiederholen Sie diesen Schritt nun 8-mal (also
drehen die Walze z.B. 8-mal um 7 Einheiten nach vorne). Wenn Sie sich nun nicht
gemerkt haben, dass die Schrittweite 7 war, können Sie nicht mehr von der Sonne
zu der Zitrone zurückgelangen (weil die Walze bestimmt auch mehrere Mal
umgeschlagen ist). Sie nehmen aber nun nicht eine, sondern 16 Walzen mit 256
Zeichen, und zusätzlich addieren Sie auch in jedem Schritt nicht immer dieselbe
Zahl, sondern 8-mal andere Zahlen (im Bereich 0-255) zu den Ausgangsstellungen
der Walzen. Wenn Sie sich die in jedem Schritt zu addierenden
Werte nicht merken, dann können Sie auch von einer bestimmten Stellung
nicht wieder zur Ausgangsstellung zurückgelangen.
Eine
bestimmte Ausgangs-Walzenstellung wird nun dazu verwendet, eine Datei mit Hilfe
von 16 Byte großen Schlüssel-Blöcken zu verschlüsseln (man spricht in diesem
Fall auch von einer Blockchiffre). Hierzu werden die entsprechenden
Ausgang-Bytes in der Datei jedes für sich wie eine Walze nach vorne rotiert, um
wie viele Positionen, das steht in dem entsprechenden Schlüssel C(K) (C
ist ein Array mit 16 Bytes, und C ist die Abkürzung für Chiffre).
Um die spätere Entschlüsselung noch weiter zu erschweren, wird das Array C
jedes Mal, wenn es vollständig benutzt wurde (also alle 16 Bytes) neu
berechnet, man spricht in diesem Fall auch von einer neuen Runde.
Schauen Sie sich nun das nächste Listing an:
23-CHIFFRE
10 DIM A(16):DIM
B(16):DIM C(16)
20
PRINT"[SHIFT+CLR/HOME]KEY-DATEI";:INPUT K$:
A=INT(255*RND(0))
30 FOR I=0 TO 15:A(I)=INT(255*RND(1)): NEXT I
40 FOR I=0 TO 15:B(I)=INT(255*RND(1)): NEXT I
50 OPEN
15,8,15,"S:"+K$:CLOSE 15
60 OPEN 1,8,1,K$
70 FOR I=0 TO 15:PRINT#1,CHR$(A(I));:NEXT I
80 FOR I=0 TO 15:PRINT#1,CHR$(B(I));:NEXT I
90 CLOSE 1
100 PRINT"ZU VERSCHLUESSELNDE DATEI";:INPUT
N$
110 PRINT"DATEILAENGE";:INPUT L
120
PRINT"[SHIFT+CLR/HOME]KEY:"
130 FOR I=0 TO 15:POKE 1028+I,A(I):NEXT I
140 FOR I=0 TO 15:POKE 1028+16+I,B(I):NEXT I
150 PRINT"[CLR/HOME]+[CURSOR UNTEN]DATEI
VERSCHLUESSELN..."
160 FOR I=0 TO 15:C(I)=A(I):NEXT I
170 OPEN 1,8,2,N$
180 P=0:Q=0:K=0
190 GET#1,A$:IF
A$="" THEN B=0:GOTO 210
200 B=ASC(A$)
210 B=B+C(K):IF B>255 THEN B=B-256
220 POKE 1024+Q,B:POKE 16384+P,B
230 Q=Q+1:IF
Q=1000 THEN Q=0:PRINT"[SHIFT+CLR/HOME]"
240 K=K+1:IF
K>15 THEN K=0:GOSUB 1000
250 P=P+1:IF
P<L THEN GOTO 190
260 CLOSE 1
270 OPEN
15,8,15,"S:"+N$:CLOSE 15
280 OPEN 1,8,1,N$
290 FOR I=0 TO L-1
300 PRINT#1,CHR$(PEEK(16384+I));
310 NEXT I
320 CLOSE 1
330 END
1000 REM *** NEUEN SCHLUESSEL HOLEN ***
1010 FOR I=1 TO 8
1020 FOR J=0 TO 15
1030 C(J)=C(J)+B(J):IF C(J)>255 THEN C(J)=C(J)-256
1040 NEXT J
1050 NEXT I
1060 RETURN
In Zeile
10 werden zunächst folgende Arrays mit jeweils 16 Zahlen angelegt: Das
Array A enthält den Initialschlüssel, der ganz am Anfang
generiert wird, und das Array B enthält den Schlüssel, der nach der
ersten Runde verwendet wird, um den aktuellen Schlüssel C aus A
und B neu zu berechnen. Das Array C enthält also stets den
Schlüssel, der gerade benutzt wird. Um den Zufallsgenerator so zu
initialisieren, dass nicht bei jedem Programmstart stets dieselben
Zufallszahlen gezogen werden, wird nun in Zeile 20 zunächst der
Dateiname der Datei eingelesen, in die der Initialschlüssel (Array A)
und der Rundenschlüssel für die zweite Runde (Array B) abgelegt
wird. Während Sie den Dateinamen eingeben, läuft die Computeruhr natürlich
weiter (da diese über ein Interrupt gesteuert wird), und da Sie nicht immer
gleich schnell tippen, können Sie auch nicht genau ermitteln, welchen Wert die
Computeruhr nach Drücken von RETURN schließlich hat. Deshalb wird in Zeile
20 auch zusätzlich zu dem INPUT-Befehl die RND()-Funktion
mit dem Parameter 0 (statt 1) aufgerufen, um dadurch den Zufallsgenerator mit
dem Wert der Computeruhr neu zu initialisieren. Es ist an dieser Stelle
übrigens egal, welche Zufallszahl Sie sich an dieser Stelle zurückgeben lassen,
Hauptsache, Sie rufen die RND()-Funktion
einmal am Anfang mit dem Parameter 0 auf.
In Zeile
30 wird nun der Initialschlüssel A erzeugt, indem 16 Zufallszahlen
zwischen 0 und 255 in das Array A geschrieben werden. In Zeile 40
wird anschließend der Rundenschlüssel B erzeugt, indem 16 Zufallszahlen
zwischen 0 und 255 in das Array B geschrieben werden. Die Zeilen
50-90 enthalten nichts wirklich Neues, und müssen deshalb auch nicht
ausführlich erklärt werden: In Zeile 50-90 wird eine Datei mit dem Namen
K$ erzeugt (dieser wurde vorher in Zeile 20 eingegeben), die das
Array A und B in Form von einzelnen Bytes enthält. Nun benötigen
Sie noch den Namen der Datei, die Sie verschlüsseln wollen (dieser wird in Zeile
100 eingelesen und in dem String N$ abgelegt). Sie benötigen aber
zusätzlich auch die Länge der zu verschlüsselnden Datei. Diese wird in Zeile
110 eingelesen und in der Variablen L abgelegt. Sie wundern sich an
dieser Stelle vielleicht, warum Sie nicht die Länge einer Datei in Bytes
ermitteln können, aber es ist so: Die Länge einer Datei wird in
der Directory in Blocks eingetragen, nicht in Bytes, und so müssen Sie
die korrekte Anzahl an zu verschlüsselnden Bytes stets per Hand eingeben.
Nach
Eingabe des Dateinamens der zu verschlüsselnden Datei und der Länge in Bytes
kann der eigentliche Verschlüsselungsalgorithmus ausgeführt werden. Vorher
werden aber noch die Bytes des Arrays A und B als PETSCII-Zeichen
angezeigt (Zeile 120-150) und anschließend wird A nach C
kopiert (Zeile 160). Anschließend wird in Zeile 170 die zu
verschlüsselnde Datei mit OPEN geöffnet, allerdings nicht mit der
Sekundäradresse 1, sondern mit der Sekundäradresse 2- nur auf diese Weise kann
der Befehl GET# benutzt werden. Zusätzlich zum Öffnen der zur
verschlüsselnden Datei müssen die Zähler P (laufende Nummer des aktuell
verschlüsselten Bytes), Q (aktuelle Bildschirmposition) und K
(aktuell verwendetes Byte des Schlüssels im Array C) auf 0 gesetzt
werden (Zeile 180).
Danach startet
der eigentliche Verschlüsselungsalgorithmus, der in Zeile 190 zunächst
ein neues Byte der zu verschlüsselnden Datei mit GET# einliest.
Allerdings unterstützt GET# keine Fließkommazahlen, und wandelt die
Bytes auch nicht in solche um. Stattdessen wird ein String mit einer Länge von
einem Zeichen erzeugt (hier ist dies A$), und wenn der String als erstes
Zeichen ein Null-Byte enthält, wird er als leer betrachtet. Wenn A$ also
leer ist, weil Sie zuvor ein Null-Byte eingelesen haben, setzt Zeile 190
B (B enthält hier das zuletzt eingelesene Byte) auf 0 und
überspringt die Wandlung des ersten ASCII-Zeichens in A$ in einen
numerischen Wert. Die Verschlüsselung selbst ist relativ einfach: In Zeile
210 wird zunächst der Wert der Byte-Wert, der in C(K) steht, zu B
addiert. Wenn B nun größer ist, als 255, dann wird von B 256
subtrahiert. Auf diese Weise erhalten Sie eine Addition Modulo 256, und
diese Addition kann auch umschlagen. Das bedeutet nichts Anderes,
als dass (255+1) mod 256=0 ist, und Sie in diesem Fall
den Ursprungswert 255 nicht mehr ohne Kenntnis des Schlüssels rekonstruieren
können. In Zeile 220 wird nun das Ergebnis der Verschlüsselung einmal
auf dem Bildschirm angezeigt (durch POKE 1024+Q,B),
und einmal in die Adresse 16384+P geschrieben. Die verschlüsselte Datei
wird also zunächst in einem Puffer abgelegt, und erst am Ende durch das
Chiffrat ersetzt. In Zeile 230 wird nun der Zähler Q um 1 erhöht.
Q enthält die Anzahl der bereits auf dem Bildschirm ausgegebenen
Zeichen, und wenn Q=1000 ist, dann ist der Bildschirm voll und muss nach
Rücksetzen von Q auf 0 gelöscht werden. In Zeile 240 wird
anschließend K um 1 erhöht. K enthält den Array-Index des Bytes
im aktuellen Schlüssel C, das für die Verschlüsselung des nächsten Bytes
der Datei benutzt wird. Wenn K=16 ist (wir befinden uns nun außerhalb
des Schlüssels C), wird deshalb K auf 0 zurückgesetzt, und der
aktuelle Schlüssel C wird mittels Unterprogramm neu berechnet.
Allerdings muss in Zeile 250 auch zusätzlich P um 1 erhöht
werden, denn an Adresse 16384+P wird das nächste verschlüsselte Byte
abgelegt. Solange P<L ist, wird nun durch einen Rücksprung zu Zeile
190 ein neuer Durchlauf des Verschlüsselungsalgorithmus ausgeführt, was
bedeutet, dass das nächste Byte aus der Datei gelesen und wird. Ist allerdings P>=L,
so wird die zu verschlüsselnde Datei geschlossen und der Hauptalgorithmus
beendet.
In Zeile
260-330 wird nun die alte zu verschlüsselnde Datei durch die verschlüsselte
Datei ersetzt, die sich in dem Puffer an Adresse 16384 bis 16384+L
befindet. Dazu wird die alte Datei zunächst gescratcht,
und anschließend erneut mit OPEN geöffnet, allerdings mit der
Sekundäradresse 1. An dieser Stelle erkennen Sie sicherlich den Zweck des
Puffers: Um mit PRINT# die korrekten Bytes in eine Datei zu schreiben,
muss diese die Sekundäradresse 1 verwenden, und um die Datei zu überschreiben,
muss diese zunächst mit dem Scratch-Befehl gelöscht werden (Sie
erinnern sich, dass die 1541 ein Replace-Bug hat?)
Nur durch dieses Verfahren (also durch das Verwenden eines Puffers) können die
Bytes der verschlüsselten Datei korrekt auf die Diskette zurückgeschrieben
werden.
8.3.4
Ausgabe der Directory (aus dem laufenden
BASIC-Programm heraus)
Wenn Sie
zurzeit das Inhaltsverzeichnis (das sogenannte Directory) anzeigen wollen, dann
müssen Sie die BASIC-Kommandos
LOAD"$",8
LIST
benutzen.
Dies hat allerdings den Nachteil, dass dadurch Ihr aktuelles BASIC-Programm
zerstört wird. Auch
LOAD"$",8,1
erreicht nicht
das gewünscht Ziel, denn der Wert, der in der Datei $ an Position 0 und
1 steht, verweist (zumindest, wenn Sie $ als Maschinenprogramm
betrachten) auf die Adresse 1024- aber da fängt auch der Bildschirmspeicher an.
Wenn Sie also die Directory innerhalb eines
BASIC-Programms anzeigen wollen, ohne das laufende Programm zu beenden oder zu
zerstören, müssen Sie die Dateinamen direkt aus den entsprechenden
Diskettenblöcken in Spur 18 auslesen. In der Tat müssen Sie hierzu
Block-Lese-Kommandos in Form von U1-Befehlen an die 1541 senden.
Zum Glück
ist die Sache nicht so schwierig, weil das Inhaltsverzeichnis einer Diskette
eine normale Datei ist, die als ersten Block den Block 1 der Spur 18 verwendet.
Außerdem ist das Inhaltsverzeichnis sehr einfach aufgebaut: Wie bei jeder Datei
steht in den ersten zwei Bytes der Links zum nächsten Block in der Form (Spur,
Sektor). Anschließend folgen schon die Daten der ersten Datei auf der Diskette.
Wenn Sie nun den ersten Block der Directory in einen
256-Byte-Puffer laden, steht der Name der ersten Datei an Byte-Position 5 im
Puffer. Der Dateiname selbst ist 16 Zeichen lang, und kann auch Leerzeichen
enthalten. Plätze, die im Dateinamen nicht benutzt werden, werden mit dem Wert
160 belegt. Direkt vor dem Dateinamen befinden sich noch drei Bytes, die das
Dateiattribut, den Startblock und den Startsektor angeben, allerdings wird von
diesen Informationen im nächsten Beispiel nur das Dateiattribut benutzt. Es
werden also keine Dateien in den Speicher geladen, sondern nur die Namen
angezeigt. Die Information, wie groß die Datei (in Blöcken) ist, steht bei dem
ersten Dateieintrag an Position 25, alle übrigen Positionen nach dem Dateinamen
werden mit Null-Bytes aufgefüllt. Der Eintrag mit den Daten für den Dateinamen
der zweiten Datei beginnt an der Byte-Position 5+32, die Daten für den Namen
der dritten Datei beginnen an Position 5+32+32, usw. Ein Directory-Block kann
genau 8 Einträge mit Dateiattributen und Dateinamen aufnehmen, danach muss der
Link-Block nachgeladen werden. Die Directory ist voll,
wenn alle Blöcke von Spur 18 belegt sind. Schauen Sie sich nun das nächste
Listing an, das das gesamte Inhaltsverzeichnis einer Diskette anzeigt.
24-DIRECTORY
10 PRINT"[SHIFT+CLR/HOME]";
20 FOR I=49152 TO 49173:READ
A:POKE I,A:NEXT I
30 PRINT"LADE DIRECTORY..."
40 T=18:S=1
50 GOSUB 10000:L=PEEK(16384):M=PEEK(16385):P=5
60 FOR J=1 TO 8
70 FOR I=P TO P+16
80 B=PEEK(16384+I)
90 IF (B<>160) THEN PRINT CHR$(B);
100 NEXT I
110 IF PEEK(16384+P+25)=0
THEN GOTO 130
120 PRINT TAB(16);STR$(PEEK(16384+P+25));
130 IF PEEK(16384+(P-3))=0
THEN PRINT TAB(20);"(DEL)";
140 P=P+32:PRINT
150 NEXT J
160 IF L=0 THEN GOTO 180
170 T=L:S=M:GOTO 50
180 END
10000 REM *** LADE DISKBLOCK ***
10010 OPEN 15,8,15
10020 OPEN 2,8,2,"#"
10030 PRINT#15,"U1:2 0"+STR$(T)+STR$(S)
10040 INPUT#15,E$,F$,G$,H$
10050 IF E$<>"00" THEN GOTO 10080
10060 SYS 49152:CLOSE
15:CLOSE 2
10070 RETURN
10080 PRINT E$,F$,G$,H$:END
20000 REM *** FUELLE PUFFER ***
20010 DATA
162,2,32,198,255,160,0,162,2,32,207,255,153,0,64,200,208,245,32
20020 DATA 204,255,96
Wenn Sie
das Programm mit RUN starten, wird in Zeile 10 zunächst der
Bildschirm gelöscht. Anschließend liest Zeile 20 ein kleines
Maschinenprogramm ein, das mit SYS 49152 aufgerufen werden kann. Dieses
Maschinenprogramm macht nichts anderes, als die folgenden BASIC-Zeilen zu
emulieren:
20010 FOR I=0 TO 255
20020 GET#2,A$:IF A$=”” THEN B=0:GOTO 20040
20030 B=ASC(A$)
20040 POKE 16384+I,B
20050 NEXT I
Wenn Sie
die obigen Zeilen allerdings als reines BASIC-Programm implementieren würden,
würde die Ausgabe des ganzen Inhaltsverzeichnisses eine Stunde dauern.
In Zeile
30 wird nun die Meldung
LADE DIRECTORY…
ausgegeben,
in Zeile 40 wird anschließend der aktuelle Track T auf 18, und
der aktuelle Sektor S auf 1 gesetzt. Der Block mit den Daten (Spur T,
Sektor S) wird nun in einen Puffer geladen, der an der Adresse 16384
beginnt, und der 256 Bytes enthält. Sofort danach wird der Link-Block
ermittelt, L enthält hier die Link-Spur, und M den Link-Sektor.
Das Laden des Blocks in den Speicher übernimmt an dieser Stelle ein
Unterprogramm, das mit GOSUB 1000 aufgerufen wird. Dieses Unterprogramm
öffnet zunächst mit OPEN den Kommandokanal, und anschließend mit OPEN
2,8,2,"#" einen Kanal für einen Datenpuffer. In Zeile 10030
wird dann das entsprechende U1-Kommando an die Floppy (über Kanal 15)
gesendet, das den Block mit den Daten (Spur T, Sektor S)
einliest. Beachten Sie an dieser Stelle, dass dem eigentlichen Floppy-Kommando
ein Doppelpunkt folgt, und Sie die Parameter (Pufferkanal (2), Laufwerk (0),
Track (T), Sector (S)) als Text-String übergeben
müssen, bei dem die Parameter durch Leerzeichen getrennt werden. Wenn der Floppy-Befehl
anschließend korrekt ausgeführt wird, dann befinden sich die 256 Bytes des
Blocks mit den Daten (Spur T, Sektor S) im Puffer der Floppy, und
Sie können diesen dann auch mit SYS 49152 an die Adresse 16384 Ihres
C64-Adressraums laden. Allerdings können beim Lesen eines Blocks auch Fehler
auftreten, die angefangen werden müssen. Hierzu wird in Zeile 10040 der
Fehlerkanal in Form von vier Strings (E$, F$, G$, H$)
ausgelesen. E$ enthält die eigentliche Fehlernummer. Wenn E$
etwas anderes enthält, als „00“, dann ist etwas schiefgelaufen, und das Programm
wird direkt beendet, nachdem die gesamten Fehlerinformationen angezeigt wurden.
Wenn E$ allerdings „00“ enthält, dann wird das Unterprogramm
ordnungsgemäß beendet: Erst wird SYS 49152 aufgerufen, und anschließend
werden alle Floppy-Kanäle wieder geschlossen.
Die
Ausgabe der Dateinamen ist nun nicht mehr so schwierig. In einer Schleife
werden alle 8 Dateieinträge, die sich in dem Puffer an der Adresse 16384
befinden, nacheinander angearbeitet (Zähler J). Innerhalb der Schleife,
die die einzelnen Directory-Einträge im aktuellen Block durchscannt, wird
zunächst wird der Zeiger P auf das 6. Zeichen im Puffer (P=5)
gesetzt. Dies ist genau der Offset auf den Dateinamen im aktuellen
Directory-Eintrag. Die 16 Zeichen des Dateinamens werden nun durch eine FOR-Schleife
(Zeile 70-100) ausgegeben, jedoch wird ein Zeichen nur dann angezeigt,
wenn der ASCII-Wert nicht 160 ist. Nach dem Dateinamen müssen in Zeile
110-140 noch zusätzliche Informationen angezeigt werden. Dies ist einmal
die Größe der Datei in Blöcken (Byte-Position P+25 im aktuellen
Blockpuffer), und einmal die Information, ob eine Datei als gelöscht
gekennzeichnet wurde. Diese Information befindet sich allerdings noch vor dem
Dateinamen im Attribut-Byte, das immer den Wert 0 besitzt, wenn eine Datei
gelöscht wurde. Da der Zeiger P stets auf den Anfang des aktuellen
Dateinamens zeigt, befindet sich das Attribut-Byte an der Position P-3.
Bevor mit
NEXT J (Zeile 150) der nächste Directory-Eintrag im Blockpuffer
bearbeitet werden kann, muss in Zeile 140 P um genau 32 Bytes
nach vorn gerückt werden. Erst dadurch ist der Zeiger P aktuell und kann
erneut benutzt werden, um neue Daten zu adressieren. NEXT J springt an
dieser Stelle zurück zu Zeile 70, solange nicht der gesamte Blockpuffer
durchgescannt wurde. Wenn dies jedoch irgendwann der Fall ist, wird in Zeile
160 und 170 untersucht, ob nun ein Link-Block nachgeladen werden muss. Dies
ist immer dann der Fall, wenn L nicht 0 ist. In diesem Fall muss T=L
und S=M gesetzt werden, und das Programm muss zu Zeile 50
zurückspringen. Auf diese Weise wird der Link-Block nachgeladen, der nächste
Link-Block wird ermittelt, und P wird auf 5 zurückgesetzt. Der
Algorithmus zur Ausgabe der nächsten 8 Directory-Einträge wird also stets
wiederholt, solange es einen weiteren Link-Block gibt.