"Writing an OS in 1000 lines"のDocker + Rustでの開発環境を構築してみた


# 環境
<dl>
  <dt>OS</dt>
  <dd> Windows 11 ProのWSL Ubuntu </dd>
  <dt>CPU</dt>
  <dd>Intel Core i9-13900K</dd>
</dl>

# 背景
https://operating-system-in-1000-lines.vercel.app/ja/welcome

「自作OSで学ぶマイクロカーネルの設計と実装」の補足資料である上記Webページの実装は**C言語**で書かれています。
これを**Rust**で実装してみたいと思いました。

:::note warn
この記事の筆者は、「自作OSで学ぶマイクロカーネルの設計と実装」をまだ読んでいないため、このWebサイトには載っていないが、本には載っている情報がある可能性があります。
:::

# DockerでOSの実行環境構築

:::note warn
現状、dockerイメージのサイズが20GB弱になるため、PCの容量に注意
:::

参考サイトにはDocker環境が用意されていませんでしたが、WSLのUbuntu上でclang等の重要性の高い環境を万が一汚してしまうと元に戻すのが大変なので、失敗しやすい環境を用意するのが良いと考えました。UbuntuのDocker Imageをベースに必要なライブラリをインストールします。

また、このサイトではプログラムのコンパイルターゲットとして、RISCアーキテクチャを指定しています。
普段私たちが使用しているIntelやAMD, Appleのアーキテクチャとは異なるので、RISC専用の対応をする必要があります。
Webサイトの手順には```qemu-system-riscv32```などいくつかのライブラリを入れるだけで動くと書いてありましたが、自分の環境では```riscv-gnu-toolchain```をビルドしないとWebサイトのサンプルコードの```./run.sh```がエラーになり、起動することができませんでした。（原因不明）


```Dockerfile:Dockerfile
FROM ubuntu:latest

# 必要なパッケージをインストールするための準備
RUN apt-get update && \
    apt-get upgrade -y
RUN apt-get install -y \
        build-essential \
        clang \
        curl \
        git \
        wget \
        gawk \
        texinfo \
        bison \
        autoconf \
        cmake automake autotools-dev \
        python3 python3-pip \
        libmpc-dev libmpfr-dev libgmp-dev flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev ninja-build libglib2.0-dev

RUN apt-get install -y \
        lld \
        llvm \
        qemu-system-riscv32


# RISC-VのGCCツールチェーンをビルドするためのスクリプト等をダウンロード
RUN git clone --recursive https://github.com/riscv/riscv-gnu-toolchain

# RISC-V 32ビット用のツールチェーンをビルド
WORKDIR /riscv-gnu-toolchain
RUN ./configure --prefix=/opt/riscv32 --with-arch=rv32i --with-abi=ilp32 && \
    make

# 環境変数を設定して、ツールチェーンが見つかるようにする
ENV PATH="/opt/riscv32/bin:${PATH}"

RUN mkdir -p work && \
    cd work && \
    curl -LO https://github.com/qemu/qemu/raw/v8.0.4/pc-bios/opensbi-riscv32-generic-fw_dynamic.bin

WORKDIR /work
```

```yml:docker-compose.yml
services:
  main:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/work
    tty: true
```

# Rustの開発環境の構築
Dockerfileに以下のコードを追加します。
```Dockerfile
RUN curl https://sh.rustup.rs -sSf | sh && \
    source "$HOME/.cargo/env" && \
    rustup default nightly && \
    rustup target add riscv32i-unknown-none-elf && \
    cargo install cargo-binutils && \
    rustup component add llvm-tools-preview
```

