[IPSwitch] 誰でもできるコード開発 #7

はじめに

今回の内容は以下の記事の続きになります。

[IPSwitch] 誰でもできるコード開発 #6 (opens new window)

この記事を読むにあたって必ず目を通して理解しておいてください。

チーム変更コード

チーム変更コードとは試合中に自分のプレイヤーの属するチームを変更するパッチのことで、スプラトゥーンのゲームは試合中にチームが切り替わることなんて想定していないのでへんてこな現象が起きたりします。

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;
1
2
3
4

チーム情報を保存しているデータは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
1
2
3
4
5
6
7
8
9
10

本当はもっと大きいクラスなのですが、使いそうなのはせいぜいCmn::PlayerInfoクラスまでだとおもうのでここまでにとどめました。

詳しく知りたい方はソースコード (opens new window)を読んでください。

さて、ここからわかるのはGame::Playerクラスのインスタンス(ポインタ)がわかれば、そこから 0x328 だけズラしたところにチーム情報を格納する値が存在するということです。

Game::Playerクラスのインスタンスを呼び出すコードを書けばいいのですが、実はGame::Playerクラスは前回の記事で紹介したようにCmn::Singleton::GetInstance_(void)::sInstanceで呼び出されているわけではないのです。

ではどうすればいいのかということなのですが、Game::Playerクラスを司っているGame::PlayerMgrクラスを利用するのです。

インスタンスのアドレス

インスタンスのアドレスを調べるのは適当に検索をかければいいのですが、今回はあらかじめ調べたものをご紹介します。

本講座の趣旨は与えられた情報からコードをつくることであって、情報を調べるところは省略しています。

というのも、誰かがすでに見つけている情報を「あなたも見つけてください」っていうのは単純に時間の無駄だから。

ぼくは秘密主義ではないのでそんな無駄なことをさせるつもりはありません。

Game::PlayerMgr::sInstance sendSignalEvent()
02CFDCF8 0104C94C

sendSignalEvent()に関しては前回と同じです。

インスタンスを呼び出す

インスタンスを呼び出すためのテンプレートがあることは前回の記事で紹介しました。

おさらいとしてもう一度復習しましょう。

