HN 日本語サマリー

← 一覧へ戻る
プログラミング

Infidel Goes Wild

Infidel Goes Wild (blog.zarfhome.com)

27 pointsby tobr0 コメント

要約

Infocomのゲーム「Infidel」で発見された、ゲームプレイにほとんど影響しないがメモリ破壊を引き起こす「ワイルドポインタバグ」について解説しています。著者は、ENDLESS-DESERTという無限に広がる砂漠のロジックにおけるオブジェクト管理の不具合を、ZILコードとZ-machineの実装詳細を掘り下げて分析し、その原因がコンパイラバグにある可能性を示唆しています。

全文翻訳

Infidel goes wild 金曜日、2026年10月2日 コメント: 6 (ライブ含む) (最新は4時間前) タグ: if, インタラクティブフィクション, infidel, バグ, z-machine, infocom, zil, visible zorker, patreon Planetfall は昨日の公開リリースでしたが、もちろん私は過去数週間 Infidel に集中してきました。数日前、Infocom のゲームでこれまで見た中で最もひどいバグに出くわしました。このバグは、ゲームをプレイするだけでは検出が非常に困難です。メモリレベルのデバッガ(に相当するもの)でゲームをプレイした最初の人物である私だけが、これに気づいたのでしょう。では、なぜこれをひどいバグと呼ぶのでしょうか?ほとんどの人はこれを「些細な」バグと呼ぶでしょう。なぜなら、ゲームプレイにほとんど影響しないからです。しかし、よく見てください。これは意図しない方法でメモリを書き換えるワイルドポインタバグなのです。Cプログラマーとして、メモリ破壊はあらゆる罪の中で最悪のものと見なすことが法的に義務付けられています。さらに、これはコンパイラバグです!(警告:これは ZIL コードと Z-machine の実装の詳細に入ります。Infidel release 22 serial 840522、Macintosh 用のアップデートを分析します。元のリリース、serial 830916 には、メモリのアドレスが若干異なるだけで同じバグがあります。)状況を設定しましょう。Infidel は遠く離れたエジプトの砂漠を舞台にしています。あなたは自分のキャンプから始まります。ナイル川は西にあります。東には9つの砂漠の場所があり、3x3のグリッドになっています。(そのうちの1つに、あなたが探している埋められたピラミッドがあります。)その地域外に出たり、他の方向からキャンプを離れたりすると、公式に砂漠で迷子になります。好きなだけ遠くまで旅をすることができます。ただ、もっと砂漠を見つけるだけです。(熱中症に見つかるまで。)これは Enchanter で初めて見られた既知のトリックです。ENDLESS-DESERT という単一の部屋が、マッピングされていないすべての砂漠の場所を表しています。移動すると、キャンプやマッピングされたエリアに戻ってこなければ、ENDLESS-DESERT にループバックするだけです。ゲームはあなたの緯度と経度を追跡します。これを説得力のあるものにするために、ゲームはオブジェクトを管理する必要があります。ENDLESS-DESERT に物を落としてから移動すると、それらのアイテムは舞台裏に移動します。それらの緯度/経度座標は DESERT-TABLE という配列に記録されます。後でそれらの座標に戻ると、ゲームは DESERT-TABLE から ENDLESS-DESERT にオブジェクトを移動させることができ、それらはあなたを待っているでしょう。(おそらく。ENDLESS-DESERT にアイテムを落とすと、砂に埋もれて永遠に失われる確率は3分の1です。これはここでは関係ありませんが、後で見るように、バグを発見しにくくしています。)さて、ここまでの計画は順調に見えます。実際に見てみましょう:砂漠 あなたは砂と暑さの広大な荒野である砂漠にいます。>AXE, SHOVELを落とすつるはし: 落としました。シャベル: 落としました。砂丘から強い風が吹き付け、顔に砂を巻き上げ、あなたを盲目にするのに十分な時間、シャベルから目を離してしまいました。>EAST砂漠あなたは砂と暑さの広大な荒野である砂漠にいます。>WEST砂漠あなたは砂と暑さの広大な荒野である砂漠にいます。つるはしがあります。両方の場所は同じ ENDLESS-DESERT ですが、つるはしは異なる部屋の幻想を維持しています。(背を向けた間にシャベルが静かに砂に埋もれて消える方が現実的ではないでしょうか?それはおそらくプレイテストでうまくいかなかったのでしょう。)とにかく、それはうまく機能しているようです。バグはどこにあるのでしょうか?これを機能させる DESERT-TO-TABLE ルーチンを見てみましょう:<ROUTINE DESERT-TO-TABLE ( SLOC "AUX" (TBL ,DESERT-TABLE) (CNT 0) (F <FIRST? ,ENDLESS-DESERT>) N) <REPEAT () <COND (.F <SET N <NEXT? .F>>) (ELSE <RETURN>)> <COND (<EQUAL? .F ,WINNER>) (<FSET? .F ,TAKEBIT> <REPEAT () <COND (<==? <GET .TBL .CNT> 0> <PUT .TBL .CNT .SLOC> <PUT .TBL <+ .CNT 1> .F> <SET CNT <+ .CNT 2>> <REMOVE .F> <RETURN>) (ELSE <SET CNT <+ .CNT 2>>)>>)> <SET F .N>>> これと同様の TABLE-TO-DESERT ルーチンがあり、物を元に戻します。このルーチンに焦点を当てます。このルーチンには1つの必須引数があります:SLOC、緯度/経度座標。(これは単一の数値としてエンコードされていますが、それは問題ではありません。)次に、「AUX」は4つのオプション引数を示し、これらはローカル変数としても機能します。Z-machine は区別しません。TBL は書き込む配列のアドレスです。デフォルトは DESERT-TABLE です。F は ENDLESS-DESERT の内容リストの最初のオブジェクトに初期化されます。CNT と N はゼロに初期化されます。実際には、これが呼び出されるとき、ゲームは1つの引数、SLOC のみを渡します。DESERT-TABLE=DESERT-TABLE というデフォルト値に依存しています。(ゲームに2つの無限砂漠があれば、これは2つの別々のテーブルでこのルーチンを呼び出すことを望むでしょう。しかし、そうではありません。)DESERT-TABLE はアドレス 11129 から始まる 100 個の値(200 バイト)の配列です。関数本体は ENDLESS-DESERT のすべてのオブジェクト(F から開始)をループします。ポータブルなオブジェクト(風景とプレイヤーを除く)はすべて削除され、TBL 配列に2つの値を追加します:座標 SLOC とオブジェクト ID。テーブルには既にエントリがある場合があります(複数の場所に物を落とすことができるため)、空のスロットを見つけて埋めるように注意しています。リストの終わりに達すると完了です。ロジックが少し複雑に見える場合、これは連結リストを扱っていることを思い出してください。ここまではすべて順調ですか?私には順調に見えました。「ねえ」と私は自分に言いました。「DESERT-TABLE の内容を State タブに表示すべきだ!そうすれば、人々はオブジェクトが出入りするのを watching できるだろう。」そこで私はそれを設定しました...そしてそれは機能しませんでした。DESERT-TABLE 配列は断固として空のままでした。オブジェクトが消えたり現れたりするのは見えましたが(上のつるはしを見てください!)、どこへ行っていたのでしょうか?固定サイズの配列を見ている C のオタクなら誰でも「配列オーバーフローはどうなる?」と尋ねるでしょう。しかし、DESERT-TABLE にはコメントがあります;「length should be 2*number of takeable objects」確かに、2バイトごとのエントリで100バイトは50個のオブジェクトのスペースとしては十分です。ゲームのすべてのポータブルオブジェクトを積み上げると(思いますに)45個になります。いずれにせよ、何も配列のどこにも格納されていません。ゲームファイルから DESERT-TABLE 関数を逆アセンブルして確認する時が来ました。(深く入ると警告しました...)ルーチン 109bc、5 つのローカル変数 (0000, 001e, 0000, 0000, 0000) 109c7: GET_CHILD ENDLESS-DESERT -> .F [TRUE] 109cb 109cb: JZ .F [TRUE] RTRUE 109ce: GET_SIBLING .F -> .N [TRUE] 109d2 109d2: JE .F, G70 [FALSE] 109d9 109d6: JUMP 10a02 109d9: TEST_ATTR .F, #0f [FALSE] 10a02 109dd: LOADW .TBL, .CNT -> -(SP) 109e1: JZ (SP)+ [FALSE] 109fb 109e4: STOREW .TBL, .CNT, .SLOC 109e9: ADD .CNT, #01 -> -(SP) 109ed: STOREW .TBL, (SP)+, .F 109f2: ADD .CNT, #02 -> .CNT 109f6: REMOVE_OBJ .F 109f8: JUMP 10a02 109fb: ADD .CNT, #02 -> .CNT 109ff: JUMP 109dd 10a02: STORE .F, .N 10a05: JUMP 109cb (1983年版を見ている場合、このルーチンはアドレス 1051a にありますが、それ以外は同じです。) ztools の txd という逆アセンブラツールを使用していますが、変数名が表示されるように出力を編集しました。opcode 名が ZIL 用語と一致しないことに注意してください。txd は1990年代初頭にさかのぼります。ZIL マニュアルがなかったので、独自の opcode 名を作成する必要がありました。最初の行は F の初期値を部屋の最初のオブジェクトに設定します(<FIRST? ,ENDLESS-DESERT>)。2行目は F がゼロかどうかをテストします。これは関数の REPEAT ループの条件です。ループはそこから続きます。何か足りないことに気づきましたか?TBL をデフォルト値の DESERT-TABLE (11129) に初期化するのはどこでしょうか?それは明白な穴のように思えます。Aha、ZIL は言います、私たちはあなたをカバーしました。Z-machine 関数には、ローカル変数(またはオプション引数)を定数に初期化するためのスロットがあります。これはヘッダー行に示されています:ルーチン 109bc、5 つのローカル変数 (0000, 001e, 0000, 0000, 0000) CNT はゼロに初期化され、N も暗黙的に初期化されます。F は定数ではなく計算されるため、スロットから初期化されません。ZIL は関数の先頭に FIRST? (GET_CHILD) 行を生成します。DESERT-TABLE は定数なので... ...しまった。そうではありません。DESERT-TABLE はグローバル変数です。その値は決して変化しません。ゲーム全体を通して 11129 になりますが、ZIL はそれを知りません!TBL は定数値 30 (16 進数 1e) に初期化されます。これは DESER のインデックス番号です