- ```rustup```という[Rust公式ページ](https://www.rust-lang.org/ja/tools/install)で推奨されているRustの管理ツールをインストールします。
- 自作OSなど低レイヤーを扱わない限り出てこないインラインアセンブリの機能をRustで使用するためには**nightly**バージョンのRustをインストールする必要がありました。
- RustからRISC-Vアーキテクチャのアセンブリにコンパイルするためにtargetを指定しますが、RISC-V系のセットはデフォルトでは入っていないため、追加する必要があります。今回は```riscv32i-unknown-none-elf```を指定しました。unknownやnone, elfに関しては拡張命令セットが入っているかやターゲットのプラットフォーム（linux, esp, gnu, ...）などによって変える必要がありそうです。
- ```cargo-bin-utils```や```llvm-tools-preview```はobjdumpをする際にrustのコードの関数名を表示してくれるなど、Rustをビルドしたバイナリを読む際はサイトの方法でobjdumpするよりもこちらを使用したほうが良さそうでした。


# Rustで最初のカーネルコードを書く
まず、以下のコマンドを実行し、Rustプロジェクトのテンプレートを作成します。
```
$ cargo new [project_name]
```

ここでは、src/main.rsをkernel.rsにrenameし、このファイルに[kernel.c](https://operating-system-in-1000-lines.vercel.app/ja/boot#%E6%9C%80%E5%B0%8F%E9%99%90%E3%81%AE%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB)のRustコードを書きます。

### 自作OS特有の問題

- 通常のRustコードでは必須であるmain関数を使用しないため、```#![no_main]```というオプションが必要でした。
- ```kernel_main```関数はアセンブリから呼び出すため、```#[allow(dead_code)]```というオプションが必要でした
- 標準ライブラリを使用しないno_std環境ではpanic関数を自分で定義する必要がありました。

```rust
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}
```
- インラインアセンブリは以下のように使用します。C言語とは書き方が違うのでここで結構苦しみました。正しいかはわからないが、objdumpの結果を見るとうまく配置されているように感じました。
```rust
use core::arch::asm;

#[no_mangle]
#[link_section = ".text.boot"]
pub unsafe extern "C" fn boot() -> ! {
    // スタックポインタを初期化
    asm!(
        "mv sp, {stack_top}\n
        j {kernel_main}\n",
        stack_top = in(reg) &__stack_top,
        kernel_main = sym kernel_main,
    );
    loop {}
}
```


# RustからRISC-Vにコンパイルする
:::note warn
 kenel.ldやrun.shはサンプルコードがほぼそのまま使えるので、この記事では省略
:::

Cargo.tomlにバイナリの出力ファイル名と一番最初に見るRustコードを指定しました。
```toml:Cargo.toml
[[bin]]
name = "kernel_elf"
path = "src/kernel.rs"
```

コンパイルオプションは.cargo/config.tomlに記述する必要があります。
- build ターゲットを```riscv32i-unknown-none-elf```に指定
- リンカスクリプトをkernel.ldに指定
- kernel.mapというファイルにバイナリのmapping結果を出力する

```toml:.cargo/config.toml
[target.riscv32i-unknown-none-elf]
rustflags = [
  "-Clink-arg=-Tkernel.ld",
  "-Clink-arg=-Map=kernel.map"
]

[build]
target = "riscv32i-unknown-none-elf"
```

ビルドの実行を実行します。
```
$ cargo build --release
```
すると、```./target/riscv32i-unknown-none-elf/release/kernel_elf```に実行バイナリが生成されました！
また、kenel.mapも出力されています。
これをみると、参考サイトのC言語の結果とは少し違い、Rust特有の配置が行われているように感じます。

```
80200000 80200000       28     1         /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/libcompiler_builtins-2fea179addb75593.rlib(compiler_builtins-2fea179addb75593.compiler_builtins.cd3c4ffcff995398-cgu.086.rcgu.o):(.eh_frame+0x0)
80200028 80200028       24     1         /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/libcompiler_builtins-2fea179addb75593.rlib(compiler_builtins-2fea179addb75593.compiler_builtins.cd3c4ffcff995398-cgu.006.rcgu.o):(.eh_frame+0x3c)
8020004c 8020004c       14     1         /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/libcompiler_builtins-2fea179addb75593.rlib(compiler_builtins-2fea179addb75593.compiler_builtins.cd3c4ffcff995398-cgu.053.rcgu.o):(.eh_frame+0x14)
```

とりあえず、エラーがでなかったので、ここを記事の区切りとし、今後進めていく際に上記手法に問題があれば修正していきます。
（Rustや自作OSに詳しい方におかしい点などご指摘いただけたらとても有難いです。修正します）

# ソースコード
https://github.com/masa5555/os-1000-line-fork-rust-impl