RISC-Vのシミュレーション環境を構築した

そろそろクリスマスですね。ltmpcを作ってからもう一年たってしまったのかという思いでいっぱいです。

github.com

さて、前回作ったRISC-V用のgccですが、残念ながら手元にRISC-Vの命令セットが動くコンピュータがないため、コンパイルしたプログラムを動かすことができません。 そこで、シミュレータを使って動かします。

シミュレータにはいくつか種類がありますが、公式が出しているSpikeというシミュレータを使うことにします。 github.com

ただし、これ単体でビルドすることはできず、依存ライブラリを先にインストールしておく必要があります。さらに、Spikeのビルドに成功したとしても、proxy kernel(pk)がないとほとんどのプログラムは動きません。 proxy kernelというのは、要するにOSの仕事をやってくれるような(RISC-Vの命令を使った)プログラムコードのようです。

……という失敗があったので、Spikeだけをビルドしようとするのはやめましょう。素直に公式が出している、RISC-Vツールチェイン*1を全てビルドすることにします。

github.com

やり方は簡単で、

$ git clone https://github.com/riscv/riscv-tools.git
$ git submodule update --init --recursive
$ sudo apt-get install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev libusb-1.0-0-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev device-tree-compiler pkg-config libexpat-dev
$ export RISCV=/home/lpha/riscv/toolchain
$ ./build.sh

とやればよさそうです。

ただ、apt-get を行った際に perl-base のバージョンと bison flex texinfo libtool-bin の依存バージョンが異なるという衝突が発生し、面倒なことになりました。 これらがインストールされていないとビルドに失敗するため、これは大きな問題となります(ビルドの途中で失敗するうえ、ビルドに失敗した後にやり直すとまた始めからになってしまうので大変な時間ロスになります)。

aptitude コマンドを使うと、この問題の解決策が三つ表示されます(これはおそらく、何がインストールされているかによると思われます)。 そのうち二つは「インストールをあきらめる」という趣旨でしたが、これでは解決になりません。 三つ目の解決策は「perlのバージョンを下げる」というものであり、これを実行することで依存関係の衝突の問題を解消することができました。

このままでは依存関係の衝突が解消されただけで、問題の bison flex texinfo libtool-bin はまだインストールできていないので、忘れずにインストールしておきます(これに気づかずに何度もビルドに失敗しました……)。

ビルドには、すごい時間がかかります。/usr/bin/timeコマンドで計測してみたところ、二時間ほどかかったようです。

RISC-V Toolchain installation completed!
2104.01user 1129.07system 1:49:00elapsed 49%CPU (0avgtext+0avgdata 543192maxresident)k
0inputs+0outputs (0major+150543612minor)pagefaults 0swaps

シミュレータを使うには、以下のようにします。

$ /home/lpha/riscv/toolchain/bin/spike pk Hello.out

あれ、おかしいですね、PCが負の番地に飛んでクラッシュしました。

どうやら、先週ビルドしたRISC-V用のgcc8.2.0が出すプログラムは微妙に狂っているようです。

ツールチェインの中に含まれるgcc7.2.0を使ってコンパイルしたものは正しく動きます。

$ /home/lpha/riscv/toolchain/bin/spike pk Hello.out
Hello, world!

一命令ごとに実行するためには、対話的デバッグ実行オプション-dをつけます。

$ /home/lpha/riscv/toolchain/bin/spike -d pk Hello.out
:

たとえば、until pc 0 1034cなどと打ち込むことで、プログラムカウンタ(PC)が0x1034cになるまで実行をスキップできます(実は落とし穴、後述)。 この時、pc 0となっているのは、0番のコアのPCがその値になるまで、という意味です。どうやらマルチコア対応もされているようですね、すごいです。

until pc 1034cなどと打ち間違えてもスルーしてしまうのはかなり不親切です。うっかりしがちなので気を付けます。

ここでエンターキーを押せば、一命令ずつ実行されていきます。

reg 0 spなどと打ち込むことでそのレジスタの値を出力することができます。このreg 0というのはやはり0番のコアのレジスタだという意味ですね。 レジスタの名前はどうもABIで定められたものしか使えず、x2のような指定ができない点は不便です。

reg 0とだけ打ち込むことで全部のレジスタの値を出力できます。

mem 0 7f7e9b50などと打ち込むことでメモリの特定番地の値を8Byte分ダンプすることができます(アラインがあっていなくても8Byte分出てきます)。

さて、0x1034c番地はHello.outのエントリーポイントなのですが、until pc 0 1034cと打ち込むとインタラプトが発生したと出力されます。 その後エンターキーを押して行ってもその付近(0x1034c番地にあるのは0x10378番地へのjal命令ですが、そこでもない)を実行している様子はないため、不可解な状況です。

これは実は、OSのローダープログラムが動いた後に本体プログラムにジャンプした瞬間であり、初めて見るメモリ番地だったので、命令メモリを参照した際にページフォルトが発生したのが原因です。 エンターキーを押して行った時に表示されるのは、プロキシカーネル内の、ページフォルトからの復帰コードだったわけです。

そのため、もう一度until pc 0 1034cと打ち込む必要があります。gdbみたいに上キーを押しても履歴が出てくるわけではないので本当に打ち込む必要があります。面倒くさいですね……。

SpikeはISAシミュレータとうたっていますが、実際にはページフォルトへの対応をproxy kernelのコード(これもRISC-Vの命令列で行ったりと、かなり深いレベルまでシミュレーションしてくれるようです。

ログを吐かせるオプション-lをつけて実行することで命令列をダンプすることができます。 これを使って0x1034c番地に来るのが何命令目なのかを数えてみたところ、285123命令目でした。本体プログラムが実行されるまでにこれだけの命令が実行されているとすると結構すごいですね。

*1:リポジトリの名前はtoolsになっているけれどなんでだろう