th06: 東方紅魔郷のバイト単位デコンパイル
先に言っておくと これは俺が書いたものじゃない fork だ。th06 は happyhavoc(GensokyoClub)の連中がやってる東方紅魔郷のリバースエンジニアリングプロジェクト(東方紅魔郷 ~ the Embodiment of Scarlet Devil 1.02h)で 目標は ZUN が当時書いた C++ ソースを 1 ビット単位で再構築することだ。俺がこれを持ってきたのは単純に 低レベル・逆向き・古いゲームという 自分の興味 3 つの交差点にちょうどハマったから。
そそられるのは match decompilation という手法。ロジックを理解して大体同じものを書き直す 話じゃない。お前の C++ が当時のコンパイラを通して吐く機械語が オリジナルの 東方紅魔郷.exe とバイト単位で一致することを要求される。つまりロジックの復元だけじゃなく コンパイラの癖まで再現しないといけない。変数をスタックに積む順番 レジスタ割り当ての癖まで。ゲームを単に crack するよりずっと面白いし 骨の髄まで理解することを強制される。
一番刺さるのはツールチェーンの偏執っぷり。ビルド全体が Visual Studio 2002 と DirectX 8.0 の組み合わせで しかも Web Archive から拾ってきた古いバージョンだ。理由は明快で ZUN が当時この一式を使っていたから バイトを揃えたいなら同じ刀を使うしかない。Linux/macOS では wine 経由で動かし macOS では CL.EXE のヒープ問題を回避するために CrossOver が推奨されている。python scripts/create_devenv.py でコンパイラ・ライブラリ・ツールを全部引っ張ってきて設定し その後 python3 ./scripts/build.py で build.ninja を生成して ninja に渡してコンパイルさせる。
逆向きの側は Ghidra を使う。RE の成果は th06-re リポジトリに置かれていて 毎晩彼らの Ghidra Server から同期される。作業は objdiff に頼る。これがオリジナルの exe を object 単位にバラしてくれて 関数を 1 つ書き直すたびに その場でオリジナルとの差分を教えてくれる。真っ赤なら合ってない 全部緑になって初めて OK だ。作業ネタを探すなら config/stubbed.csv を見る。一時的にスタブ化された関数が並んでいるので 1 つ選んで宣言を対応する cpp に写し Ghidra のデコンパイル出力を見ながら潰していく。このフィードバックループの設計は本当に賢い。
典型的なディテールが 1 つ。現代の MSVC7 に当時の変数配置を再現させるために プロジェクトには専用の var_order pragma が移植されている。こういう筋金入りの互換性のために作られた小さな車輪こそ match decomp の魅力だ。さらに portable ブランチもあって ゲームを Linux と現代の Windows に移植して動かす。デコンパイルの副産物が実際に遊べる移植版になるわけで この筋も追う価値がある。
今のところ再構築コードは提出していない 逆向きの練習として読んでいるだけだ。ここに記録として置いておく いつか手が疼いたら stubbed 関数を 1 つ選んで揃えてみる。
- リポジトリ:https://github.com/dwgx/th06(オリジナルのプロジェクトは happyhavoc/th06 を参照)