jamchamb’s blog

昨年の夏、ゲームキューブの「どうぶつの森」の改造の可能性を探るべく、リバースエンジニアリングを開始したのですが、その際に「どうぶつの森」の開発者モードを「開発者モード」に変更しました。 この投稿では、ゲーム内にまだ残されている開発者のデバッグ機能と、それを解除するために使用できるチートコンボをどのように発見したかを探ります。

new_Debug_mode

いくつか残っているデバッグ シンボルを見ているうちに、関数や変数名に “debug” という単語が含まれていることに気づき、ゲームにどんなデバッグ機能が残っているか見てみたいと思ったのです。

最初に見たのは new_Debug_mode という関数です。これは entry 関数から呼び出され、Nintendotrademark の画面が終わるとすぐに実行されます。

この関数がentryで呼ばれた後、mainprocが呼ばれる直前に、割り当てられた構造体のオフセット0xD4に値0が設定されます。

Disassemble of the entry function

値が 0 でないときに何が起こるかを見るために、li r0, 0命令の80407C8Cからli r0, 1にパッチを適用しました。 命令li r0, 0の生バイトは38 00 00 00で、割り当てられた値は命令の最後にあるので、これを38 00 00 01に変更すればli r0, 1が得られます。 より確実に命令を組み立てるには、kstool:

$ kstool ppc32be "li 0, 1"li 0, 1 = 

のようにします。このパッチはDolphinエミュレータで、ゲームのプロパティの「パッチ」タブで、このように入力することで適用可能です。

Debug performance meter

この値を 1 に設定すると、画面の下に面白いグラフが表示されました。 (後でグラフを描画する関数の名前を調べたところ、実際に CPU とメモリの使用量のメトリクスを表示していることがわかりました。) これはきれいですが、特に便利ではありません。

Zuru mode

私は、デバッグ関連の他のリファレンスを調べ始め、「Zuru モード」というものが何回か出てくるのを見ました。 デバッグ機能を持つコードブロックへの分岐は、しばしば zurumode_flag という変数をチェックしました。

game_move_first function

上の写真の game_move_first 関数の中で、zurumode_flag がゼロ以外の場合のみ zzz_LotsOfDebug (私が作った名前) を呼び出します。

この値に関連する関数を探すと、次のようになります。

  • zurumode_init
  • zurumode_callback
  • zurumode_update
  • zurumode_cleanup

これらは一見するとよくわかりませんが、osAppNMIBuffer という変数のオフセットでいろいろといじっているようですね。これらの関数が何をするのか、まず見てみましょう。

zurumode_init

  • Set zurumode_flag to 0
  • Check some bit in osAppNMIBuffer
  • Store a pointer to the padmgr structure
  • Call zurumode_update in the function in the in the function in in in the function in the in the in the in the in the

zurumode_update

  • osAppNMIBufferのいくつかのビットをチェックする
  • これらのビットに基づきzurumode_flagを条件付きで更新する
  • OSコンソールにフォーマット文字列をプリントアウトする。

この種のものは通常、コードにコンテキストを与えるために有用ですが、文字列には印刷不可能な文字が多数含まれていました。 認識可能なテキストは “zurumode_flag” と “%d” だけでした。

zuru mode format string

マルチバイト文字エンコーディングを使用した日本語テキストかもしれないと思い、文字エンコーディング検出ツールにこの文字列をかけてみると、それはシフト JIS であることが判明しました。 翻訳された文字列は、「zurumode_flagが%dから%dに変更されました」という意味だけです。 バイナリや文字列テーブルには、このエンコードを使用する文字列がもっとたくさんあるので、Shift-JISの使用について知ることは、あまり新しい情報を提供しないのですが、そうです。

zurumode_callback

  • Calls zerumode_check_keycheck
  • Check some bits in osAppNMIBuffer
  • Prints value of zurumode_flag somewhere

  • Calls zurumode_update

zerumode_check_keycheck was not showing before because of the differentspelling.以前は表示されなかったが、スペルが違うため表示された?7644>

zerumode_check_keycheck

名前のない値に対してさらに多くのビット操作を行う巨大で複雑な関数。この時点で、zuru モードの意味がよくわからなかったので、手を引いて他のデバッグ関連の関数と変数を探すことにしました。 また、ここでいう「キーチェック」の意味もよくわからなかった。 7644>

