プログラミング
10 REM"_(C2SLFF4
要約
この記事は、1980年に公開されたBASICゲーム「The Wizard's Castle」のソースコードに含まれる奇妙なコメント行「10 REM"_(C2SLFF4」の謎を解き明かそうとするものです。このコメント行が、単なるタイプミスやゴミではなく、Z80マシンコードとして実行されている可能性を探り、その仕組みと目的について詳細な技術的分析を行っています。最終的に、このコードが乱数生成器(PRNG)のシード値を設定するための隠されたルーチンであることが判明します。
全文翻訳
Beej's Bit Bucket ⚡ テクノロジーとプログラミングの楽しみ
2026-07-21
10 REM"_(C2SLFF4
「The Wizard's Castle」(ソースコード)を掘り下げてみましょう!
10 REM"_(C2SLFF4
これは、1980年7月にRecreational Computing誌に掲載された、Exidy Sorcererプラットフォーム向けに最初に書かれた80年代のマイクロコンピュータBASICゲーム「The Wizard's Castle」の最初の行です。
これはREM(Remark)ステートメントで、その言語におけるコメントです。
10は行番号です。もしそのようなものがある言語に慣れていないなら。
しかし、興味深いのはこの「_(C2SLFF4」という塊でした。
タイプミスかゴミか?
いいえ。それは、1980年7月にRecreational Computing誌に掲載されたオリジナルのソースコードにそのまま記載されています。
一体これは何なのでしょうか?
注:10進数(BASICフレンドリー)と16進数(プログラマーフレンドリー)の間を行き来します。
16進数は0xで始まり、hで終わるか、明らかにA-Fの数字が含まれます。
注:私のハッキング仲間のJoshとChrisに感謝します。彼らはエミュレータとリサーチに多くの作業をしてくれました。
彼らの助けなしでは、私の作業は10倍の時間がかかったでしょう。
疑念
以下は、関連性のない部分を削除し、スペースを追加したソースコードのコンテキストです。
10 REM"_(C2SLFF4
40 POKE 260,218: POKE 261,1: T = USR(0): T = PEEK(-2049)
80 Q = RND(-(2*T+1))
BASICでは、:はコマンドセパレーターです。
POKEは指定されたメモリ位置にバイト値を書き込みます。
PEEKはそこから読み取ります。
Sorcerer BASICは符号付き16ビット数を使用していたようです。そのため、アドレス-2049は65536-2049、つまり16進数で0xF7FFです。
後でそのアドレスに戻ります。
擬似乱数生成器(PRNG)RND()を負の数で呼び出すことは、それをシードします。
古いPRNGは奇数でシードされることを好んだため、2*T+1という式で奇数に強制しています。
そして、USR()関数はマシンコードルーチンを呼び出します。
そして、行80は、PEEK()から初期化された後、Tの最初の使用です。
これらのものの近接性から、それらはすべてPRNGのシードに関連しているように思われます。
Sorcerer BASICにはRANDOMIZEコマンドがなかったため、当時の唯一の選択肢は次のとおりでした。
ユーザーにランダムなシードを入力するように求める。
ユーザーがキーを押すまで(またはそれに類するユーザー駆動の何か)インクリメントループを実行し、それをシードとして使用する。
既存のソフトウェアやハードウェアから、何らかのランダムに近いものをシードとして取得する。
Wizard's Castleは最初の2つを実行しませんでした。
したがって、それは最後のものに違いありません。
REMステートメントに、ASCIIテキストにエンコードされるマシンコードが含まれている可能性はありますか?
SorcererはASCIIエンコーディングを使用していました。
しかし、ASCII文字だけで有用なZ80マシンコードが得られるというのは、狂気の沙汰のように思えます。
そうですよね?
それにもかかわらず、これは実行して何かを見つけるための、十分にクレイジーな仮説のように思えました。
USR()関数
USR()呼び出しは興味深いです。これはマシンコード関数を呼び出しますが、詳細はシステムに依存します。
しかし幸いなことに、インターネットにはさまざまな場所に技術マニュアルが豊富にあり、それを理解することができました。
アドレス259には、16ビットリトルエンディアン絶対アドレスへのJP命令(無条件ジャンプ)があります。
そして、POKEはアドレス260と261用でした。
これがUSR()関数のトランポリンでした。
マシンコードの開始アドレスをアドレス260と261にPOKEし、USR()を実行して呼び出します。
40 POKE 260,218: POKE 261,1: T = USR(0): T = PEEK(-2049)
リトルエンディアンのためにそれらを逆にすると、アドレス(1 << 8) | 218、つまり474を見ていることになります。
USR(0)を実行すると、アドレス474にあるRET命令で終わるはずのマシンコードにジャンプします。
USR()の引数(0)は、マシンコードがそれを使用する必要がある場合に参照できるように、RAMの別の場所に4バイト浮動小数点数として格納されます。
このユースケースでは関連がないことが判明しました。
また、USR()からの戻り値をTに代入していますが、この戻り値が何であるかは不明です。
しかし、次の代入でTが上書きされるため、それは無関係です。
それで…アドレス474には何があるのでしょうか?
BASIC RAM
全能のExidy Sorcerer
Marcin Wichary著、CC BY 2.0
Sorcerer BASICの行を入力すると、インタープリタはそれをトークン化し、PRINTのようなコマンドを単一バイト値に置き換えます。
PRINTは0x97にトークン化されます。
REMは0xC3などにトークン化されます。
そして、各行が「ノード」であるリンクリストとしてメモリに格納されます。
ノードの形式は次のとおりです。
2バイトの「次の」ポインタ
2バイトの行番号
トークン化されたコード行を表すバイト
1バイトのヌルターミネーター(0x00バイト)
そして、RAMにおけるこのリンクリストの開始は、Sorcererのアドレス469にあります。
謎の行をもう一度見てみましょう。
10 REM"_(C2SLFF4
これは、その行のノードが次のアドレスとバイトで構成されていることを意味します。
469 次のポインタの低位バイト
470 次のポインタの高位バイト
471 行番号の低位バイト
472 行番号の高位バイト
473 REMトークン(0xc3)
474 REMテキストの最初のバイト、引用符「」文字!!
そして474はUSR()関数が私たちを連れてくる場所です!
それは文字通りREMステートメントのテキストをZ80マシンコードとして呼び出しています!
逆アセンブル、試行1
本当でしょうか?試してみましょう!
ASCII文字の16進値は次のとおりです。
" 22
_ 5F
( 28
C 43
2 32
S 53
L 4C
F 46
F 46
4 34
最後に0x00ターミネーターがありますが、それはZ80ではNOPなので無視します。
逆アセンブルすると、次のようになります。
22 5F 28 LD (285Fh),HL ; " _ (
43 LD B,E ; C
32 53 4C LD (4C53h),A ; 2 S L
46 LD B,(HL) ; F
46 LD B,(HL) ; F
34 INC (HL) ; 4
ここではZ80アセンブリの詳細には立ち入りたくありませんが、このコードは意味をなさないと信じてください。
アドレスはどこにも特に指しておらず、最初にHLに何が入っているかは誰にもわかりません。
Bは読み取られず、重複したLD Bは無意味であり、BASICに戻るためのRETもありません。
それはゴミです。
そして、エミュレータで実行すると、予想通り奇妙なことが起こります(ソフトリセットなど)。
これは一時的に行き詰まりでした。
プリンターが実際のバイトのグリフを持っておらず、他のものに置き換えたのではないか、印刷エラーだったのではないかと推測しました。
しかし、それはあまり満足のいくものではありませんでした。
PEEK(-2049)
では、話題を変えて、PEEKに進みましょう。
40 POKE 260,218: POKE 261,1: T = USR(0): T = PEEK(-2049)
アドレス-2049には何がありますか?
これを符号なしにすると、0xF7FFに相当します。
ドキュメントによると、これはメモリマップされた画面テキストの最後のバイトであり、画面の右下にある現在の文字を表します。
この値を使用してPRNGをシードします。
40 [ ... ] T = PEEK(-2049)
80 Q = RND(-(2*T+1))
今、私は魔法の力を使って、開いているターミナルウィンドウの1つを見て、画面の右下にある文字を読み取ります。
ぼやけていますが、集中すると…それは…はい、スペース文字ですよね!
合っていますか?
それは200ドルになります。
スペースは32なので、ゲームごとに-(2*32+1)でPRNGをシードすると、このランダムに生成されたダンジョンでのリプレイ性が悪くなるため、USR()関数が何らかの役割を果たしているはずです。
例えば、画面の0xF7FFアドレスに何かを配置し、PEEK()がそれを読み取るような。
画面はその後すぐにクリアされるため、プレイヤーは一時的に右下に配置された文字のちらつきに気づかないかもしれません。
RTFM
ソースコードは、そのオリジナルのドットマトリックスの栄光で
それで、これらすべてがPRNGシードプロセスの一部であるという強い疑念がありました。
証明する方法があればよかったのに!
この時点で、Joshが、プログラムが掲載された雑誌のこの行に気づきました。
「最初のコメントはRANDOM関数をシミュレートするマシン言語ルーチンです。」
まったく。物事を読まないように教訓になります。
BASICでは、RANDOMではなくRND()がランダム関数であり、負の引数でシードを設定し、正の引数で次の乱数を取得するために使用されます。
これは歴史的に0から1の間のランダムな浮動小数点結果でした。
したがって、著者が意図したのはおそらくRANDOMIZEでしょう。
これはMicrosoft BASICでランダムな数値シードを設定したり、ユーザーに値を入力するように促したりするために人気がありました。
要するに、それは私たちが考えていたことを行うはずでしたが、マシンコードはまったくそうしているようには見えませんでした。
ブレークスルー
Joshと私はMAMEでSorcererエミュレータを実行していました(Joshに感謝します。