Open6

ゼロからのOS自作入門をRustで

ooharabucyouooharabucyou

https://www.amazon.co.jp/ゼロからのOS自作入門-内田-公太/dp/4839975868/ref=sr_1_1?crid=67B0B8RYS5S&keywords=ゼロからのos自作入門&qid=1655873707&sprefix=ゼロからの%2Caps%2C205&sr=8-1

を読んでいるので、Rust でカーネルを書いてみようという試み

前提として、

わたしの状況

  • 普段はWebな技術者として、PHP、Ruby、Kotlin、TypeScript を扱うことが多い
  • C++については、この本に従って見様見真似で書いているという感じ
  • Rust に関しては、以下の本で入門した + スクラップにまとめた

https://www.amazon.co.jp/実践Rustプログラミング入門-初田-直也/dp/4798061700/ref=pd_lpo_1?pd_rd_i=4798061700&psc=1

https://zenn.dev/kawahara/scraps/5a22db01d86ec9

https://zenn.dev/kawahara/scraps/5482ab07ffb39e

https://zenn.dev/kawahara/scraps/7fa9b51e094736


GitHub: https://github.com/kawahara/mikanos_kernel_rust

ooharabucyouooharabucyou

ブートローダーの用意

一旦、ブートローダーについては、OS自作入門の4章終了時点のものを用意する。先人のRustでOSを作るための資料については、Rust を利用して開発しているが、学習のためにあえてブートローダーはEDK II上で作られたものを利用して、カーネルのみを Rust で作る。

XQuartz の導入

Mac 環境なので、XWindow 経由で QEMU (OSエミュレーター) を開けるようにするためのツールを導入する。

brew install --cask xquartz

アプリケーションから、XQantaz を開くと、メニューから設定が選べるので、環境設定 -> セキュリティから、「ネットワーク・クライアントからの接続を許可」をオンにしておく。

これは、この説明する Docker 上で、XWindow 転送を利用するために必要な措置になる。
XQuartz については、一旦終了して、再起動しておく。

xhost の設定

Mac 環境側 (ホストOS側) で、Docker からの転送を許可するために、必要な設定になる。
mikanos-docker からは、host.docker.internal:0 を経由して、ホストOSにある XWindow Server につなげるようになっているため、ローカルからの接続を許可する

xhost + 127.0.0.1 

以下のメッセージが出れば成功

127.0.0.1 being added to access control list

mikanos-docker の用意

まずは、mikanos-docker を起動する。このイメージは、OS自作入門が用意している環境を予め用意しているスグレモノである。
OS自作入門の本に合わせるため、/home/vscode/workspace に、ソースコードを配置する。

docker run --privileged --user vscode -v /path/to/local:/home/vscode/workspace -it ghcr.io/sarisia/mikanos  /bin/bash

(Host) Workspace に mikanos のコードを投入

上記で指定した /path/to/local 上で、mikanos のソースコードを入手する。
ブートローダーの作り方を学びたければ、osbook_day02a あたりからコツコツと、osbook_day04d のブートローダーの実装に写経していけば良い。(実際は osbook_day02a からコツコツとブートローダーを改良していったが、ここではセットアップを重視するために osbook_day04d から始めている。)

git clone https://github.com/uchan-nos/mikanos.git
git checkout osbook_day04d

(Container) EDK II + ローダービルド準備

cd ~/edk2
source edksetup.sh
ln -s ~/workspace/mikanos/MikanLoaderPkg/ ./
sudo apt-get update
sudo apt-get install -y vim
vim Conf/target.txt
  • ACTIVE_PLATFORM: MikanLoaderPkg/MikanLoaderPkg.dsc に書き換え
  • TARGET_ARCH: X64 に書き換え
  • TOOL_CHAIN_TAG: CLANG38 に書き換え
build

以下のような文字が出れば成功

- Done - 
Build end time: 05:59:12, Jun.22 2022
Build total time: 00:00:01

(Container) C++ で書かれた mikanos のカーネルのビルド

source ~/osbook/devenv/buildenv.sh
cd ~/workspace/mikanos/kernel
make

(Container) 起動テスト

~/osbook/devenv/run_qemu.sh ~/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi ~/workspace/mikanos/kernel/kernel.elf

起動に成功すると以下のようになる

(Host) ローカル用のイメージを更新

docker のコンテナを終了したあとに、ローダーのビルドやら設定を行うのは面倒なので、
この時点で、docker commit を行い、ローカル用のイメージを作っておく。

まずは、コンテナIDを取得

docker ps

CONTAINER ID   IMAGE          COMMAND       CREATED        STATUS        PORTS     NAMES
48d399085107   e2aa4ef2666c   "/bin/bash"   18 hours ago   Up 18 hours             frosty_khorana          

コンテナIDを元に、コミットして、local_mikanos イメージを作成する。

docker commit 48d399085107 local_mikanos

