Contents
Starlightとは
Shadowninja108氏が開発した神ツール。
ただ、公式レポジトリのは説明が全くなくてわかりにくいので個人的に説明を付け加えたフォーク版を公開しておきます。
これで誰でも確実にStarlightの開発環境が整えられるはずなので頑張るのだ。
Starlightの仕組み
Starlightの大雑把な仕組みとしてはSplatoon2の実行ファイル(以後mainと呼ぶ)のgsys::SystemTask::invokeDrawTV
をHookしてmain.cppで定義した独自の関数を呼びだすことです。
本来、gsys::SystemTask::invokeDrawTV
は0x17BC78Cというアドレスから命令が実行されていきます。
その途中で製品版か開発版かのチェックを行う命令が0x17BCA10にあり、製品版ではこのチェックで引っかかって命令が最後まで実行されないままこの関数を抜けてしまいます。
Starlightではまずそのチェックを外すために0x17BCA10の命令をNOP命令で上書きして無効化します。それを行うのがCodehook.slpatchで、まあこれはARM命令が直接書けるIPSwitchと思ってもらえれば良いでしょう。
以下にIPSwitch形式とCodehook.slpatch形式の違いを載せておきます。どちらも同じ効果が得られます。
// Codehook.slpatch [version=310, target=main] gsys::SystemTask::invokeDrawTV_+284 NOP // IPSwitch @nsobid-034C8FA7A63B7A87F96F408B2AEFFF6C @flag offset_shift 0x100 // Disable Checking @enabled 017BCA10 1F2003D5 // NOP
Codehook.slpatch
Codehook.slpatchはmainの本来の命令を上書きしてStarlightにデータを渡す役割を担うパッチです。
先程も述べたように、原理自体はIPSwitchと全く同じなのでIPSwitchでも代用は可能なのですが、いちいち機械語に翻訳するよりも直接ARMで書いたほうが可読性もいいのでこちらを推奨しています。
[version=310, target=main] gsys::SystemTask::invokeDrawTV_+284 NOP // enable display debug stuff (which is used for hook) gsys::SystemTask::invokeDrawTV_+390: MOV X1, X0 MOV X0, X25 BL renderEntrypoint B #0x2B8 enl::NinInetMatchingManagerPlatformImpl::internetInitialize+BC BL nnSocketInitHook
では、Codehook.slpatchがどんな命令に上書きしているかということなのですが「表示したいデータのインスタンスのポインタをrenderEntrypointに渡している」のです。
ここで自分は結構躓いたので、わかりやすく解説していこうと思います。
インスタンスとポインタ
さて、先程Codehook.slpatchが表示したいデータのインスタンスのポインタを渡しているということは述べました。
プログラミング初心者の方だと思うかもしれません、「直接データを渡したほうが早くない?」と。
ところがよく考えてみるとこの「ポインタを渡す」が「直接データを渡す」ということよりも優れていることがわかるのです。
ポインタ渡しとデータ渡し
データ渡しというのは、直接データを関数に渡すコーディングを指します。
例えば、あるお兄さんがアパートに配達に向かうとします。アパートには四人の人が住んでいて、在宅しているかどうかはわかりません。
もし、値渡ししかできないのであればお兄さんはこの四人(p1, p2, p3, p4)の各要素にアクセスしてenabledがtrueなら在宅、falseなら不在(これは現実世界でいうところのチャイムを鳴らすのと同義)を確認しなければいけません。
つまり、getEnabledApartment(Person)
のような関数を最低でも四回呼び出す必要があります。
ところが、ポインタ渡しではこのアパートの住所(先頭アドレス)をまずお兄さんに教えます。
するとお兄さんは「その住所のアパートなら構造(全員の電話番号)を知っている」と答え、実際に訪問しなくても各住人がどのような状態にあるかを知ることができるのです。
要するに毎回住人のデータにアクセスしなくても、アパートの住所を教えるだけでapartment->p1.enabled
のように書くだけで全データにアクセスできるのです。
もちろんこれは与えられたアパートの構造(人が何人いるか、どんなデータがあるか)を事前に知っておく必要があります。
この例え話超下手くそだからそのうち別のやつに変える!!
EventGeyserの場合
カンケツセンの挙動を司るクラスであるEventGeyserは以下のような構造を持つことが知られています。
先頭アドレスから0x398の領域まではCmn::Actor
とLp::Sys::stateMachine
というデータが入っているのですが、StarlightにLp::Sys::stateMachine
のヘッダーファイルがないためこれらを扱うことができません。
なのでここではなんか適当なデータでその部分を埋めておきます。それが_BYTE gap[0x398]
で、今後結構な頻度でお世話になる書き方です。
更にsead::Random
というクラスのデータを二つ持っており、これらはmSeed1, mSeed2, mSeed3, mSeed4という四つの数から線形合同法を使って疑似乱数を生成します。
そして最後にsead::PtrArrayImpl
という配列のクラスをもっており、ここにカンケツセンの位置などの配列が入っているというわけです。
ぶっちゃけるとぼくもクラスの概念については理解が不十分なので間違っていたら教えて下さい。
main.cpp+header
さて、Codehook.slpatchがインスタンスのポインタを渡すこと。インスタンスの構造についてあらかじめ知っていれば先頭アドレス(ポインタ)がわかれば全データにアクセスできることについては述べました。
では、それをmain.cppで受け取ってどのようにデータを扱うかを説明します。
// Before void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter) { } // After static Game::Coop::EventGeyser *mEventGeyser; void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter, Game::Coop::EventGeyser *eventGeyser) { mEventGeyser = eventGeyser; }
まず、表示したいデータを読み込む必要があるのでrenderEntrypointの引数に読み込みたいインスタンスのポインタを渡す必要があります。
また、受け取ったデータを永続的に保持するためstatic変数を使ってポインタ変数を宣言する必要もあります。
といっても、実はやるのはたったこれだけで、一度ポインタをコピーできたらあとは好き勝手に何でもできます。
例えば、疑似乱数を設定しているシードを調べたければ以下のコードで実装できます。
static Game::Coop::EventGeyser *mEventGeyser; void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter, Game::Coop::EventGeyser *eventGeyser) { mEventGeyser = eventGeyser; if (mEventGeyser != NULL) { textWriter->printf("%X\n", mEventGeyser->random.mSeed1); } }
ポインタがNULLのときにアクセスするとゲームがクラッシュするので必ずNULLチェックはすること!
ここまでの流れ
- Codehook.slpatchはmainを上書きして、インスタンスのポインタをStarlightに渡す
- インスタンスのポインタがあれば、インスタンスの構造がわかっていればStarlightは全データにアクセスできる
- main.cppの書き方は上のコードを参考にする
じゃあ「インスタンスの構造ってどうやって調べるのか?」ということに当然なるわけです。
クラスの構造を決めよう
さて、さんざん「インスタンスの構造」と言ってきましたが実際には「クラスの構造」と言った方が正しいと思います。
実は多くのクラスに関してはShadowninja108氏のおかげで既に定義されています。
なので、基本的には呼び出したいインスタンスのポインタを探すことが課題になってきます。
が、サーモンランに関しては全くクラスの構造が定義されていないためこれは自分で頑張って構築する必要があります。
いくつかぼくが定義してちゃんと動くことを確認したものがあるのでそれを載せておきます。
EventGeyser.hはEventDirector.hに名前変えないとですね…
まだクラスが定義されていない場合は、上のような感じで使いたいクラスのヘッダーファイルを作成する必要があります。
が、ここの話まで書いていると無限に長くなるので今回はクラスの構造がわかっている場合について解説します。
実際にやってみよう
ここまでずっと「これが何で必要なのか~」ということを説明してきましたが、実際にチュートリアルをやってみるのが一番だと思います。
やるべきことは以下の六つですので確認してみましょう
内容 | やること/データ | 難易度 |
表示したいインスタンス | 自分で決める | ☆ |
インスタンスのポインタ | 頑張って探す | ☆☆☆☆ |
インスタンスの定義 | 頑張って定義する | ☆☆☆☆☆ |
Codehook.slpatch | 後で解説 | ☆ |
main.cpp | 後で解説 | ☆☆☆ |
exported.txt | 後で解説 | ☆☆ |
表示したいデータについては自分がやりたいと思ったことからチャレンジしてみるのが一番です。
今回はサーモンランの設定を司っているGame::Coop::Setting
をチョイスしました。何故ならチュートリアルとして最適なくらいに簡単だからです。
インスタンスの定義ですが、ここまで説明してるとやはり長くなりすぎるので今回はできているものと仮定して話を進めます。
コンストラクタを探す
ではインスタンスのポインタをどうやって調べるかということなのですがこれにはGHIDRAかIDA Proが必ず必要になります。
GHIDRAでSplatoon2を解析する方法については以下の記事で解説しているのでどうぞ。
探し方としてはまずはGame::Coop::Setting
のメンバ関数を調べます。
このとき、以下のどちらかが見つかれば話は早いです。
Game::Coop::Setting::Setting()
いわゆるコンストラクタというやつで、これがあれば一番見つけやすいです。
が、今回はこのケースではありませんでした。
Cmn::Singleton<Game::Coop::Setting>
これも一応コンストラクタと読んでいいんでしょうか?
よくわからないのですが、今回はこちらが見つかりました。
インスタンスのアドレスを探す
さて、先程見つけたコンストラクタにジャンプすると以下のような命令が見つかります。
005C117C ADRP X8, #off_4160E08@PAGE 005C1180 LDR X8, [X8,#off_4160E08@PAGEOFF] 005C1184 MOV X0, X19 ; this 005C1188 STR XZR, [X8] ; Cmn::Singleton<Game::Coop::Setting>::GetInstance_(void)::sInstance
四行目のGetInstance_(void)がまさにビンゴ!
このとき、ADRP命令で参照されているoff_4160E08@PAGE
がまさにインスタンスのポインタになります。
実際にそこにアクセスしてみると以下のようなデータが見れると思います。
04160E08 off_4160E08 DCQ _ZZN3Cmn9SingletonIN4Game4Coop7SettingEE12GetInstance_EvE9sInstance
この、0x04160E08
こそがまさに求めていたGame::Coop::Setting
のポインタなのです。
Codehook.slpatchの編集
現状、以下のような状況であることを確認しましょう。
内容 | やること/データ |
表示したいインスタンス | Game::Coop::Setting |
インスタンスのポインタ | 0x004160E08 |
Game::Coop::Settingの定義 | できていると仮定 |
Codehook.slpatch | 未着手←次ココ |
main.cpp | 未着手 |
exported.txt | 未着手 |
さて、Codehook.slpatchはインラインアセンブラなのですが、一体どんなコードを書けばいいのでしょうか?
大雑把に説明すると、表示したいデータ一つにつき以下の三行で一組の(ような)コードをMOV X0, X25
の下に追加する必要があります。
ADRP X2, ADDRESS // sInstance LDR X2, [X2, OFFSET] LDR X2, [X2]
今回の場合は追加すると以下のようになります。
[version=310, target=main] gsys::SystemTask::invokeDrawTV_+284 NOP // enable display debug stuff (which is used for hook) gsys::SystemTask::invokeDrawTV_+390: MOV X1, X0 MOV X0, X25 ADRP X2, ADDRESS // sInstance NEW LDR X2, [X2, OFFSET] // NEW LDR X2, [X2] // NEW BL renderEntrypoint B #0x2B8 // NEED TO CHANGE enl::NinInetMatchingManagerPlatformImpl::internetInitialize+BC BL nnSocketInitHook
ここで必要になるのはADDRESSとOFFSETの値を求めることと、最後のB #0x2B8
の編集です。
ADDRESSとOFFSET
さて、先程は追加する三行で一組のコードを大雑把に説明しましたが、実際には以下のパラメータを考えなければいけません。

