Quantumleap
4372 words
22 minutes
誰でもできるコード開発 #10
2021-05-24

誰でもできるコード開発 #10#

はじめに#

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

誰でもできるコード開発 #9

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

コードの意味を理解しよう#

今回は、昔実装した 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 などという名前がついているかというと、一つのレジスタで次の四つのフラグ情報を扱っているからである。

10
Nx < 0x >= 0
Zx = 0x != 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 のサフィックスがあります。

全部は覚えなくていいけど、いくつか覚えておくと条件文が書きやすくなります。

サフィックス意味フラグ
EQEqualZ = 1
NENot EqualZ = 0
CS/HS-C = 1
CC/LO-C = 0
MIMinusN = 1
PLPlusN = 0
VS-V = 1
VC-V = 0
HI-C = 1 AND Z =0
LS-C= 0 OR Z = 1
GEGreater EqualN = V
LTLower ThanN <> V
GTGreater ThanZ = 0 AND N = V
LELower EqualZ = 1 OR N <> V
ALAlwaysAny
NVNeverAny

これを見ると最初のアセンブラであった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 = 1C = 0Z = 1Z = 0
CMPCMP X1, X2SUBS XZR, X1, X2X1 > = X2X1 < X2X1 == X2X1 != X2
CMNCMN X1, X2ADDS XZR, X1, X2
SUBSSUBS X0, X1, X2X0 = X1 - X2X1 > = X2X1 < X2X1 == X2X1 != X2
ADDSADDS X0, X1, X2X0 = X1 + X2
NEGSNEGS X1, X2SUBS X1, XZR, X20 >= X20 < X20 == X20 != X2

簡単な条件文を書くだけなら C と Z の条件フラグだけ見れば十分です。

::: tip NZCV フラグについて

CMP 命令は NZCV フラグのうち C と Z を引数の値によって変化させる。

CMP X1, X2という命令が与えられたときX1 == X2であればZ = 1、そうでなければZ = 0となり、X1 >= X2であればC = 1X1 < 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 は以下の表のようになっています。

idName
0SuperMissile
1SuperArmor
2LauncherSplash
3LauncherSuction
4LauncherQuick
5LauncherCurling
6LauncherRobo
7WaterCutter
8Jetpack
9SuperLanding
10RainCloud
11AquaBall
12SuperBubble
13Shachihoko
14-
15RainCloudEnemy
16MissileMissilePosition
17SuperBall
18SuperStamp
19BigLaser

前回のコードではスペシャル 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 : XnCMP X1, X2
CSEL Xd, Xm, Xn, EQ
IF (X1 == X2) { Xd = Xm } ELSE { Xd = Xn }
Xd = X1 != X2 ? Xm : XnCMP X1, X2
CSEL Xd, Xm, Xn, NE
IF (X1 != X2) { Xd = Xm } ELSE { Xd = Xn }
Xd = X1 <= X2 ? Xm : XnCMP X1, X2
CSEL Xd, Xm, Xn, LE
IF (X1 <= X2) { Xd = Xm } ELSE { Xd = Xn }
Xd = X1 < X2 ? Xm : XnCMP X1, X2
CSEL Xd, Xm, Xn, LS
IF (X1 < X2) { Xd = Xm } ELSE { Xd = Xn }
Xd = X1 >= X2 ? Xm : XnCMP X1, X2
CSEL Xd, Xm, Xn, GE
IF (X1 >= X2) { Xd = Xm } ELSE { Xd = Xn }
Xd = X1 > X2 ? Xm : XnCMP 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とすれば条件文と中身のどちらも反転させるので「反転の反転」で元のコードと同じ効果を持ちます。

記事は以上。

誰でもできるコード開発 #10
https://fuwari.vercel.app/posts/2021/05/ipswitch10/
Author
tkgling
Published at
2021-05-24