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.