Fehler in BASIC-ROM und KERNAL
Das BASIC-ROM und das KERNAL-ROM enthalten zahlreiche Bugs[1]. Teilweise sind diese nur in einer bestimmten Revision der ROM-Bausteine enthalten.
FRE-Funktion Rückgabewert[Bearbeiten | Quelltext bearbeiten]
Der Wert des frei verfügbaren BASIC-Speichers wird zwar korrekt ermittelt, aber fehlerhaft in eine Fließkommazahl konvertiert, sobald der Wert 32767 übersteigt.
Beispiel[Bearbeiten | Quelltext bearbeiten]
Unmittelbar nach dem Einschalten eines C64 liefert
F=FRE(0) PRINT F
-26634 READY. |
Der hier als vorzeichenbehaftete 16-Bit-Ganzzahl dargestellte Wert entspricht dem vorzeichenlosen 16-Bit-Ganzzahlwert 38902.
Was geht schief?[Bearbeiten | Quelltext bearbeiten]
Die Routine berechnet den korrekten vorzeichenlosen Wert, speichert ihn in den Adressen $62 und $63 und springt dann zu einer Routine, die einen vorzeichenbehafteten Wert in eine Fließkommazahl umrechnet:
B395 STA $62 B397 STY $63 B399 LDX #$90 B39B JMP $BC44
Die Implementierung ist noch auf Modelle ausgerichtet (PET-Linie, VC-20), die üblicherweise nicht mehr als 32 KByte RAM aufwiesen und eine derartiger Betrag nicht zu erwarten war. Daher hat man sich mit der Konvertierung des Wertes in einen vorzeichenbehafteten Fließkommawert begnügt.
Workaround[Bearbeiten | Quelltext bearbeiten]
Man kann das Ergebnis korrigieren, indem man 65536 hinzuzählt, wenn es negativ ist. Am einfachsten geht das wie folgt:
F=FRE(0) PRINT F-65536*(F<0)
38902 READY. |
(Die Variable F nimmt 7 Byte in Anspruch und das leere Basic-Programm 2 Byte. Das erklärt die Differenz zu den 38911 freien Bytes, die in der Einschaltmeldung genannt werden.)
POS-Funktion mit String-Konstante als Argument[Bearbeiten | Quelltext bearbeiten]
Der POS-Funktion muss ein Argument übergeben werden, welches allerdings nie benutzt wird. Übergibt man hierbei eine String-Konstante, wird ein Eintrag auf den String-Descriptor-Stack geschrieben, aber nicht mehr entfernt. Ab dem dritten Aufruf einer derartigen POS-Funktion wird eine Fehlermeldung ausgegeben:[2]
PRINT POS("")POS("")POS("")
0 3 ?FORMULA TOO COMPLEX ERROR READY. |
PEEK-Funktion mit String-Konstante als Argument[Bearbeiten | Quelltext bearbeiten]
Der PEEK-Funktion muss ein Argument übergeben werden. Dabei wird der Typ des übergebenen Ausdrucks nicht geprüft. Übergibt man hierbei eine String-Konstante, wird ein Eintrag auf den String-Descriptor-Stack geschrieben, aber nicht mehr entfernt. Ab dem dritten Aufruf einer derartigen PEEK-Funktion wird eine Fehlermeldung ausgegeben:
PRINT PEEK("")PEEK("")PEEK("")
47 47 ?FORMULA TOO COMPLEX ERROR READY. |
IF-Anweisung mit String-Ausdruck[Bearbeiten | Quelltext bearbeiten]
Grundsätzlich wird die Ausdrucksauswertung nach dem IF nur im numerischen Kontext verwertet, fälschlicherweise auch dann, wenn die Auswertung einen Wert vom Typ "String" ergibt. Der Interpreter stellt dabei nicht den numerischen Typ sicher, was wiederum zu verschiedenen Effekten führen kann.
Keine Auswertung[Bearbeiten | Quelltext bearbeiten]
Die Angabe einer String-Variablen als Bedingung in einem IF wird ignoriert, stattdessen wird der interne Fließkomma-Akku ausgewertet, der potenziell von früheren Ausdrücken oder sonstigen im Ausdruck stattfindenden nummerischen Berechnungen (Array-Index beispielsweise) gesetzt ist.[3]
1 B=0:IF A$ THEN PRINT"WIRD NICHT AUSGEGEBEN" 2 B=1:IF A$ THEN PRINT"WIRD AUSGEGEBEN"
oder
1 B=1 2 IF A$(0) THEN PRINT "WIRD NICHT AUSGEGEBEN"
Hier ist nicht die Zuweisung aus Zeile 1 ausschlaggebend (sie würde veranlassen, den THEN-Zweig zu beschreiten), sondern die Index-Berechnung bewirkt das Setzen des Fließkomma-Akkus auf 0 und die IF-Bedingung ist damit nicht erfüllt.
Formula too complex[Bearbeiten | Quelltext bearbeiten]
Auf die IF-Anweisung muss ein Ausdruck folgen. Handelt es sich dabei um einen String, kann dies zu einer Fehlermeldung führen:
IFA$+""THENIFA$+""THENIFA$+""THENIFA$+""THENPRINT"X"
?FORMULA TOO COMPLEX ERROR READY. |
Ursache ist hier der Überlauf des String-Descriptor-Stacks. Die IF-Bedingung erwartet kein String-Ergebnis und sorgt somit auch nicht eine für diesen Fall notwendige explizite Bereinigung des Stacks, was dann mit jedem weiteren solchen Aufruf den Stack füllt.
Umwandlung der Befehlswörter in Tokens[Bearbeiten | Quelltext bearbeiten]
Bei der Eingabe von Befehlswörtern können Abkürzungen verwendet werden, indem nur der erste Buchstabe (oder die ersten Buchstaben) und der nächste Buchstabe mittels Shift eingegeben wird. Beispielsweise kann WAIT durch W und Shift +A oder durch W ,A , Shift +I abgekürzt werden. Fehlerhaft wird das Ganze, wenn der letzten Buchstaben des Befehlsworts mit Shift gedrückt eingegeben wird.[4]
Beispiel[Bearbeiten | Quelltext bearbeiten]
(Der Zeichensatz wurde zur besseren Lesbarkeit auf Klein-/Großschrift umgestellt)
waiT198,1
?syntax error ready. |
waiT=3 print wa
3 ready. |
waiTload198,1
In den ersten beiden Beispielen verhält sich der Interpreter so, als hätte man einfach nur "WAI" eingegeben, eine Variable. Im letzten Beispiel wird der WAIT-Befehl hingegen korrekt ausgeführt.
Was geht schief?[Bearbeiten | Quelltext bearbeiten]
In der Schleife ab $A5B6 wird die Eingabe mit der Token-Tabelle ab $A09E verglichen:
A5B6 INY A5B7 INX A5B8 LDA $0200,X A5BB SEC A5BC SBC $A09E,Y A5BF BEQ $A5B6 A5C1 CMP #$80 A5C3 BNE $A5F5 ; Token erkannt
Die Token sind in der Token-Tabelle dabei so gespeichert, dass jeweils beim letzten Symbol das Bit 7 gesetzt ist, was der Eingabe des Buchstabens mit Shift-Taste entspricht. Wenn Eingabe und Token-Tabelle exakt übereinstimmen, ist der Vergleich noch nicht fertig und das nächste Zeichen wird verglichen (Sprung in $A5BF). Wenn es sich nur in Bit 7 unterscheidet, wird das Token erkannt und gespeichert (Sprung in $A5C3, falls das nicht der Fall ist.)
Das ist recht praktisch, weil so gleichzeitig vollständig eingegebene Befehlswörter, als auch die abgekürzten erkannt werden können. Wenn man allerdings das letzte Zeichen eines Befehlsworts geshiftet eingibt, erkennt die Routine das Ende der Schleife nicht und prüft weitere Eingabezeichen. Hierbei können zwei Dinge passieren:
- Die nachfolgenden Eingabezeichen stimmen nicht überein. Dann wird das erste Zeichen des Befehlsworts einfach als Einzelzeichen gespeichert und ab dem nächsten Zeichen erneut nach einem Token gesucht.
- Die nachfolgenden Eingabezeichen stimmen mit dem nachfolgenden Befehl in der Token-Tabelle überein. Dann wird diese Eingabesequenz durch das Token des ersten Befehls ersetzt und alles andere gelöscht (siehe oben bei waiTload). Das lässt sich auch noch über weitere Befehle fortsetzen, beispielsweise waiTloaDsave.
Weitere Merkwürdigkeiten[Bearbeiten | Quelltext bearbeiten]
Durch diesen Fehler kommt es zu einer Reihe an Merkwürdigkeiten:
Das Ende eines Befehls ist gleichzeitig der Anfang eines anderen Befehls[Bearbeiten | Quelltext bearbeiten]
10lisT list
10 listop ready. |
Hier wird S , Shift +T zu STOP expandiert, weil in der Token-Tabelle die Endemarkierung mit gesetztem Bit 7 keine Übereinstimmung ergibt und, nachdem l und i gespeichert wurden die Suche nach weiteren Token die Abkürzung von STOP erkennt.
Vergleichszeichen[Bearbeiten | Quelltext bearbeiten]
10oR<C=V><C=X><C=C>sgn list
10 or ready. |
Vergleichszeichen werden ebenfalls als eigenständige Token abgespeichert. Mit gesetztem Bit 7 handelt es sich um die Zeichen V,X und C, jeweils zusammen mit der C=-Taste gedrückt.
Unmögliche Variablen[Bearbeiten | Quelltext bearbeiten]
10iFf=10 20ifiFf>3then?"huhu" list
10 if=10 20 ifif>3thenprint"huhu" ready. |
run
huhu ready. |
Normalerweise kann man die Variable if nicht benutzen, da stattdessen das Token für den IF-Befehl benutzt wird. Hier werden die beiden Zeichen aber separat gespeichert, wodurch es zu einer Variablen wird. Das geht auch mit anderen zweibuchstabigen Token (on, to, fn, or und go)
Sonderzeichen[Bearbeiten | Quelltext bearbeiten]
10input<C=T>input8,a$ 20printmid<c=@>go(a$,2,2) list
10 input#8,a$ 20 printmid$(a$,2,2) ready. |
Der Fehler tritt auch bei Befehlen auf, die auf ein Sonderzeichen enden, hier INPUT# und MID$.
Auswertung von Ausdrücken[Bearbeiten | Quelltext bearbeiten]
Die Addition einer negativen Zahl zu einem String führt nicht zum Fehler ?TYPE MISMATCH ERROR, sondern zu einem Reset oder gar dem Absturz des Computers.[5][6] (PRINT"A"+-100
oder PRINT.+""+-.
)
Auswertung von Zeilennummern in Basic-Programmen[Bearbeiten | Quelltext bearbeiten]
Die Eingabe von ungeeigneten Zeilennummern in der Eingabe führt dazu, dass der Maschinencode ab $A957 ausgeführt wird. In der Regel führt dies zu einem Sprung an die Adresse 31141 ($79A5). Betroffen sind alle Zahlen, die mindestens 6 Stellen lang sind und mit 35072 bis 35327 beginnen.[7]
Beispiel[Bearbeiten | Quelltext bearbeiten]
A=31141:POKEA,238:POKEA+1,32:POKEA+2,208:POKEA+3,76:POKEA+4,165:POKEA+5,121
READY. |
353270
Die erste Zeile installiert ab Adresse 31141 ein kleines Maschinenspracheprogramm, welches einfach nur die Rahmenfarbe immer wieder erhöht (loop: INC $D020, JMP loop). Nachdem man nach "353270" die Return-Taste gedrückt hat, wird dieses Programm ausgeführt.
Was geht schief?[Bearbeiten | Quelltext bearbeiten]
Wenn eine eingegebene Zeile mit einer Ziffer beginnt, wird die Routine "LINGET" im BASIC-ROM ab $A96B ausgeführt. Ab $A97B wird dort überprüft, ob das High-Byte der bislang berechneten Zahl $19 übersteigt (was nichts anderes heißt, dass die Zahl mindestens 6400 ist). Ist das der Fall, sollte eigentlich der Fehler ?SYNTAX ERROR ausgegeben werden. Die Routine spring in diesem Fall mitten in die Routine, die den ON-Befehl auswertet (nach $A953). Es ist anzunehmen, dass in einer früheren BASIC-Version an dieser Stelle einfach nur ein Sprung zur Fehlerausgabe war.[8][9]
Inzwischen befindet sich dort aber anderer Code. Als erstes wird der Akku auf den Wert $89 geprüft. Im Akku befindet sich immer noch das High-Byte der bislang berechneten Zahl, die Routine vermutet dort aber das nächste Zeichen aus dem Eingabepuffer. Der Code soll eigentlich überprüfen, ob das nächste Zeichen das GOTO-Token ist und wenn nicht, eine Fehlermeldung ausgeben.
Bei den Zahlen 35072 bis 35327 ist das High-Byte $89, weshalb in diesem Fall keine Fehlermeldung ausgegeben wird, sondern der weitere Code des ON-Befehls ausgeführt wird:
A957 DEC $65 A959 BNE $A95F A95B PLA A95C JMP $A7EF A95F JSR $0073 A962 JSR $A96B A965 CMP #$2C A967 BEQ $A957 A969 PLA A96A RTS
Als erstes wird die Speicherzelle $65 um eins verringert. Es handelt sich dabei um das unterste Byte der Mantisse des FAC. Meist befindet sich dort eine 0 (im Fall des ON-Befehl stünde dort der Wert zwischen ON und GOTO/GOSUB). Wenn nach dem Verringern keine 0 in der Speicherzelle steht, wird das nächste Zeichen aus der Eingabe geholt. Handelt es sich um kein Komma, wird ein Byte von Stack entfernt. Dummerweise befinden sich dort aber Rücksprungadressen ($A49E, und $A679), die jeweils zwei Bytes lang sind. Durch dieses Entfernen wird deswegen der Stack durcheinander gebracht und der nachfolgende RTS-Befehl führt einen Sprung an die Adresse $79A5 aus.
Weitere Vorkommnisse[Bearbeiten | Quelltext bearbeiten]
Dieser Bug betrifft zudem alle Befehle, die eine Zeilennummer als Argument nehmen: LIST, GOTO, GOSUB, ON, THEN. Handelt es sich bei dem Argument um eine der oben beschriebenen Zeilennummern, wird der Stack durcheinander gebracht und Code an mehr oder minder zufälligen Stellen im Speicher ausgeführt. Welche Stelle genau ausgeführt wird, hängt vom jeweiligen Befehl ab.
Ein Tag dauert zu lang[Bearbeiten | Quelltext bearbeiten]
Die Systemvariablen TI und TI$ für der Zeitverwaltung sind im Wertebereich auf einen Tag beschränkt, allerdings enthält der Wertebereich sowohl 0:00:00 Uhr als auch 24:00:00 Uhr, wodurch der Tag eine Sechzigstelsekunde zu lang dauert.
Beispiel[Bearbeiten | Quelltext bearbeiten]
10 TI$="235958" 20 PRINTTI$,TI 30 IFTI<10ORTI>1000THEN20
Am Ende der Ausgabe steht folgendes:
235959 5183989 235959 5183992 235959 5183996 240000 5184000 000000 3 000000 7 READY. |
In der vierten Zeile wird links 24:00:00 Uhr ausgegeben, rechts ist der Wert bereits um 1/60 Sekunde später entweder 5184000 und schon auf 0.
Die Ausgabe kann je nach Emulator oder Hardware leicht unterschiedlich ausfallen und auch am gleichen System leicht variieren.
Was geht schief?[Bearbeiten | Quelltext bearbeiten]
Direkt nachdem die Uhrzeit im Interrupt um 1/60-Sekunde erhöht wurde, wird überprüft, ob der Wert $4F1A01 erreicht wurde und falls ja, die Uhr auf 0 zurück gesetzt (das X-Register enthält den Wert 0):
F6A7 SEC F6A8 LDA $A2 F6AA SBC #$01 F6AC LDA $A1 F6AE SBC #$1A F6B0 LDA $A0 F6B2 SBC #$4F F6B4 BCC $F6BC F6B6 STX $A0 F6B8 STX $A1 F6BA STX $A2
Der korrekte Wert, auf den geprüft werden sollte, wäre $4F1A00.
Workaround[Bearbeiten | Quelltext bearbeiten]
Im Grunde ist ein Workaround nicht notwendig, da diese Uhr, gesteuert über den abschaltbaren Interrupt IRQ, ohnehin sehr unzuverlässig ist. Beispielsweise wird sie während Lade- und Speichervorgängen angehalten. Für exakte Zeitmessungen sind die Echtzeituhren der beiden CIA-Bausteine besser geeignet. Alternativ kann man aber auch das Kernal-ROM ins RAM kopieren und dann in Speicherzelle $F6AB eine 0 schreiben: POKE 63147,0
. (Im Grunde genommen ist dann der Programmcode von $F6A8 bis $F6AB überflüssig.)
ON ... GO TO[Bearbeiten | Quelltext bearbeiten]
Den GOTO-Befehl kann man auch mit Leerzeichen zwischen GO und TO schreiben. Allerdings nicht, wenn das GOTO in einem ON-...-GOTO-Konstrukt verwendet wird. Bei der Auswertung des ON-Befehls wird das separate Token für GO nicht berücksichtigt:
10 GO TO 20 20 ON 1 GO TO 10 RUN
?SYNTAX ERROR IN 20 READY. |
RND-Minderleistung[Bearbeiten | Quelltext bearbeiten]
Die Generierung von Zufallszahlen mittels RND-Funktion führt mitunter sehr kurzen Zahlenfolgen, bei denen sich die Zahlen nicht wiederholen. Die Qualität für Zufallsfolgen kann somit als nicht sonderlich stabil angesehen werden. [10]
Im Modus, wo sich die Zufallsgenerierung auf diverse Hardware-Register stützt, um die Qualität des "Zufalls" zu verbessern, werden die Register des CIA 1 herangezogen, wobei konkret die Register von Timer A, RTC 1/10s- und 1s herangezogen werden. Die Register der RTC liefert allerdings keinen Beitrag, da sie nicht initialisiert bzw. gestartet wird.
Fehler der Fließkommaarithmetik[Bearbeiten | Quelltext bearbeiten]
So manche Fließkommaberechnung führt zu Ungereimtheiten bzw. zu falschen Ergebnissen.[11] Teilweise ziehen sich diese Fehler bis BASIC 3.5 durch, sind aber erst bei BASIC 7.0 behoben. Für den C64 und VC-20 (oder generell BASIC V2) gibt es entsprechende Patches. [12][13][14]
Beispiel:[15]
A=16777217:?10*A,A*10
167772165 167772170 READY. |
Strings ohne Ende[Bearbeiten | Quelltext bearbeiten]
Nicht wirklich ein Fehler, aber eine erwähnenswerte ungewöhnliche Eigenschaft des Parsers:
Strings kann man in BASIC als Zeichenkette, die in Anführungszeichen steht, angeben. Das schließende Anführungszeichen wird dabei aber nicht überprüft, weshalb Zeichenketten, die am Ende einer Zeile stehen auch ohne dieses angegeben werden können. Beispiel:
10 C$="'": A$="XYZ 20 PRINT C$A$C$
'XYZ' READY. |
Dies ist auch im Direktmodus oder in DATA-Zeilen möglich.
VAL-Funktionsanomalie[Bearbeiten | Quelltext bearbeiten]
Ein Phänomen[16], das nur im Fehlerfall einen Effekt hervorruft, sonst aber bei einem fehlerfreien Programmverlauf keine Auswirkungen hat.
10 A$="A"+"B" 20 B$="1"+"E39" 30 PRINT VAL(B$) 40 PRINT "<"A$"><"B$">"
?OVERFLOW ERROR IN 30 READY. GOTO 40 <1E39> |
Tatsächlich sollte die Ausgabe <ab><1e38>
anzeigen, wenn B$ mit einem zulässigen Wert wie "1E38" gesetzt wird.
Am String-Heap wird ein etwaiger unmittelbar folgender aktiver String korrumpiert (das erste Byte ist dann ein 0-Byte). Befindet sich der String jedoch im Programmtext, dann wird das Programm korrumpiert und der dem VAL-Aufruf folgende Programmcode in der Zeile "verschwindet" bzw. wird nicht mehr angezeigt und ausgeführt. Das Hinzufügen von neuen Zeilen, was ein Rechaining des Programmcodes auslöst, kommt dann durcheinander und die Restzeile wird als eigenständige Zeile rekonstruiert, deren Zeilennummer vermutlich nicht mehr passend ist und die Programmstruktur selbst den Anfang des Restes überschreibt. In der Regel wird diese auftauchende Zeile nicht mehr lauffähig sein.
10 PRINT VAL("1E39"):PRINT "VERSCHWINDET ..."
RUN ?OVERFLOW ERROR IN 10 READY. LIST 10 PRINT VAL("1E39 |
Hintergrund
Die VAL-Implementierung setzt am Ende des Strings ein Null-Byte, damit der Zahlenparser den String verarbeiten kann. Das vom Null-Byte ersetzte Byte wird dann wieder restauriert. Im Falle eines fehlerbedingten Abbruchs, passiert diese Wiederherstellung nicht mehr und das Null-Byte verbleibt am String-Heap oder im Programmcode, wo es entsprechenden Schaden anrichtet.
Referenzen[Bearbeiten | Quelltext bearbeiten]
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: Sammlung und Diskussion
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: POS-Argument-Bug
- ↑ Thema: Was macht "IF A$ THEN..."? auf Forum64.de
- ↑ A Curious Bug in the Commodore BASIC Tokenizer Routine
- ↑ 64'er 11/87 "Trick des Monats - Der totale Absturz": Bug bem Auswerten von Ausdrücken, Lösung in 64'er 03/88
- ↑ Create your own Version of Microsoft BASIC for 6502, Abschnitt Bugs never fixed
- ↑ Create your own Version of Microsoft BASIC for 6502, Abschnitt Bugs never fixed
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: Erklärung Zeilennummereingabe-Bug
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: Erklärung Zeilennummereingabe-Bug #2
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: RND-Bug
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: Erklärung Fehler Fließkommaberechnung
- ↑ sleepingelephant.com: Re: Fun with CBM arithmetics : Fließkommaberechnungsfehler
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: Fließkommaberechnungsfehler
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: Patch-Programm (korrigierte und letzte Fassung) für die Beseitigung der Fließkommaberechnungsanomalie
- ↑ Thema: Die Fehler des Basic V2 auf Forum64.de: Beispiel Fließkommaberechnungsfehler
- ↑ Software Blog: Color BASIC overflow bug – same as Commodore’s?