Back to debug

この頃、IDA にデバッグシンボルを読み込む方法に問題があることに気づきました。 ゲーム ディスクの foresta.map ファイルには、関数や変数のアドレスと名前がたくさん含まれています。 各セクションのアドレスがゼロから始まることに最初は気づかなかったので、ファイルの各行の名前エントリを追加する簡単なスクリプトをセットアップしました。

プログラムの異なるセクション (.text.rodata.data および .bss) のシンボル マップ読み込みを修正する新しい IDA スクリプトをいくつかセットアップしました。 .text セクションはすべての関数がある場所なので、今回は名前を設定するときに各アドレスで関数を自動的に検出するようにスクリプトを設定しました。

データセクションでは、各バイナリー オブジェクト (m_debug.o など、m_debug という何かのコンパイル コードとなる) に対してセグメントを作成し、各データに対してスペースと名前を設定するように設定しました。この結果、以前よりもはるかに多くの情報を得ることができましたが、各データオブジェクトを単純なバイト配列に設定したため、各データのデータ型を手動で定義する必要がありました。 (今にして思えば、少なくとも 4 バイトの倍数のサイズのデータは 32 ビット整数を含むと仮定したほうがよかったと思います。)

m_debug_mode.o の新しい .bss セグメントを見ているときに、quest_draw_statusevent_status などいくつかの変数を見かけました。 これらは、デバッグ モードでパフォーマンス グラフよりも有用なものを表示させたいので、興味深いものです。 幸運なことに、これらのデータ項目から、debug_print_flg をチェックする巨大なコードの一部への相互参照がありました。 ブレークポイントはヒットしませんでした。

その理由を確認します。この関数は game_debug_draw_last から呼び出されます。 条件付きで呼び出す前にチェックされる値は何だと思いますか? zurumode_flag.

zurumode_flag check

そのチェック(80404E18)にブレークポイントを設定したら、すぐにブレークしてくれました。 zurumode_flag の値は 0 でしたので、通常はこの関数を呼び出すのをスキップします。 Dolphin デバッガーでは、ゲームを一時停止し、命令を右クリックして、[Insert nop] をクリックすることで実行できます。 それから私は関数内部で何が起こっているかをチェックし、803981a8で興味深いもののすべてを通過する短絡的な別の分岐文を発見しました。 7644>

Debug mode letter D

この関数には、8039816C (私は zzz_DebugDrawPrint と呼びました) でさらに多くの面白そうなコードがありましたが、どれも呼び出されてはいませんでした。 この関数のグラフ表示を見てみると、関数全体を通してブロックをスキップする一連の分岐ステートメントがあることがわかります。

Branches in zzz_DebugDrawPrint

これらの分岐ステートメントの多くを NOP して、異なるものが画面に出力されるのを確認し始めました:

More debug stuff getting printed

次の質問は、コードを修正せずにこれらのデバッグ機能をどのようにして有効にするかです。また、このデバッグ描画機能で作られたいくつかの分岐文で、zurumode_flag が再び表示されます。zurumode_flag は 0 と比較されないときに特に 2 と比較されるので、zurumode_update で常に 2 に設定されるように別のパッチを追加しました。ゲームを再起動すると、画面右上にこの「msg.no」のメッセージが表示されました。

message number display

687は一番最後に表示したメッセージのエントリIDです。 初期に作ったsimpletable viewerで確認しましたが、ROMハック用に作ったフルGUIの文字列テーブルエディタでも確認できます。 エディタでのメッセージの表示は次のとおりです。

Message 687 in the string table editor

この時点で、ズルモードの把握はもはや避けられないことが明らかになり、ゲームのデバッグ機能に直接結びついていました。

Zuru mode revisited

zurumode_initに戻り、いくつかのものを初期化します:

  • 0xC(padmgr_class)zurumode_callbackのアドレスに設定します
  • 0x10(padmgr_class)padmgr_class自身のアドレスに設定します
  • 0x4(zuruKeyCheck)0x3C(osAppNMIBuffer)からロードしたワードの最後のビットに設定します。

