誰でもできるコード開発 #7
はじめに
今回の内容は以下の記事の続きになります。
この記事を読むにあたって必ず目を通して理解しておいてください。
チーム変更コード
チーム変更コードとは試合中に自分のプレイヤーの属するチームを変更するパッチのことで、スプラトゥーンのゲームは試合中にチームが切り替わることなんて想定していないのでへんてこな現象が起きたりします。
Starlight による実装
Starlight にはコントローラの入力を取得するクラスCollector::mController
があるので、これを利用することで任意のタイミングで好きなコードを実行できます。
ところが 3.1.0 以降のバージョンは Starlight が動作しないので、好きなタイミングでコントローラの入力を取得してコードを実行することはできません。
IPSwitch による実装
任意のタイミングでキー入力をすることはできないのですが、似たような動作を IPSwitch を使って再現することは可能です。
それが前回の講座で紹介したナイスやカモンのシグナルを Hook してナイスの動作を別の命令に上書きしてしまうというものでした。
ナイス自体は試合中であればいつでも呼び出せるので、Starlight によるキー入力を再現することができるのです。
チーム変更コードの仕組み
Starlight によるチーム変更の擬似コードは以下のようになります。
実際にはインスタンスの NULL チェックを行わないとクラッシュします。
Game::PlayerMgr *mPlayerMgr = Collector::mPlayerMgrInstance;
Game::Player *mPlayer = mPlayerMgrS->getControlledPerformer();
if (Collector::mController.isPressed(Controller::Buttons::UpDpad))
mPlayer->mTeam ^= 1;
チーム情報を保存しているデータはCmn::Actor->mTeam
で、これは 0、1、2 のいずれかの値を取ります。
mTeam | 意味 |
---|---|
0 | Alpha |
1 | Bravo |
2 | Neutral |
Neutral は Alpha でも Bravo でもないチームで、観戦者などが割り当てられます。
ちなみに Neutral にはインクの色属性がないので、チームを Neutral に変更してからインクの飛沫を発生させるとゲームがクラッシュします。
サーモンランではプレイヤーは常に Alpha チームで、Bravo にはシャケが割り当てられているよ。
本来であればCmn::Actor->mTeam
にアクセスするためにはCmn::Actor
のポインタを調べなければいけないのですが、Game::Player
クラスはCmn::Actor
クラスを継承しているので、Game::Player
クラスのインスタンスを見つければCmn::Actor
のアドレスはすぐに見つけることができます。
Game::Player クラス
Game::Player
クラスがどのような構造をしているかは Starlight のソースコードを見ればわかります。
0x000 Game::Player
0x000 Cmn::Actor mActor
0x000 Lp::Sys::Actor lpActor
0x2E8 Lp::Sys::XLinkIUser xlinkUser
0x320 uint64_t *xlink
0x328 uint32_t mTeam
0xXXX
0x348 _BYTE somestuff[0x138]
0x480 uint64_t mIndex
0x488 Cmn::PlayerInfo *mPlayerInfo
本当はもっと大きいクラスなのですが、使いそうなのはせいぜいCmn::PlayerInfo
クラスまでだとおもうのでここまでにとどめました。
詳しく知りたい方はソースコードを読んでください。
さて、ここからわかるのはGame::Player
クラスのインスタンス(ポインタ)がわかれば、そこから 0x328 だけズラしたところにチーム情報を格納する値が存在するということです。
Game::Player
クラスのインスタンスを呼び出すコードを書けばいいのですが、実はGame::Player
クラスは前回の記事で紹介したようにCmn::Singleton::GetInstance_(void)::sInstance
で呼び出されているわけではないのです。
ではどうすればいいのかということなのですが、Game::Player
クラスを司っているGame::PlayerMgr
クラスを利用するのです。
インスタンスのアドレス
インスタンスのアドレスを調べるのは適当に検索をかければいいのですが、今回はあらかじめ調べたものをご紹介します。
本講座の趣旨は与えられた情報からコードをつくることであって、情報を調べるところは省略しています。
というのも、誰かがすでに見つけている情報を「あなたも見つけてください」っていうのは単純に時間の無駄だから。
ぼくは秘密主義ではないのでそんな無駄なことをさせるつもりはありません。
Game::PlayerMgr::sInstance | sendSignalEvent() |
---|---|
04157578 | 00E797FC |
sendSignalEvent()
に関しては前回と同じです。
インスタンスを呼び出す
インスタンスを呼び出すためのテンプレートがあることは前回の記事で紹介しました。
おさらいとしてもう一度復習しましょう。
ADRP X0, #0xXXXXX000
LDR X0, [X0, #0xYYY]
LDR X0, [X0]
この三命令で X0 レジスタに呼び出したいインスタンスのポインタが入ります。
つまり、目的アドレス(今回の場合はGame::PlayerMgr
のアドレス)と Hook したいサブルーチンのアドレス(ナイスを Hook するのであれば毎回同じ値)から XXXXX と YYY の値を求めればよいのです。
- XXXXX の求め方
目的アドレスと Hook アドレスの下三桁無くした、目的アドレス - Hook アドレスの計算結果が XXXXX になります。
これは Windows 標準の電卓で簡単に計算することができます。
- YYY の求め方
目的アドレスの下三桁なので 578 になります。
テンプレートを完成させる
さて、テンプレの命令セットに当てはめると以下のコードができます。
ADRP X0, #0x32DE000
LDR X0, [X0, #0x578]
LDR X0, [X0]
これで無事に X0 レジスタにPlayerMgr
のインスタンスのポインタが取得できています。
次にPlayerMgr
から自分が操作するプレイヤーのGame::Player
インスタンスを取得します。
そうしないと自分でないプレイヤーのデータを弄ってしまうことになるからな。
自分の操作するプレイヤーのGame::Player
インスタンスを取得するサブルーチンとしてGame::PlayerMgr::getControlledPerformer()
があるのでこれをつかいます。
サブルーチンの呼び出し方
さて、ここで問題となるのはサブルーチンの呼び出し方です。
ARM64 命令を見ればわかりますが、関数呼び出しは BL 命令を使って実装されています。
int main() {
Game::Player *mPlayer = Game::PlayerMgr::getControlledPerformer();
}
ADRP X0, #0x32DE000
LDR X0, [X0, #0x578]
LDR X0, [X0]
BL _ZNK4Game9PlayerMgr22getControlledPerformerEv
この C++擬似コードと ARM64 命令は等価だぞ。
BL 命令というのは簡単にいえばジャンプ命令で、ジャンプした先のアドレスの命令を実行したあとで RET 命令で BL 命令の次の命令を実行します。
sendSignalEvent()
側では X0 レジスタは全く触れていませんが、BL 命令でジャンプした先のgetControlledPerformer()
が返り値を X0 レジスタにいれていす。
サブルーチン呼び出しで注意すること
プログラムを書く上では全く意識しないことなのですが、全ての関数(サブルーチン)には引数が必要です。
class Game::PlayerMgr {
Game::Player* getControlledPerformer();
}
int main() {
Game::Player *mPlayer = Game::PlayerMgr::getControlledPerformer();
}
例えば上の疑似コードはgetControlledPerformer()
はカッコの中が空っぽなので引数はないようにみえますが、実際には自分自身を引数としてとっています。
なので、本来は以下のように定義されます。
class Game::PlayerMgr {
Game::Player* getControlledPerformer(Game::PlayerMgr * __hidden this);
}
int main() {
Game::Player *mPlayer = Game::PlayerMgr::getControlledPerformer();
}
要するに見えない引数(0 番目の引数)として自分自身のポインタをとっているので、BL 命令をコールする際には必ず X0 レジスタ(これが 0 番目の引数で、1 番めの引数は X1 レジスタとなる)に BL 命令ジャンプ先で実行されるサブルーチンのインスタンスのポインタが入っていなければいけません。
getControlledPerformer()
はGame::PlayerMgr
クラスのサブルーチンなので、Game::PlayerMgr
クラスのインスタンスを X0 レジスタに読み込んでおかなければいけなかったというわけです。
BL 命令の書き方
BL 命令はインスタンスのアドレスを読み込むのと違ってオフセットがないため少しややこしいです。
今回のケースですと、ジャンプしたいアドレスというのはgetControlledPerformer()
のアドレスですので、まずこの値を調べます。
また、BL 命令をコールするアドレスですが、今回は 0104C960 とします(本来この値は BL 命令を書く場所によって変わります)
getControlledPerformer() | BL 命令をコールするアドレス |
---|---|
00F07B1C | 00E79810 |
ここまでわかれば、BL 命令を書くのは簡単です。
ちなみに、ARM64 命令は優しいのでいちいち差を電卓で計算しなくても以下のように書くことができます。
BL #0xF07B1C - 0xE79810
これでアドレス E79810 から F07B1C へジャンプしてgetControlledPerformer()
をコールする BL 命令が書けました。
BL 命令を呼ぶ前には必ず X0 レジスタに呼び出したい関数のインスタンスのポインタが入っていなければいけなかったので、ここまでをまとめると以下のコードが完成します。
ADRP X0, #0x32DE000
LDR X0, [X0, #0x578]
LDR X0, [X0]
BL #0x8E30C
チームを変更しよう
さて、BL 命令が実行されたことにより X0 レジスタにはGame::Player
のインスタンス(自分が操作するプレイヤーのもの)が入っています。
0x000 Game::Player
0x000 Cmn::Actor mActor
0x000 Lp::Sys::Actor lpActor
0x2E8 Lp::Sys::XLinkIUser xlinkUser
0x320 uint64_t *xlink
0x328 uint32_t mTeam
0xXXX
0x348 _BYTE somestuff[0x138]
0x480 uint64_t mIndex
0x488 Cmn::PlayerInfo *mPlayerInfo
チーム情報はGame::Player
クラスの先頭から 0x328 番目に入っているので、この値を取得する必要があります。
X0 に入っているのはポインタなので、データを取得するには LDR 命令を使う必要があります。
この 0x328 番目に入っているという情報はたかはる氏のコードを参考にさせていただきました。
ADRP X0, #0x32DE000
LDR X0, [X0, #0x578]
LDR X0, [X0]
BL #0x8E30C
LDR X1, [X0, #0x328] // X1 = mPlayer[0x328]
これで X0 レジスタにチーム情報である mTeam のデータを読み込むことができました。
レジスタの割り当てを考える
さて、次に読み込んだデータを変更したいのですがmTeam
の値は基本的には 0 か 1 が入っていることを思い出してください(2 は観戦者モード)
となると、チーム変更するためには保存されている値が 1 だったら 0 を、0 だったら 1 を返すコードが必要になります。
if (X0 == 1)
return 0;
if (X0 == 0)
return 1;
これを C++擬似コードで表すと上のようになるのですが、実は ARM で IF 文を実装しようとするとコスト(命令数がたくさん必要)でやっかいです。
ここは IF 文を使わずに出力することを考えましょう。
ARM64 にビット反転の演算があればそれを利用すればいいのですが、ドキュメントを探しても見つからなかった(検索力不足かも)ので別の演算で代用します。
現在のチーム情報は X1 レジスタに入っており、その値はほとんどの場合で 0 か 1 のどちらかです。
X1 レジスタを反転させる NOT X1(X1 レジスタの値を反転させる)のような命令はないのですが論理演算命令はいくつか実装されているので使えないか検討してみます。
- NOT 演算
NOT 演算があれば 1 ならば 0、0 ならば 1 が出力できます。
NOT | 0 を入力 | 1 を入力 |
---|---|---|
- | 1 を出力 | 0 を出力 |
が、この命令はないのでこれは実装できません。
- AND 演算
AND | 0 を入力 | 1 を入力 |
---|---|---|
0 と比較 | 0 | 0 |
1 と比較 | 0 | 1 |
AND 演算は二つの入力がどちらも 1 であれば 1 を返す論理演算ですが、これでは上手く反転させることができません。
- OR 演算
AND | 0 を入力 | 1 を入力 |
---|---|---|
0 と比較 | 0 | 1 |
1 と比較 | 1 | 1 |
OR 演算は入力のどちらかが 1 であれば 1 を返す論理演算ですが、これもやはりそのままの値が出力されるか、どちらも 1 を返すかになってしまうのでダメです。
- ORN 演算
AND | 0 を入力 | 1 を入力 |
---|---|---|
0 -> 1 と比較 | 1 | 1 |
1 -> 0 と比較 | 0 | 1 |
ORN 演算は Xn レジスタと Xm レジスタを反転させた値で論理和を求めます。
いろいろ使い勝手のいい論理演算ですが、今回の場合は OR 演算と同じ結果になってしまうので使えません。
- XOR 演算
AND | 0 を入力 | 1 を入力 |
---|---|---|
0 と比較 | 0 | 1 |
1 と比較 | 1 | 0 |
XOR 演算は排他的論理和といわれる論理演算です。
ARM64 では EOR 命令なのですが、今回は馴染みの深い XOR 演算として紹介します。
この演算は(ひどく大雑把にいえば)比較する二つの値が同じなら 0、異なれば 1 を返します。
ということは XOR 演算を用いて 0 と比較した場合には、
(Constant, Input) => Output
(0, 0) => 0
(0, 1) => 1
となるので 0 なら 0、1 なら 1 を返してしまい意味がないのですが、1 と比較する場合には、
(Constant, Input) => Output
(1, 0) => 1
(1, 1) => 0
となり、ビット反転を擬似的に実装できることがわかります。
変更した値を反映させるには LDR 命令の逆である STR 命令を使えばいいので、ここまでをまとめると以下のようになります。
ADRP X0, #0x32DE000
LDR X0, [X0, #0x578]
LDR X0, [X0]
BL #0x8E30C
LDR X1, [X0, #0x328]
EOR X1, X1, #1
STR X1, [X0, #0x328]
LDR X1, [X0, #0x488]
STR X1, [X0, #0x38]
コールスタック
ここまでできたのであれば「あとはOnline ARM to HEX Converterで HEX 化して終わりじゃないの?」って思う方もいるかも知れませんが、ここで最後のトラップであるコールスタックが残っています。
ここで、オリジナル状態のsendSignalEvent()
の ARM 命令を見返してみましょう。
先頭三行に何をしているのかよくわからない命令があると思います。
00E797FC STR X19, [SP,#-0x10+var_10]!
00E79800 STP X29, X30, [SP,#0x10+var_s0]
00E79804 ADD X29, SP, #0x10
これこそがコールスタックを実装している部分で、サブルーチン内に BL 命令があるのであれば必ず必要になります。
なんで必要になるかは細かく解説しているとこの記事の長さが倍になるので省略します。
とりあえず、サブルーチン内に BL 命令があるときは必ず書かなければいけないと覚えておいてください。
これを書かないと BL 命令後にプログラムカウンタが正しい位置に戻らず、フリーズしてしまいます。
コールスタックの書き方
コールスタックの書き方ですが、サブルーチンにいくつ BL 命令を書くかで変わってきます。
これを書き忘れててずっとフリーズし続けていたのはナイショです。
- BL 命令が一つの場合
命令が一つだけの場合、以下のようにコールスタックを実装します(ここでは意味がわからなくても構いません)
STP X29, X30, [SP, #-0x10]!
MOV X29, SP
LDP X29, X30, [SP], #0x10
RET
サブルーチン開始直後に二つ、RET 命令の直前に一つ合計四命令だけ余計にコードを書いて実装します。
- BL 命令が二つの場合
STR X19, [SP, #-0x20]!
STP X29, X30, [SP, #0x10]
ADD X29, SP, #0x10
LDP X29, X30, [SP, #0x10]
LDR X19, [SP], #0x20
RET
前後にそれぞれ一命令ずつ増えて全部で六命令となります。
前回の記事でコールスタックを上書きしても正しくコードが動いたのは、上書きしたコードの中に BL 命令がなかったためです。
今回は使っているため、このコードを書く必要があるというわけです。
コードをまとめる
今回は BL 命令が一つだけなので、その場合のコールスタックのテンプレートを使ってここまでのコードを全てまとめると以下のようになります。
STP X29, X30, [SP, #-0x10]!
MOV X29, SP
ADRP X0, #0x32DE000
LDR X0, [X0, #0x578]
LDR X0, [X0]
BL #0x8E30C
LDR X1, [X0, #0x328]
EOR X1, X1, #1
STR X1, [X0, #0x328]
LDR X1, [X0, #0x488]
STR X1, [X0, #0x38]
LDP X29, X30, [SP], #0x10
RET
あとはこのコードをGame::PlayerCloneHandle::sendSignalEvent
に対して上書きすれば良いのでアドレスも考えると以下のようになります。
00E797FC STP X29, X30, [SP, #-0x10]!
00E79800 MOV X29, SP
00E79804 ADRP X0, #0x32DE000
00E79808 LDR X0, [X0, #0x578]
00E7980C LDR X0, [X0]
00E79810 BL #0x8E30C
00E79814 LDR X1, [X0, #0x328]
00E79818 EOR X1, X1, #1
00E7981C STR X1, [X0, #0x328]
00E79820 LDR X1, [X0, #0x488]
00E79824 STR X1, [X0, #0x38]
00E79828 LDP X29, X30, [SP], #0x10
00E7982C RET
BL 命令はまとめて変換するとオフセットがズレるバグがあるので、BL 命令の箇所だけは必ず個別に変換してください。
ここで、なぜ先ほど BL 命令の説明をしたときに E79810 を使うと決めたかがわかると思います。
// Swap Team Color by Signal (3.1.0) [tkgling]
@disabled
00E797FC FD7BBFA9 // STP X29, X30, [SP, #-0x10]!
00E79800 FD030091 // MOV X29, SP
00E79804 E09601D0 // ADRP X0, #0x32DE000
00E79808 00BC42F9 // LDR X0, [X0, #0x578]
00E7980C 000040F9 // LDR X0, [X0]
00E79810 C3380294 // BL #0x8E30C
00E79814 019441F9 // LDR X1, [X0, #0x328]
00E79818 210040D2 // EOR X1, X1, #1
00E7981C 019401F9 // STR X1, [X0, #0x328]
00E79820 014442F9 // LDR X1, [X0, #0x488]
00E79824 011C00F9 // STR X1, [X0, #0x38]
00E79828 FD7BC1A8 // LDP X29, X30, [SP], #0x10
00E7982C C0035FD6 // RET
// Swap Team Color by Signal (5.4.0) [tkgling]
@disabled
0104C94C FD7BBFA9 // STP X29, X30, [SP, #-0x10]!
0104C950 FD030091 // MOV X29, SP
0104C954 80E500B0 // ADRP X0, #0x1CB1000
0104C958 007C46F9 // LDR X0, [X0, #0xCF8]
0104C95C 000040F9 // LDR X0, [X0]
0104C960 F3680294 // BL #0x9A3CC
0104C964 019441F9 // LDR X1, [X0, #0x328]
0104C968 210040D2 // EOR X1, X1, #1
0104C96C 019401F9 // STR X1, [X0, #0x328]
0104C970 014442F9 // LDR X1, [X0, #0x488]
0104C974 011C00F9 // STR X1, [X0, #0x38]
0104C978 FD7BC1A8 // LDP X29, X30, [SP], #0x10
0104C97C C0035FD6 // RET
上の二つはどちらも等価なコードですが、可読性をとるなら上のコードを、利用するだけなら下のコードを使えば良いと思います。
ちなみに、ナイスって書いてあるけど、カモンでも変わります。
記事は以上。