この記事は自作OS Advent Calendar 2019の19日目の記事です。本記事では、現在開発中のRISC-Vエミュレータの上で、フィボナッチ数列を計算するバイナリを動かし、それに関するRISC-Vテクノロジについて書きます。

Introduction

現在、RISC-Vのエミュレータを趣味で開発しています。メインのコードをRustで書き、WebAssemblyにコンパイルすることによって、手元のWEBブラウザ上で動かすことができます。

最終目標はOSをエミュレータ上で動かすことですが、現時点ではまだ50を超える命令セットを実装し、それらが最もプリミティブなモードであるマシンモード (M mode) で動くのみです。まだ目標達成までやることはかなり残っていますが、それでもプログラムっぽいものが動くようになってきました。以下はフィボナッチ数列を計算するRISC-Vバイナリを自作エミュレータ上で実行してみたデモです。

Demo fib on rvemu

現在のエミュレータは以下の4つのコマンドによって操作することができます。

  • upload: RISC-Vバイナリをローカルのコンピュータからアップロードする
  • ls: アップロードされたバイナリの一覧を表示する
  • run [file]: アップロードされたバイナリを実行する
  • help: コマンドの使い方を表示する

デモ動画では、あらかじめ準備しておいたフィボナッチ数を計算するバイナリファイル fib と他のテスト用バイナリファイルをアップロードし、run fibによって実行しています。実行後は32個あるレジスタがどのような値になっているのかを表示します。今回は10番目のフィボナッチ数を計算しており、実行結果を保存しているx10の行を見ると55になっているため、無事に計算できていそうなことがわかります。

RISC-V Overview

RISC-Vは誰でも無料で使用することができ、RISCの名が示す通り、命令セットの複雑さ、数、種類を減らし、できるだけ単純化する設計思想の元に作られている比較的新しい命令セット (ISA) です。

RISC-VのISAは複数のモジュールによって成り立っています。最低限必要な基本のモジュールはあるものの、その他は必要な機能だけを実装することが可能です。つまり、低コスト・低パフォーマンスで良い機器であれば最低限のモジュールだけを実装し、逆に何よりもパフォーマンスを重視している場合はSIMD命令等のモジュールも実装することができます。

本エミュレータはOSを動かすことが目標のため、基本的なモジュールだけではなく、仕様書に定義されているRV32GまたはRV64Gというモジュールセットを実装する必要があると考えています。RV32GRV64Gはそれぞれ32/64 bit アーキテクチャのための汎用ISA (“general-purpose” ISA) と言われており、以下の全てのモジュールを含みます。

  • RV32I/RV64I: 足し算、ブランチ、ロード、ストアなど最も基本的な命令
  • RV32M/RV64M: 掛け算、割り算の命令
  • RV32A/RV64A: アトミック命令
  • RV32F/RV64F: 浮動小数点数 (Float) の命令
  • RV32D/RV64D: 浮動小数点数 (Double) の命令
  • Zicsr: コントロールレジスタを操作するのための命令
  • Zifencei: フェンス命令 (メモリの書き込み順序を保証するための命令)

現在はこのうちの RV32I/RV64I/RV32M/RV64M を実装済みです。

仕様書は主に ISA やメモリモデルについて定義している Unprivileged Specification とスーパバイザなどの特権モードについて定義している Privileged Specification の2種類があります。どちらも RISC-V Foundation のサイトから見ることができます。今回の実装に関しては、全て Unprevileged Specification (The RISC-V Instruction Set ManualVolume I: Unprivileged ISADocument Version 20191213) を参考にしました。

RISC-V Assembly for Fibnacci Numbers

今回動かすバイナリはC言語で書いたフィボナッチ数列を求めるプログラムを、RISC-Vツールチェーンでアセンブルしたものを手で簡略化しました。フィボナッチ数列を求めるときには再帰関数を使う手法もありますが、スタックがまだうまく動いていないようだったので、メイン関数の中でループを回す手法を採用し、変数を全てレジスタに置き換えています。また、入出力等もまだ実装していないため、今回のプログラムでは決め打ちで10番目のフィボナッチ数 (55) を求めます。