仮に、次回 docker run を行うときは、以下のように起動する

docker run --privileged --user vscode -v /path/to/local:/home/vscode/workspace -it local_mikanos  /bin/bash
ooharabucyouooharabucyou

Rust 環境用意

とりあえず、Rust の環境を Dockerコンテナ上に用意する。

(Container) Rust 環境用意

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# 日付を指定して、Rust の nightly をインストールする (nightly だけでも良いが、後々の変更が入ることを見込み、2022-06-20 時点では動くということを明示する)
rustup install nightly-2022-06-20
# rust-src も必要になるので用意しておく
rustup component add rust-src --toolchain nightly-2022-06-20-x86_64-unknown-linux-gnu

(Host) ローカル用のイメージを更新

Rust の環境が入った状態のものを用意しておきたいので、前述ように、ローカル用イメージを docker commit により更新する。

ooharabucyouooharabucyou

Rust でのカーネルづくり事始め

(Container) cargo の用意

Container で、~/workspace として展開されているディレクトリ上に、cargo プロジェクトを用意する。
このとき、Rust は nightly が必要なので、rust-toolchain というファイルを作り、バージョンを固定しておく。

mkdir kernel_rust
cd kernel_rust
echo "nightly-2022-06-20" > rust-toolchain
cargo init

何もしないカーネルの作成

CPUを休ませるために、hlt を呼ぶだけのカーネルを作成する。

src/main.rs
#![no_std]
#![no_main]

use core::arch::asm;
use core::panic::PanicInfo;

#[no_mangle]
extern "C" fn kernel_main() {
    unsafe {
        loop {
            asm!("hlt");
        }
    }
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}
  • no_std アトリビュート: OSを作るような場合は、OSの機能を使うような標準ライブラリを使わない
  • no_main アトリビュート: 今回は、kernel_main をエントリポイントとするので、no_main を記載する
  • no_mangle アトリビュート: 今回は kernel_main という関数をリンカが探すので、マングル処理 (コンパイラが別の名前に置き換えたりする作業) が行われると困る。このため、no_mangle を付与して、名前を換えないようにする。(別の言語から、FFI などを利用して呼び出す際などに使う)
  • extern "C": Cからコードを呼び出す際でも理解できる関数とする。
  • panic_handler アトリビュート: プログラムがパニックを起こしたときの処理。とりあえず何もしない。
  • asm!: アセンブリを呼び出す。unsafe キーワード内でないと使えない。hlt についての説明は書籍参照。

ほかにも、用意すること

ビルドに必要な情報 build と、std を使わないことにより、一部ソースから展開する必要のある機能を unstable に記載する。

.cargo/config.toml
[build]
target = "./x86_64-unknown-none-mikankernel.json"

[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]

リンカやらの設定を行う。post-link-args で、エントリポイントや、イメージのベースアドレスなどを指定している。
本でいうと、p73〜74 あたりが参考になるだろうか。

x86_64-unknown-none-mikankernel.json
{
    "arch": "x86_64",
    "code-model": "kernel",
    "cpu": "x86-64",
    "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128",
    "disable-redzone": true,
    "executables": true,
    "features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-3dnow,-3dnowa,-avx,-avx2,+soft-float",
    "linker": "ld.lld",
    "linker-flavor": "ld",
    "llvm-target": "x86_64-unknown-none-elf",
    "max-atomic-width": 64,
    "os": "none",
    "panic-strategy": "abort",
    "relro-level": "off",
    "position-independent-executables": false,
    "post-link-args": {
      "ld": [
        "--entry=kernel_main",
        "--image-base=0x100000",
        "--static",
        "--nostdlib"
      ]
    },
    "target-pointer-width": "64"
}

(Container) カーネルビルド

JSONで設定済みのため、基本的にこれだけ。便利ー。

cargo build --release

これにより、target/x86_64-unknown-none-mikankernel/release/kernel_rust に elf ファイルが展開される。
本が用意しているブートローダーは、kernel.elf というファイル名固定になっているため、一旦、cp で、kernel.elf というファイルを展開する。

cp target/x86_64-unknown-none-mikankernel/release/kernel_rust kernel.elf

起動してみると、カーネルのアドレスが記載されているので、多分できているかな?

~/osbook/devenv/run_qemu.sh ~/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi ~/workspace/kernel_rust/kernel.elf

ooharabucyouooharabucyou

FrameBuffer を受け取る

今回予め用意されている4章終了時点でのブートローダーでは、画面に何かを表示するための FrameBufferConfig 構造体が渡される。

p97〜100 あたりを参照のこと。

名前 C++上での型 意味
frame_buffer *uint8_t 画面上に表示する最初のピクセルのメモリ位置
pixels_per_scan_line uint32_t 1行あたりのピクセル数
horizotanal_resolution uint32_t 横方向 (x) のピクセル数
vertical_resolution uint32_t 縦方向 (y) のピクセル数
pixel_format enum PixcelFormat 0: RGB, 1: BGR, 他に2種類存在するようだが、この本では煩雑さの関係から対応しておらず、ブートローダーからは2種類のみ渡される

