RISC-V の CSR 転送命令と特権命令の機構と実装

公開日:
RISC-V の CSR 転送命令と特権命令の機構と実装

RISC-V で例外や割り込みを処理するためには CSR 転送命令や特権命令を実装する必要があります。
この辺りは OS とも非常に深い関わりがあり、これらの命令がないと自作プロセッサで OS を動かすことはできません。(筆者は結局 OS を動かしていないのですが…)
これらを実装するために、初めどこから手をつけていいかわからなかったので、できるだけ丁寧に直感的に理解できるように解説してみたいと思います。

CSR 転送命令については Volume 1, Unprivileged Spec v. 20191213、特権命令については Volume 2, Privileged Spec v. 20190608 の公式の仕様書が参考になると思います。
参考になる日本語の解説記事も適宜紹介していこうと思います。

また、今回説明する CSR 転送命令や特権命令を実装したプロセッサは以下に公開して、解説記事も書いているので、参考にしてください。

他にも以下の実装が参考になると思います。

さらに、OS 側でどのような処理が行われているのかを知るためには xv6-riscv という教育用の小さな OS の実装を参考にするとより理解が深まると思います。

1. モード

権限を管理し、それぞれの権限で実行できることを指定できるように RISC-V のプロセッサには主に三つのモードが用意されています。
それぞれ、ユーザモードスーパーバイザーモードマシンモードと呼ばれ、この順に強い権限を持っています。
マシンモードは強力な権限を持っているため、OS を動かすためにスーパーバイザーモード、ユーザアプリケーションを動かすためにユーザモードという風に少し弱い権限のモードが 2 つ用意されています。
今回はこれらのうち、ユーザモードとマシンモードの 2 つのみについて扱っていくことにします。(この二つが理解できればスーパーバイザーモードも理解できると思います。)

レベルビット表現名前略称特徴
000ユーザモードUユーザアプリケーションを動かすために用いられる。
101スーパーバイザーモードSOS を動かすために用いられる。
210ハイパーバイザーモードH仕様の策定が進んでいない。
311マシンモードM強力な権限を持っていて、全ての制御レジスタにアクセスできる。
RISC-V の動作モード

2. CSR

先程説明したモードの切り替えを行ったり、例外や割り込みの処理を行ったりするために、RISC-V で用意されている制御レジスタのことを CSR (Control and Status Register) と呼びます。
このレジスタにはどのような値が格納されているのかをまず確認することにします。

このレジスタのアドレスは 12 ビットで指定され、各アドレスに 32 ビットのデータが格納されています。
つまり、2^12 = 4096 個のエントリがあるわけですが、それら全てが使われているわけではなく、簡易的に実装する上で必要なレジスタは限られているので、それらに限って説明します。

番地名前詳細
0x300MSTATUSプロセッサの現在の状態を表す。
0x304MIEどの割り込みが可能かを表す。
0x305MTVEC割り込み・例外が発生した場合にジャンプする PC を格納する。
0x341MEPC例外が発生した命令の場所を表す PC を格納する。
mret 命令ではこの値を使って元の処理に復帰する。
0x342MCAUSE例外・割り込みが発生した要因を格納する。
割り込みの場合は最上位ビットが1になる。
0x343MTVAL発生する例外に応じた値を格納する。
例えば不正命令なら命令を、ブレークポイントならその PC を格納する。
0x344MIP割り込みの待ち状態を表す。MIE と合わせて割り込みを起こすか判断する。
重要な CSR レジスタの一覧

CSR レジスタについてさらに詳しくはこちらの記事に解説されているので、興味ある方はご覧ください。

3. CSR 転送命令

CSR 転送命令とは先程説明した CSR に読み書きするために用いられる命令のことを指します。
この命令自体は特権命令ではなく、非特権命令に分類されています。

具体的には以下の 6 つの命令が存在します。

  • CSRRW / CSRRWI (CSR Read / Write): CSR と汎用レジスタのアトミック交換
  • CSRRS / CSRRSI (CSR Read / Bit Set): CSR の読み込みとビットセット
  • CSRRC / CSRRCI (CSR Read / Bit Clear): CSR の読み込みとビットクリア

