前回、RISC-Vにmv(move)命令は実は無いのだ、という衝撃?の事実をおさらいしました。今回は、and(論理積)命令です。andみたいなプリミティブな操作の命令に何か細工をする余地などあるのか?ま、当然「工夫」があるわけですが。でもね、andとorとxorは一緒、流石に。1回で3命令進捗!
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
(RISC-Vの公式ドキュメンテーションはこちら)
以下のコードはGigaDevice社GD32VF103マイコン搭載のSeeedStudio社製評価ボード上で動作確認しています。使用しているコード全文とビルド環境などは「ぐだぐだ低レベルプログラミング(28)」に掲載しておりますので、そちらをご参照ください。
今回も最初にグダグダねたを書いてしまいます。先週元気に動いていたRISC-Vデバッガに今日は接続できませんでした。原因は、Windows上のデバイスドライバでした。RISC-Vデバッガ用のドライバ、USBシリアル用からWinUSBに取り換えてあった筈が、元のシリアルに戻ってました。そういえばWindowsでUpdateかかったんだっけな。。。何かあると元に戻ってしまうのは、慣れましたけど。
RISC-Vデバッガへの接続OKになったと思ったら、今度は書き込んだプログラムからの応答がありません。調べたら、UARTのコネクタが浮いていました。しょうもない。。。ま、慣れましたけど。
今回のテストターゲットのアセンブラ・ソース
今回は and 命令の試用なので、以下のような「無駄な」コードを書いてみました。3個の引数と内部にもっている定数1個の4個の値の and をとって返すという関数です。
and1: addi sp,sp,-16 sw ra, 12(sp) // --- Under test --- mv t1, a1 andi a2, a2, -2048 and a0, a0, t1 and a0, a0, a2 // --- End of test --- lw ra, 12(sp) addi sp,sp,16 retさ
最初に、わざわざa1レジスタ(2番目の引数が置かれている)の値をt1レジスタ(テンポラリレジスタの2番目)にコピーしています。その後、a2レジスタ(3番目の引数が置かれている)と定数-2048とのandをとってa2レジスタに書き戻します。つづいて、a0レジスタ(1番目の引数がおかれている)と先ほどのt1レジスタの値との and をとってa0に書き戻します。最後にa2とa0のandをとってa0に書き戻します。a0 レジスタに置かれている値が関数戻り値となります。
どんなオブジェクトコードが生成されるのか見てみます。例によってRISC-V用のBinUtilsのobjdumpで逆アセンブルしてみます。こんな感じ。
080007ca <and1>: 80007ca: 1141 addi sp,sp,-16 80007cc: c606 sw ra,12(sp) 80007ce: 832e mv t1,a1 80007d0: 80067613 andi a2,a2,-2048 80007d4: 00657533 and a0,a0,t1 80007d8: 8d71 and a0,a0,a2 80007da: 40b2 lw ra,12(sp) 80007dc: 0141 addi sp,sp,16 80007de: 8082 ret
第1のポイントは、即値データの幅は12bit、そして「常に」符号拡張される、ということです。上記のandi命令ではソースとデスティネーションが同じですが、命令的には、ソースレジスタの値と即値とのANDをとって別なデスティネーションに格納可能です。ただし、12ビットで符号拡張という制限から、下の12ビットについては任意のビット演算が可能ですが、上の24ビットは、12ビット目の符号ビットによって0クリアされたりそのまま保存されたりと挙動が分かれることになります。
第2のポイントは、”and a0, a0, t1″ と “and a0, a0, a2” のオブジェクトコードをご覧ください。最後のレジスタの指定のみが異なるだけなのですが、前者は32ビットのRV32I命令にアセンブルされており、後者はアセンブラが気を効かせて16ビットのRV32C命令にアセンブルされています。論理演算(and, or, xor)命令の場合、以下のルールに当てはまれば16ビット化が可能です。
-
- 2オペランド形式で表現可能なこと
- 汎用レジスタx8からx15の8本のレジスタの間の演算であること
RISC-Vは基本3オペランド形式ですが、ソースとデスティネーションとの間で演算し、結果をデスティネーションに書き戻すという2オペランド形式でかけることが第1の条件です。これでオペランドのフィールド1個、5ビットを倹約できました。
2番目ですが、16ビット幅のand命令では、オペランド指定に3ビットしか割り当てられていないのです。32本のレジスタを持つRISC-Vですが、16bit幅のand命令がアクセスできるのは、「真ん中辺」の8本に限られます。x8からx15と書くと何だか分かりませんが、ABI的にはa0からa5が含まれる、というと腑に落ちるかと思います。引数レジスタの第1番目から第6番目までであり、そしてa0, a1は戻り値のレジスタでもあります。それらに対する論理演算は短縮コードで出来る。しかし他のケースは普通ね、という割り切り。これで2ビットx2か所で4ビットを倹約してます。第1での倹約の5ビットと合わせて合計9ビットも浮いたことになります。
Cからのアセンブラ関数呼び出しとその結果
ヘッダファイルへのアセンブラ関数のプロトタイプの追加は省略しました。以下のコードでアセンブラ関数and1()を呼び出せばどんな結果が返ってくるか「暗算」しておいてくだされや。
a0 = 0x5678ABCD; a1 = 0xF0FFFFFF; a2 = 0xFF0FFFFF; result = and1(a0, a1, a2); printf("and1 : %08x\n",(unsigned int)result);
結果はこのとおり、暗算結果と合ってますでしょうか。
and1 : 5008a800
アセンブラの動作をステップ・バイ・ステップで確認
最初のmv命令の直前の様子がこちら、a0, a1, a2レジスタに上記のCコードで設定した値が入っているのが見えると思います。
mvした後、最初の即値andの直前の様子が以下に。t1の値が書き換わっています。なお、tレジスタとaレジスタの値は呼び出され(コリー)側でセーブする必要はありません。
andi命令の実行結果が以下に。-2048という即値は命令コード中では0x800とコードされますが、符号拡張され 0xFFFF800 となって演算に使われます。a2レジスタの値に反映されていることが分かります。
続いて、a0とt1の間の演算結果です。こちらは32bit幅の命令です。
最後の演算で戻り値が完成。こちらは16ビット幅の命令の実行結果ですが、命令コードの長短が動作に影響するわけもなく。
andできました。同時に頭の中では or と xor もOKってか。