ぐだぐだ低レベルプログラミング(29) RISC-VでMV(MOVE)命令、本当は無い

Joseph Halfmoon

前回、RISC-Vのアセンブラを再開できたので、今回からは1命令づつ命令を動かしていきたいと思います。最初はMOVE(RISC-VのニーモニックではMV)命令です。単なるレジスタ間の転送。しかし、そこにある「仕掛け」をみるとRISC-Vのやり方というものが理解できる感じがします。単なるMOVE、されどMOVEか。クセが強いのう。

(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関数を呼び出している行にブレークポイントを置いて走らせたところがこちら。

BreakATfunc

ここから「ステップイン」を使って1命令づつ実行してみます。まず、アセンブラ記述の関数mv1に入ったところが下です。関数のプロローグとサイクルカウンタの読み取りがありますが、そこは置いておいて、引数「1」がa0レジスタに置かれていることをご確認ください。転送先のt1レジスタには0x80000000という「以前の値」が残されていました。

STEP000第1ターゲットの16ビット幅の ”mv” 命令の実行直前がこちら。

STEP001第1ターゲット実行後が以下に。ちゃんと t1レジスタに転送されました。

STEP002続いて第2ターゲットの32ビットの addi 命令(mv)の実行後が以下に。同じ操作を繰り返しているだけなので、レジスタに変化はありませぬ。

STEP003mv それも疑似命令、1個で1回か、先は長い?

ぐだぐだ低レベルプログラミング(28) RISC-Vでアセンブラ再開、環境のレストア に戻る

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