Fehler in BASIC-ROM und KERNAL

Aus C64-Wiki
Zur Navigation springenZur Suche springen

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:

  1. 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.
  2. 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]

  1. Thema: Die Fehler des Basic V2 auf Forum64.de: Sammlung und Diskussion
  2. Thema: Die Fehler des Basic V2 auf Forum64.de: POS-Argument-Bug
  3. Thema: Was macht "IF A$ THEN..."? auf Forum64.de
  4. A Curious Bug in the Commodore BASIC Tokenizer Routine Sprache:englisch
  5. 64'er 11/87 "Trick des Monats - Der totale Absturz": Bug bem Auswerten von Ausdrücken, Lösung in 64'er 03/88
  6. Create your own Version of Microsoft BASIC for 6502, Abschnitt Bugs never fixed Sprache:englisch
  7. Create your own Version of Microsoft BASIC for 6502, Abschnitt Bugs never fixed Sprache:englisch
  8. Thema: Die Fehler des Basic V2 auf Forum64.de: Erklärung Zeilennummereingabe-Bug
  9. Thema: Die Fehler des Basic V2 auf Forum64.de: Erklärung Zeilennummereingabe-Bug #2
  10. Thema: Die Fehler des Basic V2 auf Forum64.de: RND-Bug
  11. Thema: Die Fehler des Basic V2 auf Forum64.de: Erklärung Fehler Fließkommaberechnung
  12. sleepingelephant.com: Re: Fun with CBM arithmetics Sprache:englisch: Fließkommaberechnungsfehler
  13. Thema: Die Fehler des Basic V2 auf Forum64.de: Fließkommaberechnungsfehler
  14. Thema: Die Fehler des Basic V2 auf Forum64.de: Patch-Programm (korrigierte und letzte Fassung) für die Beseitigung der Fließkommaberechnungsanomalie
  15. Thema: Die Fehler des Basic V2 auf Forum64.de: Beispiel Fließkommaberechnungsfehler
  16. Software Blog: Color BASIC overflow bug – same as Commodore’s? Sprache:englisch