padmgrの意味を調べてみたら、「ゲームパッドマネージャ」の略だった。 これは、ズール モードをアクティブにするためにゲームパッドで入力する特別なキー (ボタン) の組み合わせがあるか、またはそれをアクティブにする信号を送信するために使用できる特別なデバッグ デバイスまたは開発コンソール機能がある可能性があることを示唆しています。

zurumode_init は、ゲームが最初にロードされたときのみ実行されます (リセット ボタンを押してもトリガーされません)。

0x4(zuruKeyCheck) が設定されている 8040efa4 でブレークポイントを設定すると、起動時にキーを押さずに 0 に設定されていることが分かります。 これを 1 に置き換えると、興味深いことが起こります。

Title screen with zuru mode

右上隅に再び「D」の文字が表示され(今回は黄色ではなく緑)、いくつかの構築情報も表示されます。

起動時に 0x4(zuruKeyCheck) を常に 1 にするパッチ:

8040ef9c 38c00001

これがズルモードを初期化する正しい方法と思われます。 その後、特定のデバッグ情報を表示させるために必要な別のアクションがあるかもしれません。

おそらく、zurumode_updatezurumode_callbackだと思われます。

zurumode_update

zurumode_update はまず zurumode_init から呼ばれ、その後 zurumode_callback から繰り返し呼ばれます。

このフラグは 0x3C(osAppNMIBuffer) の最後のビットを再びチェックし、その値に基づいて zurumode_flag を更新します。

そうでなければ、r50x3c(osAppNMIBuffer)のフル値である状態で次の命令が実行されます:

extrwi r3, r5, 1, 28

これはr5から28番目のビットを取り出してr3に保存しています。次に0x3c(osAppNMIBuffer)の28ビット目と最後のビットが何番目に設定されているかによって、

zurumode_flagと前の結果が比較される。 もし何も変わらなかったら、この関数は終了し、フラグの現在の値を返します。

日本語のメッセージが表示されます。「zurumode_flag has been changed from %d to %d」これは、前述のメッセージです。 フラグがゼロの場合、JC_JUTDbPrint_setVisibleには0が引数として渡され、フラグがゼロでない場合、および0x3C(osAppNMIBuffer)にビット25またはビット31がセットされている場合、setVisible関数には1が引数として渡されます。

これが zuru モードを有効にする最初のキーです。デバッグ表示を表示し、zurumode_flag をゼロ以外の値に設定するためには、0x3C(osAppNMIBuffer) の最後のビットを 1 に設定する必要があります。 ドルフィンデバッガでブレークポイントを設定し、コールスタックを見ると、確かに padmgr_HandleRetraceMsg から呼び出されています。

これが最初に行うことのひとつは zerucheck_key_check を実行することです。 複雑ですが、全体として zuruKeyCheck の値を読み取り、更新しているようです。

次に、0x3c(osAppNMIBuffer)のいくつかのビットを再度チェックします。 ビット26がセットされているか、ビット25がセットされていてpadmgr_isConnectedController(1)が0以外を返していれば、0x3c(osAppNMIBuffer)の最後のビットが1にセットされる!

これらのビットが両方ともセットされていないか、ビット25は少なくともセットされていてpadmgr_isConnectedController(1)が0を返していれば、0x4(zuruKeyCheck)でバイトが0であるかチェックする。 もしそうなら、元の値の最後のビットをゼロにして0x3c(osAppNMIBuffer)にそれを書き戻す。

擬似コードでは次のようになります。

