はじめに

x86はやはり疲れるので、一旦RiscVで進めることに方針転換した。

あと、せっかく先進的なRiscVを使うなら、他のツールも新しくしたいと思い、clangでコンパイルした。

もはやgccではないが、シリーズ化して作ってしまっているのでこのまま進める。

本題

下記のCコードをRiscV向けにコンパイルのみして、オブジェクトファイルを作成した。

riscv64-unknown-elf-objdumpで、このファイルをdisassembleした。

int main() {
  return 0;
}

コマンド

riscv64-unknown-elf-objdump --disassemble $FILE.o

眺めると、sw以外は2byteの命令になっており、圧縮命令(RVC)が多用されていることが分かる。

結果

Disassembly of section .text:

0000000000000000 <main>:
   0:   1101                    addi    sp,sp,-32
   2:   ec06                    sd      ra,24(sp)
   4:   e822                    sd      s0,16(sp)
   6:   1000                    addi    s0,sp,32
   8:   4501                    li      a0,0
   a:   fea42623                sw      a0,-20(s0)
   e:   60e2                    ld      ra,24(sp)
  10:   6442                    ld      s0,16(sp)
  12:   6105                    addi    sp,sp,32
  14:   8082                    ret

読みづらいので、手で書き換えた。

sp -= 32
[24 + sp] = ra
[16 + sp] = s0
s0 = sp + 32
a0 = 0
[-20 + sp] = a0[31:0]
ra = [24 + sp]
s0 = [16 + sp]
sp += 32

まずスタックポインタを32引く(下に進むということと思われる)。 レジスタサイズは64bit(8byte)なので、ローカル変数用に4つの値を格納できるようにメモリを確保したと言える。

sp -= 32

[sp + 24]に、今のraを保存しておく(別の関数を実行したときにraが失われないようにしておく。) retする直前に、raレジスタに戻す(つまり、メモリに一時退避するということ)。

[24 + sp] = ra

[sp + 16]に、s0を代入する。 つまり、スタックに今のs0(フレームポインタ)を保存する。関数実行の開始を表す。 retする直前に、s0レジスタに戻す。

[16 + sp] = s0

s0レジスタに、sp + 32を代入する。 新たな関数の実行が開始したことを、s0で表現する。 具体的には、今後のスタックに関する操作はs0を基準に行う。

s0 = sp + 32

a0レジスタに0を代入する。 a0は関数の返り値を表すので、このままretしたら0が返る。

a0 = 0

[s0 - 20]に、a0を代入する。

[-20 + s0] = a0[31:0]

raレジスタに、元のraの値を復元(このコードに限れば、raは変えていなかったので不要だった)

ra = [24 + sp]

同様にs0レジスタに、元のs0の値を復元(このコードに限れば、s0は変えていなかったので不要だった)

s0 = [16 + sp]

関数の実行が終わったので、spを元に戻す。

sp += 32

関数のshreturnを行い、pcレジスタへraレジスタの値をセットする。

ret

RiscVレジスタメモ

全レジスタ

pc, x0-x31(1+32で33個)

ただし、xから始まる名前のレジスタ(つまり、pc以外)にはそれぞれ別名がついている。

スタック

アドレス空間を下へ進む。関数の中で関数が実行されるごとにスタックフレームが追加され、関数から戻るとスタックフレームが取り除かれる。

sp

レジスタ番号: x2

スタックポインタ。小さい方へ伸びる。現在のスタックフレームの開始地点を表す。

関数を実行するごとに、先に下げて使うことで安全に扱える(s0と併用することで簡単に安全になる)。

s0

レジスタ番号: x8

フレームポインタ。スタックフレーム内の基準とする点。

spと一見かなり似た挙動をするが、1つののスタック内のローカル変数などスタックフレーム内の値に対する操作はs0を基準に行う。

ra

レジスタ番号: x1

return address。スタックフレームが終わるとき(つまり、おそらく大体は関数が返るとき)に、呼び出し元に戻る必要がある。

このとき、呼び出し元の処理が途中なので、適切なところに戻る必要があるが、その命令列が格納されたメモリの戻るべきアドレス位置を入れる。

別の言い方をすれば、ret実行時にpcレジスタにraレジスタの値がコピーされるので、そのときに適切な挙動になるようにraレジスタを設定する。

pc

プログラムカウンタ。現在実行している命令列のアドレスをもつ。実行のたびに32ビット、64ビットのCPU問わず、4byte増加する。

(なお、x86_64においては、命令は可変長で1-15byteらしい。RiscVは基本的に2byteか4byte。)

圧縮命令(Reduced Vector Compression, RVC)の場合は、2byteしか増えないこともあるらしい。