CBM3032 で見つけたバグを再確認
むかしむかし、あるところに
かつて、40年以上前の事、自分ではパーソナルコンピューターなんて持てなかったので、せっせと某所のマイコンの ROM ダンプをこれまた某所のプリンタで印刷して、手書きディスアセンブルしていた時期がありました。 なかでも、 CBM3032 (PET) は、 BASIC が使っているゼロページ領域が非公開だったために、絶対安全な領域を探し出すために労力をかけていました。 今回の記事は、そんなディスアセンブルの結果バグを見つけてしまったお話です。
どんなバグ?
それは、ある範囲の行番号を入れると、あらぬところに飛んで最悪の場合には暴走してしまうという恐ろしいバグでした。 たしか、プロンプトから510000と6桁の行番号(もちろん誤った行番号です)を入れるだけで CBM3032 はモニタに飛んでしまい、 VIC-1001 はリセットしてしまうのでした。 本当に 510000 が正しいのか、記憶が定かではありません。
ソースコードが公開された?
そんな中、ソースコードが公開されたというニュースが流れました。 なんと、 GitHub のリポジトリ https://github.com/microsoft/BASIC-M6502/ で公開されたとか。 これは、ぜひ、確かめに行かなくては。
行番号解析ルーチン
というわけで、行番号解析ルーチン (LINGET) を見つけました。 コメントがあると、すぐに見つかるね。 中身は、行番号の数字を一文字ずつ取ってきて、 16-bit の行番号を返すというものです。 結果は、 LINNUM のラベルのアドレスに格納されます。 とにかく、 6502 は、汎用レジスタがほぼ無いので、メモリアクセスが多めです。
アルゴリズムは、 LINNUM にまずゼロを入れておいて、数字一文字を数値に変換して LINNUM を10倍した値と足して LINNUM に格納していき、数字が途切れたら解析終了、桁数が多すぎて 64000 以上になったらエラー、というものです。 この「64000 以上になったら」を検出する仕組みが巧妙です。
LINNUM+1 には、これまで累積してきた行番号の上位 8-bit 入っていますが、これとマジックナンバー 25 を比較しています。 この時、 LINNUM には、数字一文字を取り込む前の値が入っています。 数字一文字を取り込んだ後の行番号が 64000 に達するのは、 LINNUM が 6400 に達していた時です。 つまり、 LINNUM の上位 8-bit が 6400/256=25 に達したかどうかを比較しています。 達していたら、数字一文字が続くと桁数が多くなりすぎエラーとなるので、 SNERR3 に飛び Syntax Error となります。
当時の BASIC は、とにかく少ないメモリで実現しなくてはならなかったので、エラーの種類も多くできません。 とりあえず、何かあったら、 Syntax Error にしてしまいがちです。
SNERR3 の先は、 ON ... GOTO/GOSUB を処理しているらしい所で、さらに SNERR2 に飛んでいます。
SNERR2 からは、さらに JMP 命令で SNERR に飛んでいきます。 これも、 6502 特有の技法で、とにかく射程距離の短いブランチ命令をつないで目的の場所に飛ぼうとします。 理由は、メモリが少ないからです。 ブランチ命令は 2-byte 命令で、全てのアドレスに飛ぶことができる JMP は 3-byte 命令です。 ブランチ命令を多用すると 1-byte 削減できます。
さらに困ったことに、 6502 には 6800 の BRA のような無条件ブランチの命令がありません。 そのため、フラグの値が既知であれば積極的に条件ブランチ命令を使います。 もし、フラグの値を見誤っていたら、即バグの温床です。
SNERR3 は、条件付き
と、ここまで追いかけてきて、お気づきになったでしょうか。 SNERR3 から SNERR2 に分岐しているときに CMPI 命令でアキュムレータの値に条件が付いています。
SNERR3: CMPI GOTOTK ;MUST BE "GOTOTK".
BNE SNERR2
本来、この構文は ON ... GOTO/GOSUB 命令の GOTO/GOSUB の部分を切り分けている部分で、 GOTO/GOSUB ではない単語が来たら Syntax Error としています。 必要ないはずなのに単語 GOTO を示すトークンと比較してしまっているのです。
この当時、インタプリタの字句解析は BASIC が使用する「予約語」だけはトークンと呼ばれる「最上位ビットがセットされた 8-bit の値」で表現していました。 これは、字句解析の時間を短縮できるからというよりは、メモリの消費を抑えたかったという理由が大きいかと思います。 予約語表は、ここにありました。
DCI という名前のマクロを駆使して描かれているのでわかりずらいのですが、ラベル Q の値をインクリメントしながら予約語が並んでいて、途中で Q の値を例えば GOTOTK などのラベルに格納しています。 Q の初期値は ENDTK=128 なので、GOTOTK=137 でしょうか。 つまり、 SNERR3 に飛んできたときにアキュムレータの値がちょうど 137 になっていたら、正しく Syntax Error にならずにどこかに飛んで行ってしまうのです。
アキュムレータが 137 になる条件とは
SNERR3 に飛んできたとき、アキュムレータには LINNUM の上位 8-bit の値が入っています。 つまり、十進数に直すと 137*256=35072 から 35072+255=35327 までの値に数字一文字を追加した 350720~353279 の値を行番号として入力すると暴走してしまいます。 あれあれ? 結論が記憶と違うぞ?
結論
BASIC-M6502 のソースコードが公開されたので、その昔見つけたバグを検証したのですが、バグの症状が記憶と違っていたようです。 どなたか、追試してもらえませんか。
この記事へのコメント