x = osAppNMIBufferif (BIT(x, 26) || (BIT(x, 25) && isConnectedController(1)) || zuruKeyCheck != 0) { osAppNMIBuffer = x | 1 // set last bit} else { osAppNMIBuffer = x & ~1 // clear last bit}

その後、ビット26がセットされていない場合、zurumode_updateを呼び出して終了します。

もしセットされていれば、0x4(zuruKeyCheck)がゼロでなければ、フォーマットストリングをロードして、プリントアウトするように見えます。 「

Recap

ここで何が起こるか:

padmgr_HandleRetraceMsg call the zurumode_callback.My guess that “handle retrace message” means it just scanned key presseson the controller.私の推測では、このメッセージはコントローラのキーをスキャンしたことを意味しています。 スキャンするたびに、一連の異なるコールバックを呼び出すかもしれません。

zurumode_callbackが実行されると、現在のキー(ボタン)押しをチェックします。

NMIバッファの最後のビットは、そのcurrentvalueの特定のビットと、zuruKeyCheckバイトの1つ(0x4(zuruKeyCheck))の値に基づいて更新されます。

次にzurumode_updateが実行されてそのビットをチェックします。 0ならzuruモードフラグを0にbesetし、1ならビット28がセットされているかどうかに基づいてモードフラグを1または2に更新する。

ズルモードを有効にする方法は3つあります。

  1. 0x3C(osAppNMIBuffer) でビット26がセットされている場合
  2. 0x3C(osAppNMIBuffer) でビット25がセットされていて、ポート2にコントローラが接続されている場合
  3. 0x4(zuruKeyCheck) is not zero

osAppNMIBuffer

osAppNMIBufferとは何か考えてみた。 まず「NMI」で検索してみると、任天堂の文脈で「ノンマスカブルインタラプト」という表現が見つかりました。 その結果、NINTENDO64の開発者向けドキュメントにも、この変数名全体が出てくることがわかりました。

osAppNMIBuffer は 64 バイトのバッファで、コールド リセット時にクリアされます。 NMI によりシステムが再起動した場合、このバッファは変更されません。

基本的に、これはソフト リブートにわたって持続するメモリの小さな断片です。 オリジナルの「どうぶつの森」は、実際には Nintendo 64 でリリースされたので、このようなものがコードに表示されるのは当然です。 ざっと見たところ、OR演算で0x3c(osAppNMIBuffer)の様々なビットがセットされることになる一連のチェックがあることがわかる。

Industaining OR operand values to look out would be:

  • Bit 31: 0x01
  • Bit 30: 0x02
  • Bit 29: 0x04
  • Bit 28: 0x08
  • Bit 27.0
  • Bit 29: 0x02
  • 。 ビット25、26、28は特に興味深いことを覚えておいてください:25と26は、ズルモードが有効であるかどうかを決定し、ビット28は、フラグのレベル(1または2)を決定します。

    Bit 26

    最初に、800062e00x3cのバッファの値に対してori r0, r0, 0x20命令があることを思い出してください。 これはビット26をセットすることになり、常にズルズルモードが有効になります。

    Setting bit 26

    このビットをセットするためには、DVDGetCurrentDiskIDから返ってくる8バイト目が0x99でなければなりません。このIDはゲームディスクイメージの一番最初にあり、メモリ内の80000000でロードアップされます。 通常の小売リリースの場合、ID は次のようになります:

    47 41 46 45 30 31 00 00 GAFE01..

    ID の最後のバイトを 0x99 にパッチすると、ゲーム起動時に次のことが起こります:

    Game version ID 0x99

    そして OS コンソールで次のように出力されます。

    06:43:404 HW\EXI_DeviceIPL.cpp:339 N: ZURUMODE2 ENABLE08:00:288 HW\EXI_DeviceIPL.cpp:339 N: osAppNMIBuffer=0x00000078

    他のパッチはすべて削除でき、画面右上にDの文字も再び表示されますが、他のデバッグ表示は一切有効ではありません。

    Bit 25

    Bit 25は、ポート2コントローラチェックを行う際に使用されるものです。 7644>

    Bit 25 and 28

    これは、ビット28で使用されるのと同じチェックがあることがわかりました:バージョンが0x90より大きいか等しいことです。

    ただし、バージョンが0x900x98の間の場合、zuruモードはすぐには有効になりません。zurumode_callbackで行ったチェックを思い出すと、ビット25が設定され、padmgr_isConnectedController(1)が0以外を返した場合にのみ有効になります。 タイトル画面にはDの文字とビルド情報が表示され、2つ目のコントローラのボタンを押すとデバッグ表示を操作できます!

    ボタンによっては、ゲームの速度を上げるなど、表示の変更以外のこともできますよ!

    zerucheck_key_check

    最後の謎は0x4(zuruKeyCheck)ですね。 7644>

    zerumode_check_keycheck

    ドルフィンデバッガを使って、この関数がチェックする値は、2番目のコントローラのボタン押下に対応するビットのセットであると特定できました。 ボタン押下履歴は、0x2(zuruKeyCheck)の16ビット値に格納されています。 コントローラが接続されていない場合、値は0x7638です。

    コントローラ2のボタン押下に関するフラグを含む2バイトがロードされ、zerucheck_key_checkの先頭付近が更新されます。 新しい値は、padmgr_HandleRetraceMsgがコールバック関数を呼び出すときに、レジスタ r4 で渡されます。

    key check end

    zerucheck_key_check の終わり近くで、実は 0x4(zuruKeyCheck) が更新される別の場所があります。 この関数はベースアドレスとしてr3を使用しているため、クロスリファレンスのリストには表示されず、r3が何であるかは、この関数が呼ばれようとしているときに設定されているものを見ることによってのみ知ることができます。 これは同じ場所からロードされ、その直前に1とXORされています。 これでバイトの値(実際には最後のビットだけ)が0と1の間で切り替わるはずです(0なら1とのXORの結果は1になります)。 これを見るには XOR の真理値表を調べてください。)

    key check end

    以前はメモリの値を見ていてこの動作に気づきませんでしたが、デバッガでこの命令をブレークして何が起こっているか見てみます。 元の値は8040ed7c.

    コントローラのボタンを一切触らず、タイトル画面ではこのブレークポイントにヒットしません。 このコードブロックに到達するには、r5の値が、その前にある分岐命令(8040ed74)の前に0xbあることが必要です。 7644>

    Set r5 to 0xb

    なお、r50xBにするブロックに到達するには、r0が直前に0x1000と等しくなければならない。 関数の先頭までブロックを辿っていくと、このブロックに到達するために必要な制約が見えてくる:

    • 8040ed74: r50xB でなければならない
    • 8040ed60: r0 must be 0x1000
    • 8040ebe8: r5 must be 0xA
    • 8040ebe4: r5 must be less than 0x5B
    • 8040eba4: r5 must be greater than 0x7
    • 8040eb94: r6 は 1
    • でなければならない

    • 8040eb5c: r0 は 0
    • であってはなりません

    • 8040eb74: Port 2 button values must have changed

    コードパスをたどる

    ここで、古いボタンの値をロードし、新しい値を保存するポイントに到達しました。 その後、新しい値と古い値に適用されるいくつかの演算があります。

    old_vals = old_vals XOR new_valsold_vals = old_vals AND new_vals

    XOR 演算は、2 つの値間で変更されたビットをすべてマークします。 次にAND演算で新しい入力をマスクし、現在セットされていないビットをアンセットします。 r0の結果は、新しい値の新しいビット(ボタン押下)のセットである。

    r00x1000であるためには、16個のボタンのトレースビットのうち4番目がちょうど変化したはずです。XOR/AND演算の後にブレークポイントを設定することにより、どのボタンがこれを引き起こすかを特定できます:それはSTART(開始)ボタンです。 r5r6はキーチェック機能の最初に0x0(zuruKeyCheck)からロードされ、0x4(zuruKeyCheck)をトグルするコードブロックを実行しないときに終わり近くで更新されます。

    r50xA に設定される直前にはいくつかの場所があります:

    • 8040ed50
    • 8040ed00
    • 8040ed38
    8040ed38
    • 8040ed34 ですね。 r0 must be 0x4000 (Bボタンが押された)
    • 8040ebe0: r50x5b
    • 8040eba4 でなければならない。 r5 must be greater than 0x7
    • same as before from here…

    r5 must start at 0x5b

    8040ed00
    • 8040ecfcとなります。 r00xC000 でなければならない(AとBが押された)
    • 8040ebf8 でなければならない。 r5 は >= 9
    • 8040ebf0 でなければならない。 r5 は 10
    • 8040ebe4 よりも小さくなければならない。 r50x5b
    • 8040eba4 よりも小さくなければならない。 r5 must be greater than 0x7
    • same as before from here…

    r5 must start at 9

    8040ed50
    • 8040ed4cとなります。 r00x8000 (Aが押された)
    • 8040ec04 でなければならない。 r50x5d
    • 8040ebe4 よりも小さくなければならない。 r50x5b
    • よりも大きくなければならない 8040eba4: r5 must be greater than 0x7
    • same as before from here…

    r5 must start at 0x5c

    ボタンを押す間に何らかの状態があるらしく、STARTで終了して一定のボタンコンボが入る必要があるようです。

    r5を9に設定するコードパスをたどっていくと、あるパターンが浮かび上がってきました。 r5はインクリメント値で、r0で正しいボタン押下値を見つけたときに増加するか、0にリセットされる。0x00xBの間の値でない奇妙なケースは、AとBを同時に押すなど複数のボタンを扱うときに発生する。

    Continuing with the different code paths:

    • r5 is set to 9 when RIGHT is pressed at 8040ece8.
    • r58040eccc でCスティック右を押すと8に設定される.
    • r58040ecb0 でCスティック左を押すと7に設定される.
    • r58040ec98 でLEFTを押すと6になる.
    • r58040ec7c でDOWNを押すと5(とr6=1)に設定される.
    • 8040ec7c でCスティック右を押すと5に設定される.
    • r58040ec64でCスティックアップを押すと4に設定されます。
    • r58040ec48でCスティックダウンを押すと3に設定されます。
    • r58040ec30でUPを押すと2に設定されます。
    • r5はでZを押すと1(r6を1に設定)に設定されます。

    現在のシーケンスは以下の通り:

    Z, UP, C-DOWN, C-UP, DOWN, LEFT, C-LEFT, C-RIGHT, RIGHT, A+B, START

    Zチェックの前にもう一つ条件があります:新しく押したボタンをZにして、現在のフラグは 0x2030 でなければなりません:左右バンパーも押していなければいけません(値は 0x100x20). また、UP/DOWN/LEFT/RIGHTはアナログスティックではなく、D-padのボタンです。

    チートコード

    フルコンボは。

  1. L+Rバンパーを押したままZ
  2. D-UP
  3. C-DOWN
  4. D-UP
  5. C-DOWN
  6. D-」。LEFT
  7. C-LEFT
  8. C-RIGHT
  9. D-RIGHT
  10. A+B
  11. START