int main() {
  int n=10;
  if(n <= 1) {
    return n;
  }
  int fib = 1;
  int fibPrev = 1;
  for(int i = 2; i < n; i++) {
    int temp = fib;
    fib += fibPrev;
    fibPrev = temp;
  }
  return fib;
}
main:
  li t3,10              # int n=10;
  li t4,1
  blt t4,t3,L1          # if (1<n) goto L1;
  mv t4,t3
  j L3
L1:
  li t4,1		# int fib=1;
  li t5,1 		# int fibPrev=1;
  li t0,2		# int i=2;
L2:
  bge t0,t3,L3          # if (i>=n) goto L3;
  mv t6,t4		# int temp=fib;
  add t4,t4,t5		# fib+=fibPrev;
  mv t5,t6 		# fibPrev=temp;
  addi t0,t0,1		# i++
  j L2
L3:
  mv a0,t4

Assembler Mnemonics for Registers

上のアセンブリでまず気になるのが、レジスタの名前です。RISC-Vはx0からx31までの32個の汎用レジスタを持っています。しかし、上のアセンブリではt3a0などとなっています。これはアセンブラのニーモニックです。汎用レジスタと言いながらもそれぞれ役割を持っており、名前によって役割がわかるようになっています。

以下の表はx0からx31までのレジスタに名付けられたニーモニックです。今回使用したのは、変数を保存するための一時的なt0t3からt6と、変数の返り値としてa0を使用しました。zero (x0)は例外的に常に0が入っており、このレジスタの値を書き換えようとしても何も変化しません。

Assembler mnemonics The RISC-V Instruction Set Manual Volume I: Unprivileged ISA Document Version 20191213 (Chapter 26 RISC-V Assembly Programmer’s Handbook / Table 26.1)

Pseudoinstructions

次にアセンブリ自体を見ていきます。実はlijmvなどのニーモニックはRISC-VのISAには存在しません。これらはアセンブリ言語の擬似コードで、実際に実行する前には変換されます。

具体的には、以下のように変換されます。

  • li t3, 10 –> addi x28, x0, 10: レジスタx28x0と即値10を足した数を代入する
  • j label3 –> jal x0, label3: label3のオフセットをプログラムカウンタ (PC) に足す
  • mv t6, t4 –> addi x31, x29, 0: レジスタx31x29と即値0を足した数を代入する

x86 ではよく使用するmov命令さえ無いことに初めは驚きましたが、addiによって一行で代用できてしまうので全く問題ありません。擬似コードを実在するコードに変換する際にとてもありがたいのが常に値が0のレジスタx0の存在です。例えば、nopなども実際には存在しませんが、addi x0, x0, 0で代用することができます。x0は常に0なのでプログラムカウンタが進むだけで何も変化しません。このようにアセンブリの擬似コードを使用することによって、読みやすさを確保しつつも、実際のISAの数を減らして簡単な仕様を保っているんですね。

Future Work

今後の予定としては、まず実装していないISAを実装すること、そしてスーパーバイザのモードを追加すること、そして、コードをクラウドサーバにアップロードして誰でもアクセスできる状態にすることなどを考えています。

また、エミュレータを作成しはじめたモチベーションの1つとして、ISAの仕様書を全て読み通してみたい気持ちがありました。以前OS自作界で有名な緑の本を読みつつコードを書いていたとき、どの部分が仕様などで決まっていて自分で勝手に決められないのか、どの部分なら自分で好き勝手に決めてよいのかわかりませんでした。なので、仕様書を読むことで、ISA (CPU) によって決められている範囲はどこからどこまでかを知ることができると考えたからです。 現在はまだ Unprivileged Specification の4分の1程度しか読めていないので、他の仕様書も含めて読み通すことも今後の目標です。

コードは全てd0iasm/rvemuのリポジトリで公開しているので、興味があれば見てみてください。