画像描画用のコードを用意する。ここらへんのコードは、以下を大いに参考にしている

https://github.com/skoji/laranja-os

まずは、構造体や列挙体を用意しておく。Rust でCポインタを扱うときは、*mut で定義する必要がある。

この辺の情報は Unsafe Rust に記載がある。

https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html

また、repr(C) をアトリビュートで指定することで、C言語の振る舞いになる。こちらについては、以下のドキュメントに記載がある。

https://doc.rust-jp.rs/rust-nomicon-ja/other-reprs.html

src/graphics.rs
#[derive(Debug, Copy, Clone)]
#[repr(u32)]
pub enum PixelFormat {
    Rgb = 0,
    Bgr,
}

#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct FrameBuffer {
    pub frame_buffer: *mut u8,
    pub pixels_per_scan_line: u32,
    pub horizotanal_resolution: u32,
    pub vertical_resolution: u32,
    pub format: PixelFormat
}

FrameBuffer の frame_buffer のメモリの先の値を書き換える実装も行う。例によって unsafe としないとコンパイルが通らない。

src/graphics.rs
impl FrameBuffer {
    pub unsafe fn write_byte(&mut self, index: usize, val: u8) {
        self.frame_buffer.add(index).write_volatile(val)
    }

    pub unsafe fn write_value(&mut self, index: usize, value: [u8; 3]) {
        (self.frame_buffer.add(index) as *mut [u8; 3]).write_volatile(value)
    }
}

一気に、いろいろ作る。PixelColor は色をRGBの3つの輝度で表す構造体。3bytesで表す。
Graphics 構造体は、FrameBuffer の管理と、ピクセルの書き出しに対応する。本でいうと、PixelWriter クラスに当たる。Graphis.pixel_writer については、コンストラクタで FrameBuffer の format に応じて、関数を渡す。

src/graphics.rs
pub struct PixelColor(pub u8, pub u8, pub u8);

pub struct Graphics {
    fb: FrameBuffer,
    pixel_writer: unsafe fn(&mut FrameBuffer, usize, &PixelColor),
}

impl Graphics {
    pub fn new(fb: FrameBuffer) -> Self {
        unsafe fn write_pixel_rgb(fb: &mut FrameBuffer, index: usize, rgb: &PixelColor) {
            fb.write_value(index, [rgb.0, rgb.1, rgb.2]);
        }
        unsafe fn write_pixel_bgr(fb: &mut FrameBuffer, index: usize, rgb: &PixelColor) {
            fb.write_value(index, [rgb.2, rgb.1, rgb.0]);
        }
        let pixel_writer = match fb.format {
            PixelFormat::Rgb => write_pixel_rgb,
            PixelFormat::Bgr => write_pixel_bgr,
            _ => {
                panic!("This pixel format is not supported by the drawing demo");
            }
        };


        Graphics {fb, pixel_writer}
    }

    pub fn write_pixel(&mut self, x: usize, y: usize, color: &PixelColor) {
        if x > self.fb.horizotanal_resolution as usize {
            panic!("bad x coord");
        }
        if y > self.fb.vertical_resolution as usize {
            panic!("bad y coord");
        }
        let pixel_index = y * (self.fb.pixels_per_scan_line as usize) + x;
        let base = 4 * pixel_index;
        unsafe {
            (self.pixel_writer)(&mut self.fb, base, &color);
        }
    }
}

main.rs で、ブートローダーから渡された、ポインタの先にある値を読み出す。このときも、unsafe を利用する必要がある。

src/main.rs
#![no_std]
#![no_main]

pub mod graphics;

use core::panic::PanicInfo;
use core::arch::asm;
use graphics::{Graphics, FrameBuffer, PixelColor};

#[no_mangle]
extern "C" fn kernel_main(fb: *mut FrameBuffer) {
    let fb_a = unsafe {*fb};
    let mut graphics = Graphics::new(fb_a);

    for x in 0..fb_a.horizotanal_resolution as usize {
        for y in 0..fb_a.vertical_resolution as usize {
            graphics.write_pixel(x, y, &PixelColor(255, 255, 255));
        }
    }

    for x in 0..200 {
        for y in 0..100 {
            graphics.write_pixel(x, y, &PixelColor(0, 255, 0))
        }
    }

    unsafe {
        loop {
            asm!("hlt");
        }
    }
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

ビルドして、起動してみると、本の4章の終わりの状態のカーネルになったことが確認できる。

cargo build --release
cp target/x86_64-unknown-none-mikankernel/release/kernel_rust kernel.elf
~/osbook/devenv/run_qemu.sh ~/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi ~/workspace/kernel_rust/kernel.elf