前回、RISC-Vのアセンブラを再開できたので、今回からは1命令づつ命令を動かしていきたいと思います。最初はMOVE(RISC-VのニーモニックではMV)命令です。単なるレジスタ間の転送。しかし、そこにある「仕掛け」をみるとRISC-Vのやり方というものが理解できる感じがします。単なるMOVE、されどMOVEか。クセが強いのう。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
(RISC-Vの公式ドキュメンテーションはこちら)
まず、アセンブラを書くにあたって、最低線、汎用(整数)レジスタ名くらいは覚えておかないと良く分からなくなるので以下に表を貼り付けておきます。全ての基本、32ビットのRV32I命令セットで32ビット幅32本です。64ビットのRV64になると64ビット幅になりますがまったく同様です。頭にxをつけて数字の0から31を付けたお名前で呼ぶことができます。
x0だけが「定数0」専用です。その他は汎用、どう使うかはプログラマ次第ではありますが、ABIというお約束が存在します。ABIにそってレジスタを使わないとコンパイラやらリンカやらを当てにできませぬ。ABIに沿った使い方をする場合、無意味なx0..x31というレジスタ名を直接使うより、以下のテーブルのABI名を使う方が記憶するのに優しいです。
例えばテンポラリレジスタ(呼び出される側で勝手に使って良い、セーブ不要)はt、関数引数レジスタ(アーギュメント)はaという具合に頭文字を見れば使い方のお約束が分かります。なお、tもaもsも連続したアサインになっておらず、離れたところに置かれているのは、後でこれが意味を持ってくるためです。
とりあえず今回は、引数の1番目のa0レジスタとテンポラリの2番目のt1レジスタを使いますので宜しく。
RISC-V の整数レジスタ名とABI名
レジスタ名 | ABI名 | 役割 |
---|---|---|
x0 | zero | 定数0 |
x1 | ra | 戻りアドレス |
x2 | sp | スタックポインタ |
x3 | gp | グローバルポインタ |
x4 | tp | スレッドポインタ |
x5-x7 | t0-t2 | テンポラリ |
x8 | s0/fp | 保存レジスタ/フレームポインタ |
x9 | s1 | 保存レジスタ |
x10-x15 | a0-a5 | 関数引数, a0,a1は戻値兼用 |
x16-x17 | a6-a7 | 関数引数 |
x18-x27 | s2-s11 | 保存レジスタ |
x28-x31 | t3-t6 | テンポラリ |
mv rd, rs1 レジスタ間転送命令、rs1の内容をrdにコピーする
1命令目から腰を折るような話ですが、mv (move) という「転送」命令は基本命令セットであるRV32Iには存在しません。mvはアセンブラがプログラマに便宜をはかってくれる「疑似命令」で以下のRV32I命令にアセンブラが翻訳することになっています。
addi rd, rs1, 0
上記はrs1(ソース1)で指定される汎用レジスタの値に、即値0を加えて、rd(デスティネーション)で指定される汎用レジスタに格納する、という3オペランドの加算命令です。0加算なので結果的には転送したことになります。
これがRV32I命令セットでのmvの解釈なのですが、GD32VF103の場合、基本となるRV32I命令に加えて、RV32C「圧縮命令」拡張(圧縮を拡張するというのは妙な言い方ですが)を備えています。RV32Iは32ビット幅の命令ですが、RV32Cは16ビット幅のコンパクトな命令セットです。ArmでいうThumbみたいなもん(ちょっと違うですが。)RV32Iの命令のうち、RV32Cで記述可能な命令が、これまたアセンブラが気を聞かせてRV32Cで書き換えてくれるということになっています。全てのRV32C動作は等価なRV32I命令があるので、プログラマが明示的にRV32C命令を書くことはないと思います。RV32Iのmv「疑似」命令は、RV32Cの16ビット幅の以下の命令に翻訳されます。
c.mv rd, rs2
こちらではmvが存在するのね。でもこれはエンコードの上だけで、通常のインプリの内部でハードウエアが実行するのは addi 命令みたいです。ちょっと込み入った関係だな。
生成されたオブジェクトコードを観察するために、RV32Iのaddi命令とRV32Cのc.mv命令の機械語のエンコディーングを控えておきます。
32ビット幅命令
31 | 19 | 14 | 11 | 6 | |
---|---|---|---|---|---|
addi rd, rs1, 0 | immediate[11:0] | rs1 | 000 | rd | 0010011 |
16ビット幅命令
15 | 12 | 11 | 6 | 1 | |
---|---|---|---|---|---|
c.mv rd, rs2 | 100 | 0 | rd | rs2 | 10 |
mv rd, rs1 サンプルプログラムとそのオブジェクトコード
さて、mvの実験用に追加したアセンブラ関数 mv1() は以下です。前回ソース全ファイルを掲げたので、実験用のアセンブラソースに加えた関数のみ記載します(ヘッダファイルとかは適宜よろしく。)
mv1: addi sp,sp,-16 sw ra, 12(sp) rdcycle t0 mv t1, a0 addi t1, a0, 0 rdcycle a0 sub a0, a0, t0 lw ra, 12(sp) addi sp,sp,16 ret
最初の関数プロローグなどのお約束ごとは飛ばし、上記で見るところは、4番目の mv 命令と、5番目の addi 命令です。4番目は疑似命令 mv を使っており、5番目の命令は、RV32Iであれば該当の mv 命令が翻訳されるであろう、32ビットのaddi命令です。
これをアセンブルした結果を riscv-nuclei-elf-objdump (ツールチェーンのディレクトリにRISC-V用のgccなどと並んでRISC-V用のGNU binutilsのコマンド群がインストールされている筈)を使って逆アセンブル してみると以下のようです。
08000792 <mv1>: 8000792: 1141 addi sp,sp,-16 8000794: c606 sw ra,12(sp) 8000796: c00022f3 rdcycle t0 800079a: 832a mv t1,a0 800079c: 00050313 mv t1,a0 80007a0: c0002573 rdcycle a0 80007a4: 40550533 sub a0,a0,t0 80007a8: 40b2 lw ra,12(sp) 80007aa: 0141 addi sp,sp,16 80007ac: 8082 ret
先ほどの mv 命令も addi 命令も逆アセンブルの結果は “mv t1, a0” と同じ表記をされています。しかし、生成されたオブジェクトコードを見ると、最初の mv は 0x832a と16ビット幅でRV32Cになっており、2番目のmv は 0x00050313と32ビット幅でRV32Iです。
同じ命令について複数のエンコードがありえるし、また、混在もできる、です。その割にオペコード空間は余裕のよっちゃんというのがRISC-Vの特徴かと思います。エンコードは異なってもハードの動作は一緒。
mv rd, rs1 サンプルプログラムの動作
さて、Cのメインプログラムから作成した mv1関数を呼び出して動作させてみます。デバッグモードで起動したところはアイキャッチ画像に掲げました。mv1関数を呼び出している行にブレークポイントを置いて走らせたところがこちら。
ここから「ステップイン」を使って1命令づつ実行してみます。まず、アセンブラ記述の関数mv1に入ったところが下です。関数のプロローグとサイクルカウンタの読み取りがありますが、そこは置いておいて、引数「1」がa0レジスタに置かれていることをご確認ください。転送先のt1レジスタには0x80000000という「以前の値」が残されていました。
第1ターゲットの16ビット幅の ”mv” 命令の実行直前がこちら。
第1ターゲット実行後が以下に。ちゃんと t1レジスタに転送されました。
続いて第2ターゲットの32ビットの addi 命令(mv)の実行後が以下に。同じ操作を繰り返しているだけなので、レジスタに変化はありませぬ。