RISC-VというCPUはライセンスフリーとのことでどんなもんか興味あって、そんなこともあり自作のCコンパイラをRISC-Vに対応したいと思っていた。 そのために動かせる環境を用意したくて、最初はQEMUなどのエミュレータでLinuxを動かして…と思っていたが、spikeというシミュレータを使うことで実現した。
環境構築編
以前XV6のRISCV版を動かす際に開発環境をセットアップしたがPCを買い替えてしまって環境は失われてしまっていて、またゼロから環境構築する必要があった。
RISC-V Linux on QEMU(失敗)
当初はQEMUを使ってLinuxを動かしたいと思っていたんだけど、どうにもうまくいかなかった。
公式のRunning 64- and 32-bit RISC-V Linux on QEMU — RISC-V - Getting Started Guideの手順に沿ってやってみたが、qemu実行の際にbiosを指定しろ、とエラーが出てしまい進めなかった。
わけも分からず-bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.bin
というオプションを追加してみても起動しなかった。
あれこれ自分でビルドせずに、出来合いのイメージバイナリを用意してくれていてダウンロードしてサクッと動かせないもんなんだろうか…。
シミュレータ環境の構築 (spike, pk)
行き詰まっていたところ、エミュレーション手順の説明ページを見つけた: RISC-V向けの実行ファイルをmacOSとUbuntu22.04それぞれでエミュレーション実行する方法 #homebrew - Qiita spikeがシミュレータで、pkがプロキシカーネルとブートローダー、ということらしい。
pkのビルド(RISC-V64用)
macOSの場合、brew install riscv-isa-sim
でspikeは入るらしく簡単なんだけど、
自分の環境ではpkのビルドがそのままではいかずmake
時にエラーが出てしまった。
../configure ...
でMakefileが生成されるが、その中のコンパイラ指定が
CC := clang |
となってしまい、クロスコンパイルにならなくてエラーになっていた。
これは自分が環境変数 CC
を設定してしまっていたからのようで、 CC= ../configure ...
と指定する必要があった。
以上を踏まえて、自分の全手順:
export RISCV=$HOME/rv64 # .zshrcにも追加 |
RISC-V用の実行ファイル作成
シミュレータの動作確認もかねて、クロスコンパイラでRISC-V64用のバイナリを作成して動かしてみる。 hello worldなどの適当なCのコードを用意して、
$ riscv64-unknown-elf-gcc -o hello hello.c |
とコンパイルできる。
- 出力形式はELFファイル
- その他、binutilsのツールも使える:
- 出力結果を
riscv64-unknown-elf-objdump -S hello
などとして逆アセンブルを見たりすることもできる
- 出力結果を
実行方法
spike pk
を使って、
$ spike $RISCV/riscv64-unknown-elf/bin/pk hello |
実行すると bbl loader
とかいう不要なメッセージが出力されてしまうが、オプションで抑制するやり方がわからないので、スクリプトを用意してやる:
|
コンパイラ作成編
RISC-V用のバイナリを動かせる環境ができたので、自作のCコンパイラからの出力に挑戦してみる。 これまではx86-64とaarch64のアセンブリを吐けるようにしていたが、バックエンドを追加して出力できるようにする。 バックエンドで中間表現から各アーキテクチャ用のアセンブリを出力するようにしているので、そこでRISC-Vの実際の命令を出力することで対応する。
CPUアーキテクチャの概要
RISC-Vの仕様をくまなく調べたわけではなく、大雑把に理解したところによると:
- 固定長な命令:各4バイト(コンパクト命令で2バイトというのもある)
- 汎用レジスタ:
x0
〜x31
の32個で、用途ごとに別名もついている- ゼロレジスタがある:
zero
==x0
- ゼロレジスタがある:
- 汎用レジスタで扱えるのは64ビットだけ?で、メモリへのストアやロード時に何バイトにするかで処理する?
- アドレッシングモード:レジスタ間接+オフセットのみ?
- フラグレジスタがない
- 条件分岐はレジスタ同士の比較によって行う
汎用レジスタ
レジスタ | 別名 | 用途 | 保存 |
---|---|---|---|
x0 |
zero |
常にゼロ | – |
x1 |
ra |
call の戻り先 |
Caller |
x2 |
sp |
スタックポインタ | Callee |
x3 |
gp |
グローバルポインタ | – |
x4 |
tp |
スレッドポインタ | – |
x5 〜x7 |
t0~t2 |
テンポラリ | Caller |
x8 |
s0/fp |
フレームポインタ | Callee |
x9 |
s1 |
保存 | Callee |
x10 〜x17 |
a0 〜a7 |
関数パラメータ用 | Caller |
x18 〜x27 |
s2 〜s11 |
保存 | Callee |
x28 〜x31 |
t3 〜t6 |
テンポラリ | Caller |
zero
, ra
, sp
, gp
, tp
の5個は用途が決められているので、残りの27個をレジスタ割付の対象にする
(fp
も用途は決まっているが関数がスタックフレームを使用しない場合は保存・復帰してやれば割付が可能)。
命令セット
主に利用する命令たち:
命令 | 内容 |
---|---|
mv |
レジスタ間コピー |
li |
直値ロード(疑似命令) |
add , addi |
レジスタ間または直値との加算 |
sub |
減算(直値は扱えない) |
mul , div , rem |
乗除、余り |
lb , lh , lw , ld |
メモリからのロード(1, 2, 4, 8バイト) |
sb , sh , sw , sd |
メモリへのストア(同上) |
j |
無条件ジャンプ |
beq など |
条件ジャンプ |
call , ret |
コール、リターン |
実際には疑似命令も多数あって、直値のロードはlui
命令で上位20ビット+addi
で下位12ビットをロード(それ以上の場合シフトも組み合わせる)、無条件ジャンプはjal zero, label
など、アセンブラで実際の命令に変換される。
また自動的にコンパクト命令を使ってくれる。
- 疑似命令あるんだったら
subi
も対応してくれてもいいのに…
浮動小数点数
RISC-Vでは基本命令セットを小さく抑えていて、浮動小数点数演算やレジスタはオプショナルになっている。 ハードウェア的にサポートされない場合にはソフトウェア的に処理する必要があるが、それは大変なのでハードウェア的にサポートしてるものとする。 そうした場合、浮動小数点レジスタ(FPレジスタ)や命令セットがある:
- FPレジスタ:
f0
〜f31
の32個(同じく別名あり)
主な命令:
命令 | 内容 |
---|---|
fmv.d |
レジスタ間コピー |
fadd.d , fsub.d , fmul.d , fdiv.d |
加減乗除 |
fld , fsd |
ロード、ストア |
fcvt |
値の変換(double , float , int ) |
feq など |
比較結果を汎用レジスタに取り出す |
.d
の代わりに.s
でfloat
になる(レジスタ名は変わらず)
以下、コンパイラから出力する際のトピック:
コンパイラをクロスコンパイルに対応させる
今まではターゲット環境が開発環境と同じだったので、コンパイラの定義済みラベル__x86_64__
やら__aarch64__
やらでターゲット環境を選択していた。
これを明示的に選べるようにコンパイルオプションの-D
でターゲットとなるアーキテクチャを指定できるようにして、クロスコンパイルできるようにする。
Cコンパイラの出力結果をアセンブルやリンクする実行ファイルもプレフィクスをつけて選択できる様にしてやる:riscv64-unknown-elf-as
, riscv64-unknown-elf-ld
など。
PUSH/POP
push
やpop
のようなプリデクリメントやポストインクリメントのアドレッシングがないので、自分でスタックポインタを操作+レジスタ間接ロード・ストアする。
スタックポインタは16バイトアラインされている必要がある。
条件分岐
x86-64やaarch64で条件分岐を行うにはcmp
で比較して、フラグレジスタの条件によってブランチしていた。
がRISC-Vではフラグレジスタがなく、beq
などの命令がオペランドとしてレジスタを2つ取り、その比較によって行う。
また比較の条件がeq
, ne
, lt
, ge
しかないので、le
やgt
はオペランドの順を入れ替えてやる。
FPレジスタはオペランドに与えられないので、feq
などのFPレジスタ間の比較命令でGPレジスタに1または0が得られ、それを使って分岐する。
FP比較命令はnot equalがないので、feq
ののちに条件を反転する必要がある。
標準入出力
自作コンパイラがちょっと厄介な作りになっていて、ライブラリのヘッダファイルは独自のものを使っているが、x86-64以外ではリンクはシステムのライブラリを利用している。 なので自作のヘッダでの定義がライブラリと合うようにする必要がある。
x86-64やaarch64ではstdin
, stdout
, stderr
は単純にextern FILE*
としてやればよかったが、RISC-Vではどうも違うっぽい。
単にprintf
を使う分には問題ないが、fprintf(stderr, ...)
などとするとリンクで未定義エラーが出てしまう。
プリプロセスした結果からRISC-Vツールチェインのヘッダを見てみると、
// /opt/homebrew/Cellar/riscv-gnu-toolchain/main/riscv64-unknown-elf/include/stdio.h |
となっていて、リエントラントに対応するためのコードになってるっぽい。
- これらのコードはnewlibと同じなので、RISC-Vツールチェインはnewlibを使っているっぽい
呼出規約
関数の呼出規約はx86-64やaarch64と同様にint
やdouble
などのスカラー値はレジスタ渡しで、8個を超えた分や構造体はスタック渡し。
レジスタによって呼出元保存や呼出先保存となっているのでそれに従う
(tn
レジスタがテンポラリ=破壊される=Caller Save、sn
は保存される=Callee Save)。
任意長引数
riscv64-unknown-elf-gcc
の出力結果を見ると、呼び出す関数が任意長引数の場合はdouble
の値でもFPレジスタではなく汎用レジスタで渡すっぽい。
なんでそうなってるのか推測だけどRISC-Vは構成によってはハードウェアでの浮動小数点演算なしとかもあり得るので、そういうケースを考慮してのことかもしれない。
FPレジスタからの取り出しはfmv.x.d
命令でバイナリ表現そのままGPレジスタに持ってきてやる。
またx86-64などではGPとFPそれぞれ別に管理が必要なためva_list
の実体は構造体が使われているが、
RISC-Vでは受け渡しが汎用レジスタのみということもあってか古き良き実装でありがちな単なるポインタになっている。
ただ8個まではレジスタ渡しされるので、スタック渡しの引数と連続になるように関数プロローグで一番最初にスタックに積んで残りのスタック引数と連続させる必要がある。
call
命令でリターンアドレスがra
レジスタに格納されるので、先に任意長引数の可能性のあるレジスタをスタックに積んで、その後リターンアドレスやフレームポインタを積んだりスタックフレームの確保を行う。
- 構造体引数と混ぜられたら対処できなそうな気がする…
- ヘッダのパス: /opt/homebrew/Cellar/riscv-gnu-toolchain/main/lib/gcc/riscv64-unknown-elf/12.2.0/include/stdarg.h
浮動小数点数の丸めモード
C言語でdouble
からint
へのキャストをすると小数点以下は切り捨てになる。
これを単にfcvt.w.d
命令を使うと四捨五入になってしまうので、アセンブリで丸めモードを指定する必要がある:
fcvt.w.d a0, fa0, rtz |
rtz
(Round towards Zero)fcvt
だけじゃなく、すべてのFP命令でrm
の丸めモードを指定できるらしい
レジスタ割付
レジスタ割付は、レジスタ構成がaarch64と大差ないので特に問題はなかった。あとがき
ひとまずRISC-Vのアセンブリを出力して、コンパイル結果をspikeシミュレータ上で動作させることができるようになった。
- 作ったとはいえまだ命令セットをよく理解してないので、アセンブラやリンカーも対応したい
- できればQEMU/Linuxや、実際のハードウェアでも動かしたい
追記
- “bbl loader”表示は解消された
- WSL2/Ubuntuでは
configure
のhost
にriscv64-unknown-linux-gnu
を指定する、 がそれだけではダメで、--with-abi=lp64d
も必要だった:https://blog.csdn.net/weixin_43283275/article/details/133465072 spike
でのpk
指定をフルパスにしなきゃ動かないのはspike
が想定しているパスと異なるからっぽい。spikeを自前でビルドする場合configure
時に--with-target=riscv64-unknown-XXX
で指定する。
リンク
- 対応のマージコミット
- RISC-Vツールチェイン: riscv-collab/riscv-gnu-toolchain: GNU toolchain for RISC-V, including GCC
This is the RISC-V C and C++ cross-compiler. It supports two build modes: a generic ELF/Newlib toolchain and a more sophisticated Linux-ELF/glibc toolchain.
- RISC-Vツールチェインは一般ELF/newlibと、Linux-ELF/glibcの2つのビルドモードがある
- spike: riscv-software-src/riscv-isa-sim: Spike, a RISC-V ISA Simulator
- Running 64- and 32-bit RISC-V Linux on QEMU — RISC-V - Getting Started Guide 自分は動かせなかった…
- RISC-Vの命令セット:Specifications – RISC-V International
- 呼出規約(pdf): Chapter 18. Calling Convention
- 任意長引数の場合浮動小数点数をGPレジスタ経由で渡すことも書いてあった汗
- RISC-V Instruction Set Manual, Volume I: RISC-V User-Level ISA | Five EmbedDev
- DTSインサイトのRISC-V技術ブログ(17) QEMUの試行 | DTSインサイト|技術ブログ
- qemu-system-riscv64でubuntuをbootさせられなかった(失敗編) - Qiita
- RISC-V向けの実行ファイルをmacOSとUbuntu22.04それぞれでエミュレーション実行する方法 #homebrew - Qiita
- RISC-V Instruction Set Specifications RISC-Vの命令と簡単な説明と、対応するマシン語のビット構成が見れる
- bitwise operators - How do I write NOT Operation for the Risc-V (Assembly Language)? - Stack Overflow 疑似命令について説明されている