アセンブリで書くときは例えば、csrrw rd, csr_addr, r1 のように書き、この命令は csr_addr で指定されるアドレスにある CSR の値を rd レジスタに書き込み、r1 レジスタの値を CSR に書き込むという操作を表しています。

これらの命令については以下の記事も参考になると思います。

さらに、これらをわかりやすく書くために擬似アセンブリも用意されていて、以下のようなものがあります。

擬似コードオリジナルの命令説明
csrr rd, csr_addrcsrrs rd, csr_addr, x0CSR 読み込み (CSR の内容は更新しない)
csrw csr_addr, rscsrrw x0, csr_addr, rsCSR 書き込み (汎用レジスタは更新しない)
csrs csr_addr, rscsrrs x0, csr_addr, rsCSR のビット設定 (汎用レジスタは更新しない)
csrc csr_addr, rscsrrc x0, csr_addr, rsCSR のビットクリア (汎用レジスタは更新しない)
csrwi csr_addr, immcsrrwi x0, csr_addr, immCSR の即値書き込み (汎用レジスタは更新しない)
csrsi csr_addr, immcsrrsi x0, csr_addr, immCSR の即値ビット設定 (汎用レジスタは更新しない)
csrci csr_addr, immcsrrci x0, csr_addr, immCSR の即値ビットクリア (汎用レジスタは更新しない)
CSR 用の擬似アセンブリ

この擬似アセンブリは先程も紹介した以下の記事を参考にしました。

4. 例外・割り込み処理の概要

ここまで、プロセッサの動作モードや CSR、CSR 転送命令について見てきました。
これらを元にどのように例外処理や割り込み処理が行われていくのかをこれから詳しく書いていきますが、その前に大まかにこれらの処理がどのように行われるのかを見ていきましょう。

まず、普通の命令を実行している際は動作モードはユーザモードになっています。
そこから、例外や割り込みが発生するという流れになります。

例外や割り込みの種類としては実装したものだけ上げておくと以下のようなものがあります。

  • 例外
    • 2: 不正命令 (無効オペコード)
    • 3: ブレークポイント
    • 8: ユーザモードからのシステムコール
    • 11: マシンモードからのシステムコール
  • 割り込み
    • 7: マシン・タイマ割り込み
    • 11: マシン・外部割り込み

一覧については Volume 2, Privileged Spec v. 20190608 の仕様書の p.37 「Table 3.6: Machine cause register (mcause) values after trap.」にまとめられているので、参考にしてください。

まず、例外についてですが、これは正しく命令を実行しようとしたが、命令自体が何かしら正しくなかったり (オペコードが無効である、アクセスできないメモリへのアクセスを要求しているなど)、OS の処理を必要とする命令だったり (システムコールなど) という場合に生じて、OS 側が指定した命令アドレスにジャンプして (プログラムカウンタを変更して)、マシンモードで 、OS に指定された命令列を実行していくという処理を行います。
最後に、mret 命令があるはずなので、この命令を読むと、OS が指定したアドレスにジャンプして、ユーザモードに戻り、通常の処理を再開します。

次に、割り込みについてですが、割り込みはプロセッサの外部から割り込み信号が届くと、割り込みを発生させて良いかどうかを確認し、よければ例外と同じように、OS の指定した命令アドレスにジャンプして、mretで処理に復帰するという流れで動作します。

このようにして例外や割り込みが起こるので、この挙動を再現していけばいいということになります。

今回実装した特権命令は mret のみですが、スーパバイザーモードから戻るための sret などの特権命令も存在します。

また、今回は多段の割り込みや例外はサポートせずに、どちらか一方のみが発生しているか、何も発生していない状態しかないことを想定していて、割り込みよりも優先して例外を扱うことにしています。
さらに、これ以降の解説は前提として、Fetch、Decode、Execute、Write からなる 4 段パイプライン構造のインオーダ実行インオーダ完了プロセッサに実装することを考えています。

例外、割り込み処理については以下の記事も参考になると思います。

5. 例外処理

