1000行で作るOS - RISC-V入門

  1. はじめに
  2. 開発環境
  3. RISC-V入門
  4. OSの全体像
  5. ブート
  6. Hello World!
  7. C標準ライブラリ
  8. カーネルパニック
  9. 例外処理
  10. メモリ割り当て
  11. プロセス
  12. ページテーブル
  13. アプリケーション
  14. ユーザーモード
  15. システムコール
  16. ディスク読み書き
  17. ファイルシステム
  18. おわりに

RISC-V

アプリケーションを開発する際に「どのOSで動かすか」を意識するのと同じように、OSもその下のレイヤであるハードウェア (特にCPU) で動かすかを考える必要があります。本書では、RISC-V (リスク・ファイブ) を次の理由から選択しました。

なお、本書では32ビットのRISC-Vを利用します。64ビットでも大体同じように実装できるのですが、ビット幅が広い分複雑になるのと、アドレスが長く読むのが億劫なので、最初は32ビットがおすすめです。

virtマシン

コンピュータはCPUだけではなく、メモリをはじめとする様々なデバイスから構成されています。例えば、iPhoneとRaspberry Piは同じArmのCPUを利用していますが、別物だと考えるのが自然です。

本書では、RISC-Vベースの中でもQEMU virtマシン (ドキュメント) に次の理由から対応することにしました。

RISC-Vアセンブリ入門の手引き

アセンブリを手っ取り早く学ぶ方法は「C言語のコードがどのようなアセンブリに変わるのか観察する」ことです。あとは、使われている命令の機能をひとつずつ辿っていくと良いでしょう。特に次の項目が基本的な知識となります。入門書で書くのもなんですが、ChatGPTに聞くと良いでしょう。

アセンブリを学ぶときに重宝するのが、Compiler Explorer というオンラインコンパイラです。ここでは、C言語のコードを入力すると、コンパイルした結果の逆アセンブリを見ることができます。どの機械語がC言語のコードのどの部分に対応しているかを色でわかりやすくしてくれます。

また、コンパイラオプションに -O0 (最適化オフ) や -O2 (最適化レベル2) などの最適化オプションを指定して、コンパイラがどのようなアセンブリを出力するのかを確認するのも勉強になるでしょう。

デフォルトではx86-64 CPUのアセンブリが出力されるようになっています。右のペインでコンパイラをRISC-V rv32gc clang (trunk)を指定すると32ビットRISC-Vのアセンブリを出力するようになります。

CPUの動作モード

CPUでは動作モードによって実行できる命令や挙動が異なります。RISC-Vでは、次の3つの動作モードがあります。

モード 概要
M-mode OpenSBIが動作するモード。
S-mode カーネルが動作するモード。
U-mode アプリケーションが動作するモード。

特権命令

CPUの命令の中には、特権命令と呼ばれるアプリケーションが実行できない類があります。本書では、CPUの動作設定を変更する特権命令がいくつか登場します。以下の命令が本書で登場する特権命令です。

命令とオペランド 概要 擬似コード
csrr rd, csr CSRの読み出し rd = csr;
csrw csr, rs CSRの書き込み csr = rs;
csrrw rd, csr, rs CSRの読み出しと書き込みを一度に行う tmp = csr; csr = rs; rd = tmp;
sret トラップハンドラからの復帰 (プログラムカウンタ・動作モードの復元など)
sfence.vma Translation Lookaside Buffer (TLB) のクリア

ここで登場するCSR (Control and Status Register) とは、CPUの動作設定を格納するレジスタです。各CSRの説明は本書中にありますが、一覧を見たい場合は RISC-Vの仕様書 (PDF) を参照してください。

インラインアセンブリ

本文中では、次のような特殊なC言語の記法が登場します。

uint32_t value;
__asm__ __volatile__("csrr %0, sepc" : "=r"(value));

これは「インラインアセンブリ」と呼ばれる、C言語の中にアセンブリを埋め込む記法です。別途アセンブリのファイル (.S拡張子) にアセンブリのみを書いてコンパイルする方法もありますが、インラインアセンブリを使うことで次のようなメリットがあります。

インラインアセンブリの書き方

インラインアセンブリは、次のような形式で書きます。

__asm__ __volatile__("アセンブリ" : 出力オペランド : 入力オペランド : アセンブリ中で破壊するレジスタ);

インラインアセンブリの例

uint32_t value;
__asm__ __volatile__("csrr %0, sepc" : "=r"(value));

上記のインラインアセンブリは、csrr 命令で sepc レジスタの値を読み出し、value 変数に代入します。%0value 変数に対応しています。

__asm__ __volatile__("csrw sscratch, %0" : : "r"(123));

上記のインラインアセンブリは、csrw 命令で sscratch レジスタに 123 を書き込みます。%0123 が入ったレジスタ (r 制約) に対応し、次のように展開されます。

li    a0, 123        // a0 レジスタに 123 をセット
csrw  sscratch, a0   // sscratch レジスタに a0 レジスタの値を書き込み

インラインアセンブリには csrw 命令しか記述していませんが、"r" 制約を満たすために li 命令がコンパイラによって自動的に挿入されています。

インラインアセンブリは、C言語の仕様にはないコンパイラの独自拡張機能です。詳細な使い方はGCCのドキュメントで確認できます。ただし、CPUアーキテクチャによって制約の書き方が違っていたり、機能が多く複雑だったりと理解に時間を要する機能です。

初学者におすすめなのは実例に多く触れることです。例えば、HinaOSのインラインアセンブリ集が参考になるでしょう。