simotin13's message

simotin13といいます。記事の内容でご質問やご意見がありましたらお気軽にコメントしてください\^o^/

RISC-Vのアセンブラについて調べてみた

昨日はFreedom Studioでデバッグする方法について書きましたが今日はRICV-Vに慣れるためにレジスタアセンブラについて少し調べてみました。
mcommit.hatenadiary.com

概要

HiFive1 Rev.Bで使用されているCPUはFE310-G002。命令セットはRV32IMACというISAらしい。
名前を知ったところで現時点では何も分からない。

よく出てくる命令

とりあえずCのコードの逆アセを眺めて出てきた命令を列挙してみました。

命令 意味(英語) 処理
jal Jump and Link 関数呼び出し
add Add 加算(レジスタ)
addi Add Immediate 加算(直値)
sw store word メモリへのストア(word)
sd store double メモリへのストア(double)
li load Immediate レジスタへのロード

jal

X86のCALL、ARMのBL(Branch with Link)命令相当。
RISC-Vでは$raがARMのリンクレジスタに相当するらしい。

add, addi

レジスタを使った加算はadd。直値を使う場合はaddiになる。
レジスタか直値で命令が違うのはあまりみたことがなかった。
デコードするときに扱える値の範囲が変わるというメリットはあるかもしれない。

sw,sd

オフセット(レジスタ名)
例). 8($sp)
のような表記になります。一度覚えてしまうとわかりやすい表現方法のような気がします。

gcc の冗長さ

最適化をかけないと関数の入り口と出口で、使おうが使うまいが愚直にプロローグコードとエピローグコード(ローカル変数用領域の確保と解放)を出力しています。
また reutrn 0 や return 1 のような直値を返す関数であっても一度a5レジスタを介してa0に書き込みをしています...
MIPS流のお作法なのかもしれないがさすがに冗長ですよね...

なお、この状況なコードは -O1 の最適化をつけると一瞬にして素直なコードになりました。


直値のエンコードの謎

Cで0を返すコード(return 0;)をコンパイルしてみました。
0を返す機械語が4501になっていたので不思議に思い0~5までの直値を返す関数のアセンブラを見てみました。

int return_zero(void)
{
    return 0;
}

int return_one(void)
{
    return 1;
}

int return_two(void)
{
    return 2;
}

int return_three(void)
{
    return 3;
}

int return_four(void)
{
    return 4;
}

int return_five(void)
{
    return 5;
}

結果、

0000000000000002 <return_zero>:
   2:	4501                	li	a0,0
   4:	8082                	ret

0000000000000006 <return_one>:
   6:	4505                	li	a0,1
   8:	8082                	ret

000000000000000a <return_two>:
   a:	4509                	li	a0,2
   c:	8082                	ret

000000000000000e <return_three>:
   e:	450d                	li	a0,3
  10:	8082                	ret

0000000000000012 <return_four>:
  12:	4511                	li	a0,4
  14:	8082                	ret

0000000000000016 <return_five>:
  16:	4515                	li	a0,5
  18:	8082                	ret

0→4501を開始として、1:4505 2:4509 3:450d ... と直値は4ずつ増えてエンコードされているようです。
どこまでつづくのかは分かりませんが、ぱっと見た限りでは、4 * n + 1 がエンコードされた値になります。

ちなみに、関数の戻り値は a0 レジスタで受け渡しするようです。

圧縮命令ってなんだ?


li a0, 1

を tmp.sというファイルに保存して、riscv64-unknown-elf-asでアセンブルしてみました。objdumpで見てみると、上記のC言語からコンパイルしたコードとは違うコードが生成されました。
上記のC言語からコンパイルしたアセンブラでは16bitでエンコードされています。
ところがアセンブラエンコードされたコードは同じニーモニックにも関わらず 4byteでエンコードされています。

調べてみるとRISC-Vには圧縮命令という命令セットがあり、短い命令長でエンコードされるようです。雰囲気的にはARMのThumb命令と似たような感じでしょうか...
ただし、ARMの場合は命令のアラインメント位置でThumb命令かどうかの判別ができますがRISC-Vの場合ぱっと見では32bit命令なのか圧縮命令なのか分かりませんね。

つまり、上記の

0000000000000002 <return_zero>:
   2:	4501                	li	a0,0
   4:	8082                	ret

のような2byteでエンコードされた命令は圧縮命令になります。

見比べてみた限りでは、li命令は 32bit命令では 0513 が下位ワードに来るようですので1ワードの特定のビットでスムーズにデコードできるようになっているんだと思います。
圧縮命令は4501や4505なので14bit目のようにも見えます。この辺は真面目に命令セットの仕様を見てみないとよくわかりません。

直値が4ずつ増えるのも圧縮命令と関係がありそうな気もします。

とりあえず手元のモナリザ本を開いてみると7章にRV32C:圧縮命令の章がありました。

RISC-V原典  オープンアーキテクチャのススメ

RISC-V原典 オープンアーキテクチャのススメ

今日は疲れたので、明日はこの章と第2章のRV32I:RISC-V基本整数ISAの章を読んでみたいと思います。