必要なパラメータはそれぞれ赤・青・緑の箇所で、ここをそれぞれ変える必要があります。
じゃあどうやって変えた値を計算するかというと、元々のコードから何回コードを追加したかだけで決まります。
今回は初めて追加するので、追加する引数は1つ目ですね。
追加する引数 | 赤 | 青の計算式 | Bの値 |
1つ目 | X2 | 17BCB24 | 2AC |
2つ目 | X3 | 17BCB30 | 2A0 |
3つ目 | X4 | 17BCB3C | 294 |
4つ目 | X5 | 17BCB48 | 288 |
5つ目 | X6 | 17BCB54 | 27C |
Bの値については後述!

赤に入るのはすぐにX2だとわかるのですが、青に入る数値を計算するにはプログラマ電卓が必要です。
ここで、インスタンスのアドレスから表の数字を引きます。今回の場合だとインスタンスのアドレス4160000 - 17BC000 = 29A4000
を計算すればいいわけです。
このとき、下の三桁を全部0に置き換えた数字が青い箇所に入る数字になります。よって、青い箇所に入る数字は29A4000
になります。
緑に入る数字は簡単で、インスタンスのアドレスの下三桁です。
パラメータ | 値 |
インスタンスのアドレス | 0x4160E08 |
赤の値 | X2 |
青の計算式 | 4160000 – 17BC000 = 29A4000 |
緑の値 | E08 |
Bのオフセット | 2AC |
B命令のオフセット
BのオフセットはX2を追加した場合には2B0なので、ここまでできてCodehook.slpatchの編集は終了です。
以下、完成したものを載せておくので参考にしてください。
[version=310, target=main] gsys::SystemTask::invokeDrawTV_+284 NOP // enable display debug stuff (which is used for hook) gsys::SystemTask::invokeDrawTV_+390: MOV X1, X0 MOV X0, X25 ADRP X2, #0x29A4000 // sInstance NEW LDR X2, [X2, #0xE08] // NEW LDR X2, [X2] // NEW BL renderEntrypoint B #0x2AC // NEED TO CHANGE enl::NinInetMatchingManagerPlatformImpl::internetInitialize+BC BL nnSocketInitHook
おまけ
自分が調べたインスタンスのアドレスの値を載せておきます。
Class | sInstance | Address |
EnemyDirector | 0x544014 | 0x4165740 |
EventDirector | 0x5B9868 | 0x4167BC0 |
CoopSetting | 0x5C28EC | 0x4160E08 |
main.cpp, main.hppの編集
内容 | やること/データ |
表示したいインスタンス | Game::Coop::Setting |
インスタンスのポインタ | 0x004160E08 |
Game::Coop::Settingの定義 | できていると仮定 |
Codehook.slpatch | できた! |
main.cpp | 未着手←次ココ |
exported.txt | 未着手 |
Codehook.slpatchを編集して、renderEntrypoint()に引数を追加するところまではできました。
引数を追加したということは関数の定義が変わったということなので、それをStarlight側に教えてあげなければいけません。なのでrenderEntrypointを書き換えましょう
// main.cpp static Game::Coop::Setting *mCoopSetting; void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter, Game::Coop::Setting *coopSetting) { }
同様に、main.hppも書き換えます。このときインスタンスの構造が書かれているSetting.h
を読み込む必要があるのでそれも書きましょう。
// main.hpp #include "Game/Coop/Setting.h" void renderEntrypoint(agl::DrawContext *drawContext, sead::TextWriter *textWriter, Game::Coop::Setting *coopSetting);
exported.txtの編集
内容 | やること/データ |
表示したいインスタンス | Game::Coop::Setting |
インスタンスのポインタ | 0x004160E08 |
Game::Coop::Settingの定義 | できていると仮定 |
Codehook.slpatch | できた |
main.cpp | できた |
exported.txt | 未着手←次ココ |
exported.txtはrenderEntrypoint()の関数の変更をスプラトゥーン側に伝える(多分そう)ためのファイルです。
これを定義しておかないとMakeの最後の関数をリンクするところで必ずコケます。
exported.txtはデフォルトでは以下のような内容になっているはずですが、_Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterE
というところだけ書き換える必要があります。
// exported.txt { global: _Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterE; _Z16nnSocketInitHookv; __custom_init; __custom_fini; local: *; };
そのためにまずは、build310フォルダ内のStarlight310.mapという24MBくらいある巨大なテキストファイルを開きます。ここで_Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterE
と検索すると2870行目付近でよく似た名前の関数が見つかると思います。
.text._Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterEPN4Game4Coop7SettingE; 0x00000000000002fc 0xf84 main.o 0x00000000000002fc renderEntrypoint(agl::DrawContext*, sead::TextWriter*, Game::Coop::Setting*)
あとは見つかったコードを上書きして保存すれば成功です。
// exported.txt { global: _Z16renderEntrypointPN3agl11DrawContextEPN4sead10TextWriterEPN4Game4Coop7SettingE; _Z16nnSocketInitHookv; __custom_init; __custom_fini; local: *; };
ここまでできたら再びmake install
とコマンドを打てばエラーなくビルドできるはずです。
まとめ
まあここまで長々と書きましたが、ぼくのレポジトリにあるStarlightは既にGame::Coop::Setting
を読み込む設定になっているので、そのままビルドすれば動きます。
ぜひとも活用してみて下さい。
自身を天才と信じて疑わないマッドサイエンティスト。二つ上の姉は大英図書館特殊工作部勤務、額の十字架の疵は彼女につけられた。
コメント
What kind of mod is this?
この、0x00460E08こそがまさに求めていたGame::Coop::Settingのポインタなのです。
って
この、0x004160E08こそがまさに求めていたGame::Coop::Settingのポインタなのです。
の打ち間違いですか?
仰られるとおりです。
ミスでしたので修正しました。ご報告ありがとうございました。