例外については、不正命令、ブレークポイント、システムコールの三つを扱うが、それぞれ無効な命令、ecallebreakE ステージで実行されようとした時に例外が発生します。
ここで、例外が発生した時に F ステージD ステージにある命令は一度フラッシュした後に、例外に対して適切な処理を行って、再度実行し直す必要があります。
さらに、例外の処理が終わった後に返ってくるアドレスとしては、例外を起こした命令の次の PC にある命令と考えて処理を行うことにします。

これを踏まえて、処理の順序を改めて記述すると以下のようになります。

  1. ユーザモードで実行中に E ステージで例外が発生する。
  2. MCAUSE に例外コードを設定する。
  3. MEPC に例外を起こした命令の PC を設定する。
  4. 例外に応じて MTVAL に適切な値を設定する。
  5. MSTATUS を現在の状態に合わせて更新する。
  6. パイプラインをフラッシュする。
  7. マシンモードに遷移する。
  8. MTVEC の値を元に PC を更新する。
  9. 例外ハンドラの処理を開始する。
  10. 使用するレジスタを退避する。
  11. 例外を処理する。
  12. 次に実行する PC を MEPC にセットする。
  13. 退避していたレジスタを回復する。
  14. mret を実行して例外ハンドラの処理を終了する。
  15. MSTATUS を現在の状態に合わせて更新する。
  16. パイプラインをフラッシュする。
  17. ユーザモードに遷移する。
  18. PC を MEPC の値を元に更新する。
  19. 通常の処理に戻る。

ここで、ジャンプ先のアドレスを格納している MTVEC の更新や、9 – 14 に関わる命令列については OS 側で適切に用意してくれているものだと筆者は考えています。(もし違っていたら教えてくださると幸いです… 🙏)

6. 割り込み処理

割り込み処理は外部からの信号 ext_intrtime_intr の二種類に起因して発生すると考えることにします。
このとき、割り込み処理が発生する条件は以下の三つの条件を満たすことだと考えられます。

  • ユーザモードである
  • ext_intrtimer_intr に 1 が入っていて、対応する MIE のビットが 1 である
  • E ステージにある命令が例外を引き起こさない

これらが満たされた場合に割り込みが発生するが、パイプライン化を行っているためどの命令を実行している時に割り込みが発生したと考えるかが難しくなります。
まず、W ステージにあるものは割り込みが発生する時には処理が終了しているので、終了した命令と考えることができます。
そのため、EDF の順に各ステージを見て、実行中の命令があれば、その命令の実行中に割り込みが発生したと考えます。
どのステージでも命令が実行されていない場合には PC の値が次に実行される命令のはずなので、それを実行中の命令と考えることにします。
割り込みを処理した後は、先程実行中だった命令の PC に復帰することで元の処理に戻ることができます。

これを踏まえて、処理の順序を改めて記述すると以下のようになります。

  1. 条件を満たした状態で割り込みが発生する。
  2. MCAUSE に割り込みコードを設定し、最上位ビットを 1 にする。
  3. MEPC に割り込みが発生した時に実行中の PC を設定する。
  4. MTVAL に 0 を設定する。
  5. MSTATUS を現在の状態に合わせて更新する。
  6. パイプラインをフラッシュする。
  7. マシンモードに遷移する。
  8. MTVEC の値を元に PC を更新する。
  9. 割り込みハンドラの処理を開始する。
  10. 使用するレジスタを退避する。
  11. 割り込みを処理する。
  12. 退避していたレジスタを回復する。
  13. mret を実行して割り込みハンドラの処理を終了する。
  14. MSTATUS を現在の状態に合わせて更新する。
  15. パイプラインをフラッシュする。
  16. ユーザモードに遷移する。
  17. PC を MEPC の値を元に更新する。
  18. 通常の処理に戻る。

大まかな流れとしては例外処理と同じだが、各 CSR の設定や、実行中だった命令に戻ればいいので MEPC を更新する必要がないことなどの違いがあります。

7. まとめ

長くなってしまいましたが、CSR 転送命令や特権命令の機構や例外・割り込み処理の機構についてまとめました。
プロセッサを書く中で OS と関わりが深い部分なので、様々な知識が必要だったり、ややこしい仕様を理解しないといけなかったりして大変な部分もありました。
また、あまり詳しく書かれている日本語の記事もなかったので、余計に難しかったと感じたように思います。
この記事が少しでも助けになればいいなと思います 👍

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA