えいむーさんは明日も頑張るよ

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

価格

# [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;

チーム情報を保存しているデータは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クラスまでだとおもうのでここまでにとどめました。

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

さて、ここからわかるのは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 になります。

0415700E79=032DE04157-00E79=032DE

これは 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 命令を書くのは簡単です。

00F07B1C00E79810=0008E30C00F07B1C-00E79810=0008E30C

ちなみに、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 (opens new window)で HEX 化して終わりじゃないの?」って思う方もいるかも知れませんが、ここで最後のトラップであるコールスタック (opens new window)が残っています。

ここで、オリジナル状態の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

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

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

記事は以上。

価格
    えいむーさんは明日も頑張るよ © 2022