前回ようやく終わったadd系(足し算)の次はsub系(引き算)かというと似たことを繰り返しても仕方ないなと思いました。そこで算術演算命令の中で「微妙な」cmp系へ行きたいと思います。RISCあるあるの「実はcmp命令なんて無い」というオチの命令群です。勿論ちゃんと動作します。裏ではsub系命令が暗躍?していますぞ。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
※動作確認は普及価格帯のAndroidスマホで行っています。Cortex-A73/Cortex-A53 各4コアの bigLITTLE 、64ビット動作です。Android上にインストールしたTermux環境にWindowsPCからSSH接続し、clang/llvmのツールチェーンでビルドしております。
エイリアス、CMP、CMN命令
さて、RISCあるあるの「実はそんな命令はない」の件ですが、以前やったRISC-Vでは「疑似命令(日本語の場合)」と呼んでいました。個人的には「オブジェクトコードに落ちる命令を疑似命令と呼ぶのはなんだかな~」と思っていました。私は「疑似命令というのはアセンブラに対する指示」派です。その点、Armの場合の呼び方はしっくりきます。
エイリアス
です。別名、あだ名みたいなもん?そして正面からとりあげなかったSUB系の命令群はエイリアスの宝庫?です。以下の表をご覧ください。左端に書かれているオペコードがエイリアス名です。cmp, cmn, neg, negs, ngc, ngcsと6種類もあります。それに対して横の列の上端に書かれているのがADD/SUB系の「実在する」命令です。交点にRなんとかと書かれているところがエイリアスの実体をどうしたら作れるか略記したものです。今回実験するCMP命令の場合だと、「SUBSのデスティネーションレジスタ(Rd)に31番レジスタを選ぶとCMPに」なります。ここで
ARM64の31番レジスタはゼロレジスタ
です。RISC-Vでは0番がゼロレジスタで分かり易かったですが、Armは同じにしたくなかったのか31番がゼロです。ゼロレジスタは読み出せば常にゼロですが、書き込めば全てを飲み込むブラックホール(というかヌル)です。つまり今回とりあげる命令は、
-
- CMPは、SUBSの引き算結果を捨てる
- CMNは、ADDSの足し算結果を捨てる
という操作に他なりません。算術計算の結果は捨てますが、フラグは残ると。
今回実験するアセンブラ・コード
今回はエイリアスなので、殊更に同じ機械語命令に落ちる異なるアセンブリ言語表記を並べてあります。たとえば、最初の例
subs wzr, w1, #1
と
cmp w1, #1
は同じ命令です。w1レジスタの内容と即値1を比べてフラグを立てる命令です。実際にはw1レジスタから即値1を引き算しており、結果を wzr(32ビット幅のゼロレジスタ)に捨てています。
Cの上位関数にフラグの結果を戻すのに、CSET命令を使っています。指定条件が真なら1、偽なら0を返す命令です(これまた他の命令のエイリアスです。)
.globl cmpW, cmpX, cmnW, cmnX .text .balign 4 cmpW: subs wzr, w1, #1 cmp w1, #1 cset w0, EQ ret cmpX: subs xzr, x1, #1 cmp x1, #1 cset x0, EQ ret cmnW: adds wzr, w1, #1 cmn w1, #1 cset w0, EQ ret cmnX: adds xzr, x1, #1 cmn x1, #1 cset x0, EQ ret
上記のアセンブリ言語ソースをアセンブルした結果を、逆アセンブルして確認してみました。別表記のソースからまったく同じ機械語コードが生成されているのが分かると思います。逆アセンブラはcmp/cmn表記がお好みみたいです。こんな感じ。
駆動用のC言語ソース
上記のアセンブラソースを駆動してテスト結果を表示するためのC言語のメイン関数が以下です。真なら1、偽なら0が出力されるっと。
#include <stdio.h> extern int32_t cmpW(int32_t, int32_t); extern int64_t cmpX(int64_t, int64_t); extern int32_t cmnW(int32_t, int32_t); extern int64_t cmnX(int64_t, int64_t); int main(void) { uint32_t result; uint64_t resultX; result = cmpW(0, 1); printf ("cmpW(0, 1): %d\n", result); result = cmpW(0, 2); printf ("cmpW(0, 2): %d\n", result); resultX = cmpX(0, 1); printf ("cmpX(0, 1): %ld\n", resultX); resultX = cmpX(0, 2); printf ("cmpX(0, 2): %ld\n", resultX); result = cmnW(0, -1); printf ("cmnW(0, -1): %d\n", result); result = cmnW(0, 2); printf ("cmnW(0, 2): %d\n", result); resultX = cmnX(0, -1); printf ("cmnX(0, -1): %ld\n", resultX); resultX = cmnX(0, 2); printf ("cmnX(0, 2): %ld\n", resultX); return 0; }
ビルドして実行
Androidスマホ上のLinuxで、clang処理系でビルドしているのでこんな感じです。gccがインストールしてある処理系ならば、clangをgccに変えれば動くと思います。知らんけど。
$ clang -g -O0 -o arthAlias arthAlias.c arthAlias.s
実行結果は以下に。
$ ./arthAlias cmpW(0, 1): 1 cmpW(0, 2): 0 cmpX(0, 1): 1 cmpX(0, 2): 0 cmnW(0, -1): 1 cmnW(0, 2): 0 cmnX(0, -1): 1 cmnX(0, 2): 0
cmp #1は1と比べれば真、cmn #1はー1と比べれば真、それ以外は偽とな。予定どおりであります。