ぐだぐだ低レベルプログラミング(68) ARM64(AArch64)、算術命令エイリアス#2

Joseph Halfmoon

前回に続き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オペコード(実際にはエイリアス)も割り当てられてます。

    1. NEG ソースレジスタを符号反転してデスティネーションへ
    2. NEGS ソースレジスタを符号反転してデスティネーションへ、ついでに反転結果をフラグに反映
    3. NGC ソースレジスタとキャリーフラグを反転したものを足した結果を符号反転してデスティネーションへ
    4. 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)が以下に。disasm000

ngc命令の場合が以下に。disasm001

実験に使用した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

予定通りの結果が得られましたな。

ぐだぐだ低レベルプログラミング(67) ARM64(AArch64)、算術命令エイリアス#1 へ戻る

ぐだぐだ低レベルプログラミング(69) ARM64(AArch64)、論理命令AND一族#1 へ進む