うまくいった!

  • C-LEFT
  • C-RIGHT
  • D-RIGHT
  • うまくいきました。 2番目のポートにコントローラーを取り付け、コードを入力すると、デバッグ用のディスプレイが表示されます。

    このコンボは、ゲームのバージョン番号にパッチを適用しなくても動作します。

     実際のゲームキューブでコードを使用する

    zurumode_callbackの「ZURU %d/%d」メッセージは、ディスクIDがすでに0x99のときにこの組み合わせを入力すると、その状態を印刷するために使用します(おそらくチートコード自体のデバッグ用と思われます)。 最初の数字は現在の位置で、r5と一致する。

    ほとんどの表示は、画面上に何の表示があるか説明されていないので、何をやっているかを知るには、それらを処理する関数を見つける必要があります。 たとえば、画面上部に表示される青と赤の長い円盤は、さまざまなクエストの状態を表示するためのプレースホルダーです。

    Z を押したときに表示される黒い画面はデバッグメッセージを表示するコンソールですが、 特にメモリ割り当てやヒープエラー、その他の悪い例外などの低レベルのもののために表示されます。 fault_callback_scroll の動作は、システムが再起動する前にこれらのエラーを表示するためのものである可能性を示唆しています。 私はこれらのエラーのいずれも引き起こしませんでしたが、いくつかの NOP を含むいくつかのゴミ文字を表示させることができました。 これは、後でカスタムデバッグメッセージを印刷するのに非常に便利だと思います:

    JUTConsole garbage characters

    これをすべて行った後、バージョン ID を 0x99 にパッチしてデバッグモードを得ることはすでに知られていると分かりました。 https://tcrf.net/Animal_Crossing#Debug_Mode. (各種表示の内容や、ポート3のコントローラを使ってできることなど、良いメモもあります。)私の知る限り、チートの組み合わせはまだ公開されていませんが。

    今回の投稿は以上です。 デバッグマップ画面やファミコンエミュレータの選択画面、パッチを使わずにそれらを有効にする方法など、まだまだ開拓したい開発機能があります。

    Map Select screen

    また、MOD製作のためにダイアログやイベント、クエストシステムを逆にした場合の書き込みを掲載する予定です。