前回に続きARM64の算術演算命令のエイリアスを動かしていきたいと思います。テーマはNegate、「符号反転」命令です。「2の補数」世界なので符号ビットの操作では終わりませんよ。上の表をみていただけばお分かりのとおり、NEG、NEGS、NGC、NGCSと4命令もあります。まあみんなSUB系命令のエイリアスですが。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
※動作確認は普及価格帯のAndroidスマホで行っています。Cortex-A73/Cortex-A53 各4コアの bigLITTLE 、64ビット動作です。Android上にインストールしたTermux環境にWindowsPCからSSH接続し、clang/llvmのツールチェーンでビルドしております。
コンディションコード
本筋からちょっと離れますが、Armのアセンブラでコンディションコードが表になっているページがあったので以下にリンクを貼っておきます。
ARM Compiler toolchain Assembler Reference Version 5.02 >> Condition code
最近、フラグを扱う命令などの実験も多く、結構「なんだったけ?」となって適当に書くとバグったりするためです。上記はArm純正のツールチェーンのドキュメンテーションですが、当方使用中の処理系でも同じみたいです。
Negate
Negate操作といったら「反転して1を足す」という操作が直ぐに頭に浮かぶ人は飛ばしてください。2の補数表現(2の補数も知らない人はWebで調べてね)での符号反転操作です。上の「反転して1を足す」は典型的な操作例ですが、これだけでなく符号反転の方法は複数あります。今回ARM64命令で採用されているのは、「ゼロから反転前の値を引く」という方法です。0から1引けばマイナス1っと。
ARM64命令ではゴージャスにも4オペコード(実際にはエイリアス)も割り当てられてます。
-
- NEG ソースレジスタを符号反転してデスティネーションへ
- NEGS ソースレジスタを符号反転してデスティネーションへ、ついでに反転結果をフラグに反映
- NGC ソースレジスタとキャリーフラグを反転したものを足した結果を符号反転してデスティネーションへ
- NGCS ソースレジスタとキャリーフラグを反転したものを足した結果を符号反転してデスティネーションへ、ついでに反転結果をフラグに反映
上記すべては、該当するSUB命令において、ゼロレジスタ(ARM64ではレジスタ番号31番がゼロレジスタです)から反転する値が入ったソースレジスタを引くという計算に帰着させています。たとえば
neg w0, w1
というエイリアス命令の実体は以下のSUB命令です。
sub w0, wzr, w1
実験に使用したアセンブラ・ソース
例によって、1命令=ほぼ1アセンブラ関数というスタイルでテスト用のコードを書いています。
-
- ARM64の32ビットレジスタ命令 W と64ビットレジスタ命令 X を並べてあります。
- neg, negs, ngc についてはオブジェクトコードの確認用に エイリアス表記でないSUB系命令を使った表現とエイリアス表記を並べてあります。ngcsはSUB系表記は抜けてます。
- キャリーを含めた計算命令では、事前にキャリーをセットしています。
- 結果をフラグに反映させる命令では、フラグがMI状態「マイナスまたはネガティブ」であれば1を返し、そうでなければ0を返すようにしています。
ソースコードが以下に。
.globl negW, negX, negsW, negsX, ngcW, ngcX, ngcsW, ngcsX .text .balign 4 negW: sub w0, wzr, w1 neg w0, w1 ret negX: sub x0, xzr, x1 neg x0, x1 ret negsW: subs w0, wzr, w1 negs w0, w1 cset w0, MI ret negsX: subs x0, xzr, x1 negs x0, x1 cset x0, MI ret ngcW: sub w0, wzr, w0 subs wzr, w0, #1 sbc w0, wzr, w1 ngc w0, w1 ret ngcX: sub x0, xzr, x0 subs xzr, x0, #1 sbc x0, xzr, x1 ngc x0, x1 ret ngcsW: sub w0, wzr, w0 subs wzr, w0, #1 ngcs w0, w1 cset w0, MI ret ngcsX: sub x0, xzr, x0 subs xzr, x0, #1 ngcs x0, x1 cset x0, MI ret
上のソースでは、SUB系命令としての表記とエイリアス表記を並べてありました。実際にアセンブル後のオブジェクトを逆アセンブルして確認したもの(neg, negs)が以下に。
実験に使用したC言語ソース
上記のアセンブリ言語ソースで定義された関数を呼び出して結果を観察するためのC言語コードが以下に(テストとしては各1例でザルも甚だしいものですが、動くケースの存在証明、ということで。)
#include <stdio.h> extern int32_t negW(int32_t, int32_t); extern int64_t negX(int64_t, int64_t); extern int32_t negsW(int32_t, int32_t); extern int64_t negsX(int64_t, int64_t); extern int32_t ngcW(int32_t, int32_t); extern int64_t ngcX(int64_t, int64_t); extern int32_t ngcsW(int32_t, int32_t); extern int64_t ngcsX(int64_t, int64_t); int main(void) { int32_t result; int64_t resultX; result = negW(0, 1); printf ("negW(0, 1): %d\n", result); resultX = negX(0, 1); printf ("negX(0, 1): %ld\n", resultX); result = negsW(0, 1); printf ("negsW(0, 1): %d\n", result); resultX = negsX(0, 1); printf ("negsX(0, 1): %ld\n", resultX); result = negsW(0, 0); printf ("negsW(0, 0): %d\n", result); resultX = negsX(0, 0); printf ("negsX(0, 0): %ld\n", resultX); result = ngcW(0, 0); printf ("ngcW(0, 0): %d\n", result); resultX = ngcX(0, 0); printf ("ngcX(0, 0): %ld\n", resultX); result = ngcsW(0, 0); printf ("ngcsW(0, 0): %d\n", result); resultX = ngcsX(0, 0); printf ("ngcsX(0, 0): %ld\n", resultX); return 0; }
ビルドして実行
clang / llvm 処理系でのビルド時のコマンドラインが以下に(処理系がインストールされていれば、clang を gcc に換えても大丈夫だと思います。知らんけど。)
$ clang -g -O0 -o neg neg.c neg.s
実行結果が以下に。なお、negsW/negsXが2つづつあるのは、「ちゃんとフラグに反映されているのだよね」の確認です。
$ ./neg negW(0, 1): -1 negX(0, 1): -1 negsW(0, 1): 1 negsX(0, 1): 1 negsW(0, 0): 0 negsX(0, 0): 0 ngcW(0, 0): -1 ngcX(0, 0): -1 ngcsW(0, 0): 1 ngcsX(0, 0): 1
予定通りの結果が得られましたな。