前回は、論理演算 and命令を使って、オペランドによって生成される命令がRV32Iだったり、RV32Cになったりするのを目にしました。今回は算術演算 add と sub です。RISC-Vに「直交的な」エンコーディングを想像してはなりませぬ。今回は2つの命令の「割り切った」関係を観察したいと思います。割り算じゃないけど。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
今回のポイント
RISC-Vの基本命令セットである、RV32Iの add, sub は3オペランド形式であり、第1のレジスタと第2のレジスタの間で 32ビット幅のaddまたはsubをおこなって第3のレジスタに書き込むことができます。レジスタ上ではバイト、ハーフワードといったデータ幅の指定はありません。また、addの第2のレジスタの代わりに即値(イミーディエイト値)をとることも可能です。ここで
sub命令には即値を取るものはない
です。即値12ビット幅ですが、常に符号拡張されて32ビットとされます。負の値を add すれば sub じゃん、という割り切りだと思います。
また、16ビット幅に命令を縮小するRV32C命令拡張(ArmにおけるThumbみたいなもの)が使える場合、アセンブラは以下のルールで勝手に命令を縮小してくれたりします。
-
- 2オペランド形式で表現可能な add はどのレジスタ間でもRV32C化
- 単一レジスタに5ビット幅で表現可能な即値を add する場合はRV32C化
- 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までのレジスタが見えています。
ステップインしてアセンブラ関数の中に飛び込んだところが以下です。この時点で引数の値がレジスタa0, a1, a2, a3(Cでの引数変数名は対応が分かりやすいように「あわせて」あります)に置かれているのが分かるかと思います。
最初のadd命令を実行した直後がこちら、a0+a1の結果が、t0レジスタに入っているのが分かるかと思います。
つぎのsub命令を実行した直後が以下です。a2 – a3の結果がt1レジスタに格納されています。
即値addをつかって t0に1000を足した直後がこちら。1000=0x3E8です。t0の元の値が3だったので、結果は1003(0x3EB)。
続いて即値add再び。こちらは-1なので狭いビット幅で表現可能なので短いRV32C命令に変換されてます。
次はテンポラリレジスタ間のadd。こちらのaddはRV32C化されています。
次のsub は上のadd同様にテンポラリレジスタ間なのですが、対応するRV32C命令が無いので、RV32Iのままです。
その後 sub もう1回。オペランドが a2とa3なので、この組み合わせだとRV32C化が可能っと。
最後、どうでも良い結果ですが、戻り値を a0に 書き込んだところが以下に。
以上、ステップ・バイ・ステップでアセンブラを追いました。Cの上位層の実行結果(USBシリアルに出力される)はこんな感じ。予定どおり。
LOOP : 0 andsub1 : 15
これで、add (前々回でやったとおり mv の実体でもある)と sub は一応できましたかね。次はシフトあたり?