ぐだぐだ低レベルプログラミング(31) RISC-V、ADDとSUBも凸凹じゃけんの

Joseph Halfmoon

前回は、論理演算 and命令を使って、オペランドによって生成される命令がRV32Iだったり、RV32Cになったりするのを目にしました。今回は算術演算 add と sub です。RISC-Vに「直交的な」エンコーディングを想像してはなりませぬ。今回は2つの命令の「割り切った」関係を観察したいと思います。割り算じゃないけど。

※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら

    • RISC-Vの公式ドキュメンテーションはこちら
    • 実験に使用しているGD32VF103VBT6開発ボードはこちら
今回のポイント

RISC-Vの基本命令セットである、RV32Iの add, sub は3オペランド形式であり、第1のレジスタと第2のレジスタの間で 32ビット幅のaddまたはsubをおこなって第3のレジスタに書き込むことができます。レジスタ上ではバイト、ハーフワードといったデータ幅の指定はありません。また、addの第2のレジスタの代わりに即値(イミーディエイト値)をとることも可能です。ここで

sub命令には即値を取るものはない

です。即値12ビット幅ですが、常に符号拡張されて32ビットとされます。負の値を add すれば sub じゃん、という割り切りだと思います。

また、16ビット幅に命令を縮小するRV32C命令拡張(ArmにおけるThumbみたいなもの)が使える場合、アセンブラは以下のルールで勝手に命令を縮小してくれたりします。

    1. 2オペランド形式で表現可能な add はどのレジスタ間でもRV32C化
    2. 単一レジスタに5ビット幅で表現可能な即値を add する場合はRV32C化
    3. 2オペランド形式で表現可能な sub のうち、x8-x15レジスタ間で処理される場合はRV32C化

これまた割り切りで、命令エンコードの空間圧縮のため add 優遇、sub 冷や飯という感じですが、まあ使用のシーンを考えると合理的なのでしょう。

今回実験するアセンブラ関数

今回の実験に使ったアセンブラ関数を以下に示しました。必要なソースの全文は第28回に掲載したのでそちらもご参照ください。

以下のコードは4個の引数の間に、意味無しの add, sub を繰り返した結果の値1個を返すだけのものです。RISC-VのABI上、t0, t1はテンポラリ、a0, a1, a2, a3は引数(a0は32ビット返り値)で、いずれも呼び出される側ではセーブ不要のレジスタです。なおt0, t1はx5, x6の、a0~a3はx11~x13レジスタの別名です(レジスタとその別名の一覧は第29回。)上に記したルールを適用すれば、以下の命令のそれぞれが、32ビット幅命令になるのか、16ビット幅命令になるのか分かると思います。

addsub1:
    addi    sp,sp,-16
    sw      ra, 12(sp)
// --- Under test ---
    add     t0, a0, a1
    sub     t1, a2, a3
    addi    t0, t0, 1000
    addi    t1, t1, -1
    add     t0, t0, t1
    sub     t1, t1, t0
    sub     a2, a2, a3
    add     a0, t0, t1
// --- End of test ---
    lw      ra, 12(sp)
    addi    sp,sp,16
    ret

以下が、アセンブルした結果を objdump(RISC-V用)で確かめたものです。予想通りになっていますかね?

080007b4 <addsub1>:
 80007b4:	1141                	addi	sp,sp,-16
 80007b6:	c606                	sw	ra,12(sp)
 80007b8:	00b502b3          	add	t0,a0,a1
 80007bc:	40d60333          	sub	t1,a2,a3
 80007c0:	3e828293          	addi	t0,t0,1000
 80007c4:	137d                	addi	t1,t1,-1
 80007c6:	929a                	add	t0,t0,t1
 80007c8:	40530333          	sub	t1,t1,t0
 80007cc:	8e15                	sub	a2,a2,a3
 80007ce:	00628533          	add	a0,t0,t1
 80007d2:	40b2                	lw	ra,12(sp)
 80007d4:	0141                	addi	sp,sp,16
 80007d6:	8082                	ret

念のため、Cのmain関数のアセンブラ関数を呼び出す付近も以下に書いておきます。

int main( void ) {
    int a0, a1, a2, a3;
    int result;
//~途中略~
        a0 = 1;
        a1 = 2;
        a2 = 31;
        a3 = 15;
        result = addsub1(a0, a1, a2, a3);
        printf("andsub1   : %d\n",result);
アセンブラ関数の実行

アセンブラ関数を呼び出す行にブレークポイントを張って止めたところが以下です。左下にt0~a3までのレジスタが見えています。

StartAddSubステップインしてアセンブラ関数の中に飛び込んだところが以下です。この時点で引数の値がレジスタa0, a1, a2, a3(Cでの引数変数名は対応が分かりやすいように「あわせて」あります)に置かれているのが分かるかと思います。

FuncArgs最初のadd命令を実行した直後がこちら、a0+a1の結果が、t0レジスタに入っているのが分かるかと思います。

FirstAddつぎのsub命令を実行した直後が以下です。a2 – a3の結果がt1レジスタに格納されています。

FirstSub

即値addをつかって t0に1000を足した直後がこちら。1000=0x3E8です。t0の元の値が3だったので、結果は1003(0x3EB)。

FirstAddi続いて即値add再び。こちらは-1なので狭いビット幅で表現可能なので短いRV32C命令に変換されてます。

SecondAddi次はテンポラリレジスタ間のadd。こちらのaddはRV32C化されています。

SecondAdd次のsub は上のadd同様にテンポラリレジスタ間なのですが、対応するRV32C命令が無いので、RV32Iのままです。

SecondSubその後 sub もう1回。オペランドが a2とa3なので、この組み合わせだとRV32C化が可能っと。

ThirdSub最後、どうでも良い結果ですが、戻り値を a0に 書き込んだところが以下に。

ThirdAdd以上、ステップ・バイ・ステップでアセンブラを追いました。Cの上位層の実行結果(USBシリアルに出力される)はこんな感じ。予定どおり。

LOOP : 0
andsub1 : 15

これで、add (前々回でやったとおり mv の実体でもある)と sub は一応できましたかね。次はシフトあたり?

ぐだぐだ低レベルプログラミング(30) RISC-V、AND命令に隠された?凸凹 に戻る

ぐだぐだ低レベルプログラミング(32) RISC-V、RV32Iシフトあれどもローテイト無 へ進む