jamchamb’s blog
昨年の夏、ゲームキューブの「どうぶつの森」の改造の可能性を探るべく、リバースエンジニアリングを開始したのですが、その際に「どうぶつの森」の開発者モードを「開発者モード」に変更しました。 この投稿では、ゲーム内にまだ残されている開発者のデバッグ機能と、それを解除するために使用できるチートコンボをどのように発見したかを探ります。
new_Debug_mode
いくつか残っているデバッグ シンボルを見ているうちに、関数や変数名に “debug” という単語が含まれていることに気づき、ゲームにどんなデバッグ機能が残っているか見てみたいと思ったのです。
最初に見たのは new_Debug_mode
という関数です。これは entry
関数から呼び出され、Nintendotrademark の画面が終わるとすぐに実行されます。
この関数がentry
で呼ばれた後、mainproc
が呼ばれる直前に、割り当てられた構造体のオフセット0xD4
に値0が設定されます。
値が 0 でないときに何が起こるかを見るために、li r0, 0
命令の80407C8C
からli r0, 1
にパッチを適用しました。 命令li r0, 0
の生バイトは38 00 00 00
で、割り当てられた値は命令の最後にあるので、これを38 00 00 01
に変更すればli r0, 1
が得られます。 より確実に命令を組み立てるには、kstool
:
のようにします。このパッチはDolphinエミュレータで、ゲームのプロパティの「パッチ」タブで、このように入力することで適用可能です。
この値を 1 に設定すると、画面の下に面白いグラフが表示されました。 (後でグラフを描画する関数の名前を調べたところ、実際に CPU とメモリの使用量のメトリクスを表示していることがわかりました。) これはきれいですが、特に便利ではありません。
Zuru mode
私は、デバッグ関連の他のリファレンスを調べ始め、「Zuru モード」というものが何回か出てくるのを見ました。 デバッグ機能を持つコードブロックへの分岐は、しばしば zurumode_flag
という変数をチェックしました。
上の写真の 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” だけでした。
マルチバイト文字エンコーディングを使用した日本語テキストかもしれないと思い、文字エンコーディング検出ツールにこの文字列をかけてみると、それはシフト JIS であることが判明しました。 翻訳された文字列は、「zurumode_flagが%dから%dに変更されました」という意味だけです。 バイナリや文字列テーブルには、このエンコードを使用する文字列がもっとたくさんあるので、Shift-JISの使用について知ることは、あまり新しい情報を提供しないのですが、そうです。
zurumode_callback
- Calls
zerumode_check_keycheck
- Check some bits in
osAppNMIBuffer
- Calls
zurumode_update
Prints value of zurumode_flag
somewhere
zerumode_check_keycheck
was not showing before because of the differentspelling.以前は表示されなかったが、スペルが違うため表示された?7644>
名前のない値に対してさらに多くのビット操作を行う巨大で複雑な関数。この時点で、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_status
や event_status
などいくつかの変数を見かけました。 これらは、デバッグ モードでパフォーマンス グラフよりも有用なものを表示させたいので、興味深いものです。 幸運なことに、これらのデータ項目から、debug_print_flg
をチェックする巨大なコードの一部への相互参照がありました。 ブレークポイントはヒットしませんでした。
その理由を確認します。この関数は game_debug_draw_last
から呼び出されます。 条件付きで呼び出す前にチェックされる値は何だと思いますか? zurumode_flag
.
そのチェック(80404E18
)にブレークポイントを設定したら、すぐにブレークしてくれました。 zurumode_flag
の値は 0 でしたので、通常はこの関数を呼び出すのをスキップします。 Dolphin デバッガーでは、ゲームを一時停止し、命令を右クリックして、[Insert nop] をクリックすることで実行できます。 それから私は関数内部で何が起こっているかをチェックし、803981a8
で興味深いもののすべてを通過する短絡的な別の分岐文を発見しました。 7644>
この関数には、8039816C
(私は zzz_DebugDrawPrint
と呼びました) でさらに多くの面白そうなコードがありましたが、どれも呼び出されてはいませんでした。 この関数のグラフ表示を見てみると、関数全体を通してブロックをスキップする一連の分岐ステートメントがあることがわかります。
これらの分岐ステートメントの多くを NOP して、異なるものが画面に出力されるのを確認し始めました:
次の質問は、コードを修正せずにこれらのデバッグ機能をどのようにして有効にするかです。また、このデバッグ描画機能で作られたいくつかの分岐文で、zurumode_flag
が再び表示されます。zurumode_flag
は 0 と比較されないときに特に 2 と比較されるので、zurumode_update
で常に 2 に設定されるように別のパッチを追加しました。ゲームを再起動すると、画面右上にこの「msg.no」のメッセージが表示されました。
687は一番最後に表示したメッセージのエントリIDです。 初期に作ったsimpletable viewerで確認しましたが、ROMハック用に作ったフルGUIの文字列テーブルエディタでも確認できます。 エディタでのメッセージの表示は次のとおりです。
この時点で、ズルモードの把握はもはや避けられないことが明らかになり、ゲームのデバッグ機能に直接結びついていました。
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 に置き換えると、興味深いことが起こります。
右上隅に再び「D」の文字が表示され(今回は黄色ではなく緑)、いくつかの構築情報も表示されます。
起動時に 0x4(zuruKeyCheck)
を常に 1 にするパッチ:
8040ef9c 38c00001
これがズルモードを初期化する正しい方法と思われます。 その後、特定のデバッグ情報を表示させるために必要な別のアクションがあるかもしれません。
おそらく、zurumode_update
とzurumode_callback
だと思われます。
zurumode_update
zurumode_update
はまず zurumode_init
から呼ばれ、その後 zurumode_callback
から繰り返し呼ばれます。
このフラグは 0x3C(osAppNMIBuffer)
の最後のビットを再びチェックし、その値に基づいて zurumode_flag
を更新します。
そうでなければ、r5
が0x3c(osAppNMIBuffer)
のフル値である状態で次の命令が実行されます:
これは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)
にそれを書き戻す。
擬似コードでは次のようになります。
その後、ビット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つあります。
-
0x3C(osAppNMIBuffer)
でビット26がセットされている場合 -
0x3C(osAppNMIBuffer)
でビット25がセットされていて、ポート2にコントローラが接続されている場合 -
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
- 8040ed74:
r5
は0xB
でなければならない - 8040ed60:
r0
must be0x1000
- 8040ebe8:
r5
must be0xA
- 8040ebe4:
r5
must be less than0x5B
- 8040eba4:
r5
must be greater than0x7
- 8040eb94:
r6
は 1 - 8040eb5c:
r0
は 0 - 8040eb74: Port 2 button values must have changed
8040ed50
8040ed00
8040ed38
-
8040ed34
ですね。r0
must be0x4000
(Bボタンが押された) -
8040ebe0
:r5
は0x5b
-
8040eba4
でなければならない。r5
must be greater than0x7
- same as before from here…
-
8040ecfc
となります。r0
は0xC000
でなければならない(AとBが押された) -
8040ebf8
でなければならない。r5
は >= 9 -
8040ebf0
でなければならない。r5
は 10 -
8040ebe4
よりも小さくなければならない。r5
は0x5b
-
8040eba4
よりも小さくなければならない。r5
must be greater than0x7
- same as before from here…
-
8040ed4c
となります。r0
は0x8000
(Aが押された) -
8040ec04
でなければならない。r5
は0x5d
-
8040ebe4
よりも小さくなければならない。r5
は0x5b
- よりも大きくなければならない
8040eba4
:r5
must be greater than0x7
- same as before from here…
-
r5
is set to 9 when RIGHT is pressed at8040ece8
. -
r5
は8040eccc
でCスティック右を押すと8に設定される. -
r5
は8040ecb0
でCスティック左を押すと7に設定される. -
r5
は8040ec98
でLEFTを押すと6になる. -
r5
は8040ec7c
でDOWNを押すと5(とr6=1)に設定される. - は
8040ec7c
でCスティック右を押すと5に設定される. -
r5
は8040ec64
でCスティックアップを押すと4に設定されます。 -
r5
は8040ec48
でCスティックダウンを押すと3に設定されます。 -
r5
は8040ec30
でUPを押すと2に設定されます。 -
r5
はでZを押すと1(r6
を1に設定)に設定されます。
。 ビット25、26、28は特に興味深いことを覚えておいてください:25と26は、ズルモードが有効であるかどうかを決定し、ビット28は、フラグのレベル(1または2)を決定します。
Bit 26
最初に、800062e0
で0x3c
のバッファの値に対してori r0, r0, 0x20
命令があることを思い出してください。 これはビット26をセットすることになり、常にズルズルモードが有効になります。
このビットをセットするためには、DVDGetCurrentDiskID
から返ってくる8バイト目が0x99
でなければなりません。このIDはゲームディスクイメージの一番最初にあり、メモリ内の80000000
でロードアップされます。 通常の小売リリースの場合、ID は次のようになります:
47 41 46 45 30 31 00 00 GAFE01..
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>
これは、ビット28で使用されるのと同じチェックがあることがわかりました:バージョンが0x90
より大きいか等しいことです。
ただし、バージョンが0x90
と0x98
の間の場合、zuruモードはすぐには有効になりません。zurumode_callback
で行ったチェックを思い出すと、ビット25が設定され、padmgr_isConnectedController(1)
が0以外を返した場合にのみ有効になります。 タイトル画面にはDの文字とビルド情報が表示され、2つ目のコントローラのボタンを押すとデバッグ表示を操作できます!
ボタンによっては、ゲームの速度を上げるなど、表示の変更以外のこともできますよ!
zerucheck_key_check
最後の謎は0x4(zuruKeyCheck)
ですね。 7644>
ドルフィンデバッガを使って、この関数がチェックする値は、2番目のコントローラのボタン押下に対応するビットのセットであると特定できました。 ボタン押下履歴は、0x2(zuruKeyCheck)
の16ビット値に格納されています。 コントローラが接続されていない場合、値は0x7638
です。
コントローラ2のボタン押下に関するフラグを含む2バイトがロードされ、zerucheck_key_check
の先頭付近が更新されます。 新しい値は、padmgr_HandleRetraceMsg
がコールバック関数を呼び出すときに、レジスタ r4
で渡されます。
zerucheck_key_check
の終わり近くで、実は 0x4(zuruKeyCheck)
が更新される別の場所があります。 この関数はベースアドレスとしてr3
を使用しているため、クロスリファレンスのリストには表示されず、r3
が何であるかは、この関数が呼ばれようとしているときに設定されているものを見ることによってのみ知ることができます。 これは同じ場所からロードされ、その直前に1とXORされています。 これでバイトの値(実際には最後のビットだけ)が0と1の間で切り替わるはずです(0なら1とのXORの結果は1になります)。 これを見るには XOR の真理値表を調べてください。)
以前はメモリの値を見ていてこの動作に気づきませんでしたが、デバッガでこの命令をブレークして何が起こっているか見てみます。 元の値は8040ed7c
.
コントローラのボタンを一切触らず、タイトル画面ではこのブレークポイントにヒットしません。 このコードブロックに到達するには、r5
の値が、その前にある分岐命令(8040ed74
)の前に0xb
あることが必要です。 7644>
なお、r5
を0xB
にするブロックに到達するには、r0
が直前に0x1000
と等しくなければならない。 関数の先頭までブロックを辿っていくと、このブロックに到達するために必要な制約が見えてくる:
でなければならない
であってはなりません
ここで、古いボタンの値をロードし、新しい値を保存するポイントに到達しました。 その後、新しい値と古い値に適用されるいくつかの演算があります。
old_vals = old_vals XOR new_valsold_vals = old_vals AND new_vals
XOR 演算は、2 つの値間で変更されたビットをすべてマークします。 次にAND演算で新しい入力をマスクし、現在セットされていないビットをアンセットします。 r0
の結果は、新しい値の新しいビット(ボタン押下)のセットである。
r0
が0x1000
であるためには、16個のボタンのトレースビットのうち4番目がちょうど変化したはずです。XOR/AND演算の後にブレークポイントを設定することにより、どのボタンがこれを引き起こすかを特定できます:それはSTART(開始)ボタンです。 r5
とr6
はキーチェック機能の最初に0x0(zuruKeyCheck)
からロードされ、0x4(zuruKeyCheck)
をトグルするコードブロックを実行しないときに終わり近くで更新されます。
r5
が 0xA
に設定される直前にはいくつかの場所があります:
8040ed38
r5
must start at 0x5b
8040ed00
r5
must start at 9
8040ed50
r5
must start at 0x5c
ボタンを押す間に何らかの状態があるらしく、STARTで終了して一定のボタンコンボが入る必要があるようです。
r5
を9に設定するコードパスをたどっていくと、あるパターンが浮かび上がってきました。 r5
はインクリメント値で、r0
で正しいボタン押下値を見つけたときに増加するか、0にリセットされる。0x0
と0xB
の間の値でない奇妙なケースは、AとBを同時に押すなど複数のボタンを扱うときに発生する。
Continuing with the different code paths:
現在のシーケンスは以下の通り:
Z, UP, C-DOWN, C-UP, DOWN, LEFT, C-LEFT, C-RIGHT, RIGHT, A+B, START
Zチェックの前にもう一つ条件があります:新しく押したボタンをZにして、現在のフラグは 0x2030
でなければなりません:左右バンパーも押していなければいけません(値は 0x10
と 0x20
). また、UP/DOWN/LEFT/RIGHTはアナログスティックではなく、D-padのボタンです。
チートコード
フルコンボは。
- L+Rバンパーを押したままZ
- D-UP
- C-DOWN
- D-UP
- C-DOWN
- D-」。LEFT
- C-LEFT
- C-RIGHT
- D-RIGHT
- A+B
- START
うまくいった!
うまくいきました。 2番目のポートにコントローラーを取り付け、コードを入力すると、デバッグ用のディスプレイが表示されます。
このコンボは、ゲームのバージョン番号にパッチを適用しなくても動作します。
zurumode_callback
の「ZURU %d/%d」メッセージは、ディスクIDがすでに0x99
のときにこの組み合わせを入力すると、その状態を印刷するために使用します(おそらくチートコード自体のデバッグ用と思われます)。 最初の数字は現在の位置で、r5
と一致する。
ほとんどの表示は、画面上に何の表示があるか説明されていないので、何をやっているかを知るには、それらを処理する関数を見つける必要があります。 たとえば、画面上部に表示される青と赤の長い円盤は、さまざまなクエストの状態を表示するためのプレースホルダーです。
Z を押したときに表示される黒い画面はデバッグメッセージを表示するコンソールですが、 特にメモリ割り当てやヒープエラー、その他の悪い例外などの低レベルのもののために表示されます。 fault_callback_scroll
の動作は、システムが再起動する前にこれらのエラーを表示するためのものである可能性を示唆しています。 私はこれらのエラーのいずれも引き起こしませんでしたが、いくつかの NOP を含むいくつかのゴミ文字を表示させることができました。 これは、後でカスタムデバッグメッセージを印刷するのに非常に便利だと思います:
これをすべて行った後、バージョン ID を 0x99
にパッチしてデバッグモードを得ることはすでに知られていると分かりました。 https://tcrf.net/Animal_Crossing#Debug_Mode. (各種表示の内容や、ポート3のコントローラを使ってできることなど、良いメモもあります。)私の知る限り、チートの組み合わせはまだ公開されていませんが。
今回の投稿は以上です。 デバッグマップ画面やファミコンエミュレータの選択画面、パッチを使わずにそれらを有効にする方法など、まだまだ開拓したい開発機能があります。
また、MOD製作のためにダイアログやイベント、クエストシステムを逆にした場合の書き込みを掲載する予定です。