誰でもできるコード開発 #10
はじめに
今回の内容は以下の記事の続きになります。
この記事を読むにあたって必ず目を通して理解しておいてください。
コードの意味を理解しよう
今回は、昔実装した 5.4.0 のスペシャルをリアルタイムに変更するコードを改良していきます。
以前書いたコードは次のようなアセンブラでした。
0104C94C STP X29, X30, [SP, #-0x10]!
0104C950 MOV X29, SP
0104C954 ADRP X0, #0x1CB1000
0104C958 LDR X0, [X0, #0xCF8]
0104C95C LDR X0, [X0]
0104C960 BL #0x9A3CC
0104C964 LDR X1, [X0, #0x450]
0104C968 CMP X1, #13
0104C96C LDR X1, [X0, #0x450]
0104C970 ADD X2, X1, #1
0104C974 CSEL X1, X2, XZR, LO
0104C978 STR X1, [X0, #0x450]
0104C97C OP
0104C980 NOP
0104C984 NOP
0104C988 NOP
0104C98C NOP
0104C990 LDP X29, X30, [SP], #0x10
0104C994 RET
::: tip 二回 LDR 命令を読み込んでいる理由
自分の前書いた記事によると、CMP X1, #13
を実行した時点で X1 レジスタの値が変わってしまうからだという。
が、ドキュメントを読むと減算結果自体は利用せず、単に NZCV レジスタのビットを更新するだけだと書いてある。
ドキュメントが正しいような気はするのだが、手元で確認できないため二回書くコードをそのまま採用した。
:::
コードを読めば一番めんどくさいのがCSEL X1, X2, XZR, LO
ということがわかる。
IF 文を使って書こうとするとこのように長くなってしまうのだが、三項演算子を使えばスマートに書ける。
基本的にどのプログラミング言語も三項演算子をサポートしており、IF 文よりも簡潔に書ける場合が多いので覚えておくと良い。
三項演算子のルール
三項演算子は以下のように記述できる。
X0 = X1 % 2 == 0 ? TRUE : FALSE
これだけだとさっぱりだと思うのだが、カッコをつければ幾分わかりやすくなるのではないだろうか。
X0 = ((X1 % 2 == 0) ? TRUE : FALSE)
三項演算子というだけあって、カッコの中には三つの項が入っており、それぞれ条件式(CONDITION)、真式、偽式と呼ばれている。
X0 = (CONDITION ? VALUE IF TRUE : VALUE IF FALSE)
つまり、CONDITION
の中身がTRUE
であれば?
の後ろの値を返し、FALSE
であれば:
の後ろの値を返す。
よって、先程の
X0 = X1 % 2 == 0 ? TRUE : FALSE
という三項演算子は、
if (X1 % 2 == 0) {
X0 = TRUE;
} else {
X0 = FALSE;
}
と書いているのと全く等価である。
この章では IF 文は三項演算子に変換可能で、アセンブラでは主にこの三項演算子を使うということを覚えておいてほしい。
CSEL の使い方
そして先程書いた CSEL というのは IF 文というよりは、三項演算子に近い考え方ができる。
コード内ではCSEL X1, X2, XZR, LO
という命令が出てきたが、これは C 言語風にX1 = NZCV is LO ? X2 : XZR
という三項演算子に変換できる。
つまり「NZCV が LO なら X1 = X2 とし、LO でないなら X1 = XZR とする」という意味になる。
XZR というのは読み込めば常に 0 を返すゼロレジスタと呼ばれるものなので「NZCV が LO なら X1 = X2 とし、LO でないなら X1 = 0 とする」と読み替えても良い。
条件フラグとサフィックス
ここまで理解できれば「NZCV と LO は何なのだろう」という疑問が当然浮かぶ。
NZCV は条件フラグと呼ばれる特殊なレジスタで、計算の結果におけるフラグを保存しているレジスタである。
例えば、計算結果が桁上りしたとか、そういうデータを持っている。
何故 NZCV などという名前がついているかというと、一つのレジスタで次の四つのフラグ情報を扱っているからである。
1 | 0 | |
---|---|---|
N | x < 0 | x >= 0 |
Z | x = 0 | x != 0 |
C | 桁あふれ発生 | 桁あふれなし |
V | オーバーフロー | オーバーフローなし |
- N (Negative or Not)
- 計算結果が負かどうかを判定する
- Z (Zero or Not)
- 計算結果が 0 かどうかを判定する
- C (Carry or Not)
- キャリー(桁あふれ)が発生したかどうかを判定する
- V
- オーバーフローしたかどうかを判定する
::: tip なんで V なのか
Buffer overflow なら B とか O を使いそうなものなのに、何故 V なのかちょっとよくわからない(理由があるのだろうけれど)
:::
LO というのは先程の三項演算子内のNZCV is LO
という擬似コードを解釈するためのサフィックスであり、ARM64 には次の 16 のサフィックスがあります。
全部は覚えなくていいけど、いくつか覚えておくと条件文が書きやすくなります。
サフィックス | 意味 | フラグ |
---|---|---|
EQ | Equal | Z = 1 |
NE | Not Equal | Z = 0 |
CS/HS | - | C = 1 |
CC/LO | - | C = 0 |
MI | Minus | N = 1 |
PL | Plus | N = 0 |
VS | - | V = 1 |
VC | - | V = 0 |
HI | - | C = 1 AND Z =0 |
LS | - | C= 0 OR Z = 1 |
GE | Greater Equal | N = V |
LT | Lower Than | N <> V |
GT | Greater Than | Z = 0 AND N = V |
LE | Lower Equal | Z = 1 OR N <> V |
AL | Always | Any |
NV | Never | Any |
これを見ると最初のアセンブラであったCSEL X1, X2, XZR, LO
が C 言語風にX1 = NZCV is LO ? X2 : XZR
と書け更にサフィックスを理解することでX1 = C == 0 ? X2 : XZR
と変化するのがわかるでしょうか。
::: tip C == 0 なわけ
サフィックスのフラグではC = 0
となっているのに、何故三項演算子内ではC == 0
になっているのかと気になるかもしれない。
フラグの方はあくまでも人間的にわかりやすい書き方なのでC = 0
と書いてあるが、プログラミング的には IF (C == 0
) という意味になる。
:::
ここまでの流れ
話がややこしくなってきたので、ここで一旦まとめてみます。
CSEL X1, X2, XZR, LO
が条件文になっている- 条件文といえば IF 文が思い浮かぶが、これはどちらかといえば三項演算子に近い
- 三項演算子は
X0 = (CONDITION ? VALUE IF TRUE : VALUE IF FALSE)
という書き方をする
この三項演算子は以下の IF 文と等価である。
if (CONDITION == TRUE) {
X0 = TRUE;
} else {
X0 = FALSE;
}
CSEL X1, X2, XZR, LO
は三項演算子に変換するとX1 = NZCV is LO ? X2 : XZR
となる- NZCV は条件フラグレジスタ
- LO はサフィックスを意味する
- NZCV レジスタの LO 判定が TRUE なら X2、FALSE なら XZR を返す
じゃあ「NZCV の LO 判定」とは何なのか、ということですね。
ここでさっきの上の表を見れば「LO 判定」というのは「キャリーフラグが 0 かどうか」というところしか見ていません。
「え、でもキャリーフラグなんてどこでも弄ってないよ」と思うかもしれませんが、実はCMP X1, #13
を実行したときにこっそり更新されていたのです。
NZCV レジスタを更新する命令
全ての命令が NZCV レジスタを更新するわけではありません。
全部を覚える必要もやはりないのですが、命令名に S がついている場合は更新すると覚えておくと良いかもしれません。
例えば ADD 命令や SUB 命令は NZCV レジスタを更新しませんが ADDS 命令や SUBS 命令はその計算結果で NZCV レジスタを更新します。
「CMP 命令は S ついてないのに NZCV レジスタを更新してるじゃん」と思うかもしれませんが、実は CMP 命令はアセンブラから機械語に翻訳されるときに SUBS として解釈されます。
つまり、CMP 命令というものは実際には存在しない命令なのです。
で、全部書くとあまりにも長いので主要な命令と C と Z に関する判定をまとめてみました。
命令 | 例 | 意味 | C = 1 | C = 0 | Z = 1 | Z = 0 |
---|---|---|---|---|---|---|
CMP | CMP X1, X2 | SUBS XZR, X1, X2 | X1 > = X2 | X1 < X2 | X1 == X2 | X1 != X2 |
CMN | CMN X1, X2 | ADDS XZR, X1, X2 | ||||
SUBS | SUBS X0, X1, X2 | X0 = X1 - X2 | X1 > = X2 | X1 < X2 | X1 == X2 | X1 != X2 |
ADDS | ADDS X0, X1, X2 | X0 = X1 + X2 | ||||
NEGS | NEGS X1, X2 | SUBS X1, XZR, X2 | 0 >= X2 | 0 < X2 | 0 == X2 | 0 != X2 |
簡単な条件文を書くだけなら C と Z の条件フラグだけ見れば十分です。
::: tip NZCV フラグについて
CMP 命令は NZCV フラグのうち C と Z を引数の値によって変化させる。
CMP X1, X2
という命令が与えられたときX1 == X2
であればZ = 1
、そうでなければZ = 0
となり、X1 >= X2
であればC = 1
、X1 < X2
であれば C = 0
となる。
注意すべき点は一回の CMP 命令で C と Z のフラグのどちらも変化するということです。
:::
前回のコードの解説
CSEL X1, X2, XZR, LO
という命令は、X1 = NZCV is LO ? X2 : XZR
となり、NZCV is LO
というのが「キャリーフラグが 0 かどうか」を判定しているサフィックスなので最終的にX1 = C == 0 ? X2 : XZR
と解釈されることがわかった。
キャリーフラグはどこでも更新されていないように見えるが、実はCMP
命令で更新されており、上の表を見るとCMP Xm, Xn
という命令だとXm < Xn
ならばキャリーフラグ 0 が入ることがわかる。
よって、X1 = C == 0 ? X2 : XZR
は今回の場合X1 = Xm < Xn ? X2 : XZR
と扱うことができる。
更に、コード内 CMP 命令はCMP X1, #13
というものだったので、最終的にX1 = X1 < 13 ? X2 : XZR
という命令が得られる。
これを擬似コードで書くと、
if (X1 < 13) {
X1 = X2; // X2 = X1 + 1
} else {
X1 = XZR; // XZR = 0
}
となり、X1(現在のスペシャルの ID)の値が 12 以下の場合は X1 の値を 1 増やし、13 以上のときは 0 を返すという処理が実行できていたわけです。
::: tip 今更ながら思ったのだが
LO サフィックスは C(キャリーフラグ)が 0 ということしか見ていないので、値が負であるかどうかは考慮されていない。
今回のコードでは問題なかったが N(正負フラグ)が 1(負数)であることもチェックすべきである。
なので LO(C = 0
のみチェック)よりも LS(C = 0
またはZ = 1
)で判定したほうが良かった。
:::
コードを修正しよう
スプラトゥーン 2 におけるスペシャルの内容と ID は以下の表のようになっています。
id | Name |
---|---|
0 | SuperMissile |
1 | SuperArmor |
2 | LauncherSplash |
3 | LauncherSuction |
4 | LauncherQuick |
5 | LauncherCurling |
6 | LauncherRobo |
7 | WaterCutter |
8 | Jetpack |
9 | SuperLanding |
10 | RainCloud |
11 | AquaBall |
12 | SuperBubble |
13 | Shachihoko |
14 | - |
15 | RainCloudEnemy |
16 | MissileMissilePosition |
17 | SuperBall |
18 | SuperStamp |
19 | BigLaser |
前回のコードではスペシャル ID を 1 ずつズラし、13 まで増えると 0 に戻すという処理をしていましたが、そうしないと 14 番目の空っぽのデータにアクセスしてゲームがクラッシュしてしまっていたからです。
で、前回はガチホコまでスペシャルを移動させたら 0 のマルチミサイルに戻すという処理をしていたので、17, 18, 19 の ID を持つナイスダマやウルトラハンコに変更することができませんでした。
今回はコードを改良してナイスダマなどにも変更できるようにします。
ただ、15, 16 はパッと見た感じプレイヤーが使えそうなスペシャルではないのでここでは考えないものとします。
いきなりアセンブラを書くのは難しいので、まずはフローチャートから考えてみましょう。
要するに 13 のときと 19 のときで二回条件文を書けば良いことになります。
これを擬似コードで書くとこうなります。
else 節の中でまた if 文を書かなければいけないことになります。
if (X1 == 13) {
X1 = 17;
} else {
if (X1 == 19) {
X1 == 0;
} else {
X1 = X1 + 1;
}
}
これだとややこしいので、以下のように書き換えます。
if (X1 == 13) {
X1 = 17;
} else {
X1 = X1 + 1;
}
if (X1 == 20) {
X1 = 0;
} else {
X1 = X1
}
つまり、最初の IF 文で 17 にするか、1 を足すかを判定し、次の IF 文で 1 足された数が 20 かどうか(さっきまでの値が 19 だったかどうか)を判定しするというわけです。
こうすれば IF 文が入れ子にならないので簡単にかけます。
::: tip IF 文(条件文の入れ子)について
入れ子になった条件文は B(ジャンプ)命令などを使わないと書けないと思う、多分。
:::
まず、最初の IF 文は次のように書けます。
LDR X1, [X0, #0x450]
CMP X1, #13
ADD X2, X1, #1 // X2 = X1 + 1
MOV X3, #17 // X3 = 17
CSEL X1, X3, X2, EQ // X1 = X1 == 13 ? X3 : X2
X1 = X1 == 13 ? X3 : X2
の部分はX2 = X1 + 1, X3 = 17
ということがわかっているので、X1 = X1 == 13 ? 17 : X1 + 1
と解釈できるので「13 以外なら 1 を足し、13 なら 17 にする」というコードが実装できます。
次の IF 文は「20 以下ならそのまま、そうでないなら 0」というコードを書けば良いです。
0 にするのはゼロレジスタである XZR が使えるので、
CMP X1, #20
CSEL X1, X1, XZR, LO // X1 = X1 < 20 ? X1 : XZR
と書くことができます。
つまり、これら二つをまとめると以下のコードになります。
LDR X1, [X0, #0x450]
CMP X1, #13
ADD X2, X1, #1
MOV X3, #17
CSEL X1, X3, X2, EQ
CMP X1, #20
CSEL X1, X1, XZR, LO
コードを書く
ここまでをまとめると次のようにかけることになります。
0104C94C STP X29, X30, [SP, #-0x10]!
0104C950 MOV X29, SP
0104C954 ADRP X0, #0x01CB1000
0104C958 LDR X0, [X0, #0xCF8]
0104C95C LDR X0, [X0]
0104C960 BL #0x9A3CC
0104C964 LDR X1, [X0, #0x450]
0104C968 CMP X1, #13
0104C96C ADD X2, X1, #1
0104C970 MOV X3, #17
0104C974 CSEL X1, X3, X2, EQ
0104C978 CMP X1, #20
0104C97C CSEL X1, X1, XZR, LO
0104C980 STR X1, [X0, #0x450]
0104C984 NOP
0104C988 NOP
0104C98C NOP
0104C990 LDP X29, X30, [SP], #0x10
0104C994 RET
あとはこれをOnline ARM to HEX Converterに突っ込んであげればおしまいです。
::: danger BL 命令がズレる件について
Online ARM to HEX Converterで BL 命令を変換するとナゾのオフセットがついて変換したコードがバグってしまうので BL 命令だけは必ず単体で変換するようにしましょう。
:::
// Change Special by Signal (5.4.0) [tkgling]
@disabled
0104C94C FD7BBFA9 // STP X29, X30, [SP, #-0x10]!
0104C950 FD030091 // MOV X29, SP
0104C954 80E500B0 // ADRP X0, #0x01CB1000
0104C958 007C46F9 // LDR X0, [X0, #0xCF8]
0104C95C 000040F9 // LDR X0, [X0]
0104C960 F3680294 // BL #0x9A3CC
0104C964 012842F9 // LDR X1, [X0, #0x450]
0104C968 3F3400F1 // CMP X1, #13
0104C96C 22040091 // ADD X2, X1, #1
0104C970 230280D2 // MOV X3, #17
0104C974 4100839A // CSEL X1, X3, X2, EQ
0104C978 3F5000F1 // CMP X1, #20
0104C97C 21309F9A // CSEL X1, X1, XZR, LO
0104C980 012802F9 // STR X1, [X0, #0x450]
0104C984 1F2003D5 // NOP
0104C988 1F2003D5 // NOP
0104C98C 1F2003D5 // NOP
0104C990 FD7BC1A8 // LDP X29, X30, [SP], #0x10
0104C994 C0035FD6 // RET
ナイス玉やハンコへの切り替え、センパイキャノンも撃ててますね。
IF 文の書き方まとめ
三項演算子 | アセンブラ | IF 文 |
---|---|---|
Xd = X1 == X2 ? Xm : Xn | CMP X1, X2 CSEL Xd, Xm, Xn, EQ | IF (X1 == X2) { Xd = Xm } ELSE { Xd = Xn } |
Xd = X1 != X2 ? Xm : Xn | CMP X1, X2 CSEL Xd, Xm, Xn, NE | IF (X1 != X2) { Xd = Xm } ELSE { Xd = Xn } |
Xd = X1 <= X2 ? Xm : Xn | CMP X1, X2 CSEL Xd, Xm, Xn, LE | IF (X1 <= X2) { Xd = Xm } ELSE { Xd = Xn } |
Xd = X1 < X2 ? Xm : Xn | CMP X1, X2 CSEL Xd, Xm, Xn, LS | IF (X1 < X2) { Xd = Xm } ELSE { Xd = Xn } |
Xd = X1 >= X2 ? Xm : Xn | CMP X1, X2 CSEL Xd, Xm, Xn, GE | IF (X1 >= X2) { Xd = Xm } ELSE { Xd = Xn } |
Xd = X1 > X2 ? Xm : Xn | CMP X1, X2 CSEL Xd, Xm, Xn, GT | IF (X1 > X2) { Xd = Xm } ELSE { Xd = Xn } |
演算子の向きはどちらか一方だけ覚えておけばもう一方はレジスタを入れ替えるだけなので覚えなくても大丈夫だったりします。
課題
以下のコードと同様の効果を持つコードを別のアセンブラで実装してください。
0104C968 3F3400F1 // CMP X1, #13
0104C96C 22040091 // ADD X2, X1, #1
0104C970 230280D2 // MOV X3, #17
0104C974 4100839A // CSEL X1, X3, X2, EQ
0104C978 3F5000F1 // CMP X1, #20
0104C97C 21309F9A // CSEL X1, X1, XZR, LO
命令数は増えても減ってもいいですが、空き命令があと三つしかないので最大九命令までとします。
回答例
0104C974 4110839A // CSEL X1, X2, X3, NE
とすれば条件文と中身のどちらも反転させるので「反転の反転」で元のコードと同じ効果を持ちます。
記事は以上。