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.