ADRP X0, #0xXXXXX000
LDR X0, [X0, #0xYYY]
LDR X0, [X0]
1
2
3

この三命令で X0 レジスタに呼び出したいインスタンスのポインタが入ります。

目的 Hook
02CFDCF8 0104C94C

つまり、目的アドレス(今回の場合はGame::PlayerMgrのアドレス)と Hook したいサブルーチンのアドレス(ナイスを Hook するのであれば毎回同じ値)から XXXXX と YYY の値を求めればよいのです。

  • XXXXX の求め方

目的アドレスと Hook アドレスの下三桁を全て 0 にし、目的アドレス - Hook アドレスの計算結果が XXXXX になります。

目的 Hook 結果
02CFD000 0104C000 01CB1000

これは Windows 標準の電卓で簡単に計算することができます。

  • YYY の求め方

目的アドレスの下三桁なので CF8 になります。

テンプレートを完成させる

さて、テンプレの命令セットに当てはめると以下のコードができます。

ADRP X0, #0x1CB1000
LDR X0, [X0, #0xCF8]
LDR X0, [X0]
1
2
3

これで無事に X0 レジスタにPlayerMgrのインスタンスのポインタが取得できています。

次にPlayerMgrから自分が操作するプレイヤーのGame::Playerインスタンスを取得します。

そうしないと自分でないプレイヤーのデータを弄ってしまうことになるからな。

自分の操作するプレイヤーのGame::Playerインスタンスを取得するサブルーチンとしてGame::PlayerMgr::getControlledPerformer()があるのでこれをつかいます。

サブルーチンの呼び出し方

さて、ここで問題となるのはサブルーチンの呼び出し方です。

ARM64 命令を見ればわかりますが、関数呼び出しは BL 命令を使って実装されています。

// C++
int main() {
  Game::Player *mPlayer = Game::PlayerMgr::getControlledPerformer();
}
1
2
3
4
// ARM64
ADRP X0, #0x1CB1000
LDR X0, [X0, #0xCF8] // Game::PlayerMgr*
LDR X0, [X0]         // this (Game::PlayerMgr sInstance)
BL _ZNK4Game9PlayerMgr22getControlledPerformerEv
1
2
3
4
5

この C++ 擬似コードと ARM64 命令は等価だぞ。

BL 命令というのは簡単にいえばジャンプ命令で、ジャンプした先のアドレスの命令を実行したあとで RET 命令で BL 命令の次の命令を実行します。

sendSignalEvent()側では X0 レジスタは全く触れていませんが、BL 命令でジャンプした先のgetControlledPerformer()が返り値を X0 レジスタにいれていす。

サブルーチン呼び出しで注意すること

プログラムを書く上では全く意識しないことなのですが、全ての関数(サブルーチン)には引数が必要です。

// C++
class Game::PlayerMgr {
    Game::Player* getControlledPerformer();
}
int main() {
  Game::Player *mPlayer = Game::PlayerMgr::getControlledPerformer();
}
1
2
3
4
5
6
7
8

例えば上の疑似コードはgetControlledPerformer()はカッコの中が空っぽなので引数はないようにみえますが、実際には自分自身を引数としてとっています。

なので、本来は以下のように定義されます。

// C++
class Game::PlayerMgr {
    Game::Player* getControlledPerformer(Game::PlayerMgr * __hidden this);
}
int main() {
  Game::Player *mPlayer = Game::PlayerMgr::getControlledPerformer();
}
1
2
3
4
5
6
7
8

要するに見えない引数(0 番目の引数)として自分自身のポインタをとっているので、BL 命令をコールする際には必ず X0 レジスタ(これが 0 番目の引数で、 1 番めの引数は X1 レジスタとなる)に BL 命令ジャンプ先で実行されるサブルーチンのインスタンスのポインタが入っていなければいけません。

getControlledPerformer()Game::PlayerMgrクラスのサブルーチンなので、Game::PlayerMgrクラスのインスタンスを X0 レジスタに読み込んでおかなければいけなかったというわけです。

BL 命令の書き方

BL 命令はインスタンスのアドレスを読み込むのと違ってオフセットがないため少しややこしいです。

今回のケースですと、ジャンプしたいアドレスというのはgetControlledPerformer()のアドレスですので、まずこの値を調べます。

また、 BL 命令をコールするアドレスですが、今回は 0104C960 とします(本来この値は BL 命令を書く場所によって変わります)。

getControlledPerformer() BL 命令をコールするアドレス
010E6D2C 0104C960

ここまでわかれば、BL 命令を書くのは簡単です。

ARM64 命令は優しいのでいちいち差を電卓で計算しなくても以下のように書くことができます。

// ARM64
BL #0x9A3CC // #0x10E6D2C - 0x104C960
1
2

これでアドレス 104C960 から 10E6D2C へジャンプしてgetControlledPerformer()をコールする BL 命令が書けました。

BL 命令を呼ぶ前には必ず X0 レジスタに呼び出したい関数のインスタンスのポインタが入っていなければいけなかったので、ここまでをまとめると以下のコードが完成します。

ADRP X0, #0x1CB1000
LDR X0, [X0, #0xCF8] // Game::PlayerMgr*
LDR X0, [X0]         // this (Game::PlayerMgr sInstance)
BL #0x9A3CC          // #0x10E6D2C - 0x104C960
1
2
3
4

チームを変更しよう

さて、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
1
2
3
4
5
6
7
8
9
10

チーム情報はGame::Playerクラスの先頭から 0x328 番目に入っているので、この値を取得する必要があります。

X0 に入っているのはポインタなので、データを取得するには LDR 命令を使う必要があります。

この 0x328 番目に入っているという情報はたかはる氏のコードを参考にさせていただきました。

ADRP X0, #0x1CB1000
LDR X0, [X0, #0xCF8] // Game::PlayerMgr*
LDR X0, [X0]         // this (Game::PlayerMgr sInstance)
BL #0x9A3CC          // Game::Player *mPlayer = Game::Player::Player()
LDR X0, [X0, #0x328] // X0 = mPlayer[0x328]
1
2
3
4
5

これで X0 レジスタにチーム情報である mTeam のデータを読み込むことができました。

レジスタの割り当てを考える

ただ、ここまでコードを書いてみて X0 レジスタを何度も呼び出しているので全くデータを保存できていないことがわかります。

X0 レジスタの値が上書きされ続けてしまっているのだ。

X0 レジスタは本来、インスタンスのポインタを指すために使われるべきであり(そうしておけば非常にわかりやすい)何度も繰り返し使われるのは避けるべきです。

特に何か決まりがあるわけではないのですが、X8 レジスタあたりからデータを保存することに使われるみたいなので(スプラのコードを読んだ限りはそんな気がするだけで根拠なし)、以下のようにコードを書き換えます。

ADRP X8, #0x1CB1000
LDR X8, [X8, #0xCF8]
LDR X0, [X8] // this
BL #0x9A3CC  // #0x10E6D2C - 0x104C960
LDR X8, [X0, #0x328]
1
2
3
4
5

こうすれば X0 レジスタには常にポインタが入っていることが明らかで、可読性が遥かに向上します。

さて、次に読み込んだデータを変更したいのですがmTeamの値は基本的には 0 か 1 が入っていることを思い出してください(2 は観戦者モード)。

となると、チーム変更するためには保存されている値が 1 だったら 0 を、0 だったら 1 を返すコードが必要になります。

// C++
if (X0 == 1)
  return 0;
if (X0 == 0)
  return 1;
1
2
3
4
5

これを C++ 擬似コードで表すと上のようになるのですが、実は ARM で IF 文を実装しようとするとコスト(命令数がたくさん必要)でやっかいです。

ここは IF 文を使わずに出力することを考えましょう。

ARM64 にビット反転の演算があればそれを利用すればいいのですが、ドキュメントを探しても見つからなかった(検索力不足かも)ので別の演算で代用します。

現在のチーム情報は X8 レジスタに入っており、その値はほとんどの場合で 0 か 1 のどちらかです。

X8 レジスタを反転させる NOT X8(X8 レジスタの値を反転させる)のような命令はないのですが論理演算命令はいくつか実装されているので使えないか検討してみます。

  • 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
1
2
3

となるので 0 なら 0 、1 なら 1 を返してしまい意味がないのですが、1 と比較する場合には、

(Constant, Input) => Output
(1, 0) => 1
(1, 1) => 0
1
2
3

となり、ビット反転を擬似的に実装できることがわかります。

変更した値を反映させるには LDR 命令の逆である STR 命令を使えばいいので、ここまでをまとめると以下のようになります。

ADRP X8, #0x1CB1000
LDR X8, [X8, #0xCF8]
LDR X0, [X8]   // this
BL #0x9A3CC    // #0x10E6D2C - 0x104C960
LDR X8, [X0, #0x328]
EOR X8, X8, #1 // x8 = ~X8
STR X8, [X0, #0x328]
1
2
3
4
5
6
7

コールスタック

ここまでできたのであれば「あとは ARM to HEX Converter で HEX 化して終わりじゃないの?」って思う方もいるかも知れませんが、ここで最後のトラップであるコールスタック (opens new window)が残っています。

ここで、オリジナル状態のsendSignalEvent()の ARM 命令を見返してみましょう。

先頭三行に何をしているのかよくわからない命令があると思います。

0104C94C                 STR             X19, [SP,#var_20]!
0104C950                 STP             X29, X30, [SP,#0x20+var_10]
0104C954                 ADD             X29, SP, #0x20+var_10
1
2
3

これこそがコールスタックを実装している部分で、サブルーチン内に BL 命令があるのであれば必ず必要になります。

なんで必要になるかは細かく解説しているとこの記事の長さが倍になるので省略します。

とりあえず、サブルーチン内に BL 命令があるときは必ず書かなければいけないと覚えておいてください。

これを書かないと BL 命令後にプログラムカウンタが正しい位置に戻らず、フリーズしてしまいます。

コールスタックの書き方

コールスタックの書き方ですが、サブルーチンにいくつ BL 命令を書くかで変わってきます。

これを書き忘れててずっとフリーズし続けていたのはナイショです。

  • BL 命令が一つの場合

命令が一つだけの場合、以下のようにコールスタックを実装します(ここでは意味がわからなくても構いません)。

STP X29, X30, [SP, #-0x10]!
MOV X29, SP
LDP X29, X30, [SP], #0x10
RET
1
2
3
4
5

サブルーチン開始直後に二つ、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
1
2
3
4
5
6
7

前後にそれぞれ一命令ずつ増えて全部で六命令となります。

前回の記事でコールスタックを上書きしても正しくコードが動いたのは、上書きしたコードの中に BL 命令がなかったためです。

今回は使っているため、このコードを書く必要があるというわけです。

コードをまとめる

今回は BL 命令が一つだけなので、その場合のコールスタックのテンプレートを使ってここまでのコードを全てまとめると以下のようになります。

STP X29, X30, [SP, #-0x10]!
MOV X29, SP
ADRP X8, #0x1CB1000
LDR X8, [X8, #0xCF8]
LDR X0, [X8]
BL #0x9A3CC
LDR X8, [X0, #0x328]
EOR X8, X8, #1
STR X8, [X0, #0x328]
LDP X29, X30, [SP], #0x10
RET
1
2
3
4
5
6
7
8
9
10
11
12
13

あとはこのコードをGame::PlayerCloneHandle::sendSignalEventに対して上書きすれば良いのでアドレスも考えると以下のようになります。

0104C94C STP X29, X30, [SP, #-0x10]!
0104C950 MOV X29, SP
0104C954 ADRP X8, #0x1CB1000
0104C958 LDR X8, [X8, #0xCF8]
0104C95C LDR X0, [X8]
0104C960 BL #0x9A3CC
0104C964 LDR X9, [X0, #0x328]
0104C968 EOR X9, X9, #1
0104C96C STR X9, [X0, #0x328]
0104C970 LDR X1, [X0, #0x488]
0104C974 STR X9, [X1, #0x38]
0104C978 LDP X29, X30, [SP], #0x10
0104C97C RET
1
2
3
4
5
6
7
8
9
10
11
12
13

ここで、なぜ先ほど BL 命令の説明をしたときに 104C960 を使うと決めたかがわかると思います。

あとはこれらのコードを ARM to HEX Converter で変換するだけです。

ただ、既存の ARM to HEX Converter にはバグ(それともややこしい仕様?)があり、BL 命令を変換すると変なオフセットがつけられてしまいます。

同じ命令を三回書いているのに、ARM64 の変換結果が毎回違うというおかしなことになります。

この場合、正しいのは一番上のコードです。

// Swap Team Color by Signal [tkgling]
@disabled
0104C94C FD7BBFA9 // STP X29, X30, [SP, #-0x10]!
0104C950 FD030091 // MOV X29, SP
0104C954 88E500B0 // ADRP X8, #0x1CB1000
0104C958 087D46F9 // LDR X8, [X8, #0xCF8]
0104C95C 000140F9 // LDR X0, [X8]
0104C960 F3680294 // BL #0x9A3CC
0104C964 099441F9 // LDR X9, [X0, #0x328]
0104C968 290140D2 // EOR X9, X9, #1
0104C96C 099401F9 // STR X9, [X0, #0x328]
0104C970 014442F9 // LDR X1, [X0, #0x488]
0104C974 291C00F9 // STR X9, [X1, #0x38]
0104C978 FD7BC1A8 // LDP X29, X30, [SP], #0x10
0104C97C C0035FD6 // RET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上の二つはどちらも等価なコードですが、可読性をとるなら上のコードを、利用するだけなら下のコードを使えば良いと思います。

ちなみに、ナイスって書いてあるけど、カモンでも変わります。

記事は以上。