ゼロからのOS自作入門をRustで
を読んでいるので、Rust でカーネルを書いてみようという試み
前提として、
- ブートローダーは、この本にある EDK II 上で作成したものをそのまま利用する。カーネルのみをRustで自作する。
- RustによるOS自作の先人たちの資料として、以下を参考にしたり、時には写経・コピペを行うことがある。(ともに、BSD-3-Clause によるライセンス)
- 環境は、https://github.com/sarisia/mikanos-docker を利用する。
わたしの状況
- 普段はWebな技術者として、PHP、Ruby、Kotlin、TypeScript を扱うことが多い
- C++については、この本に従って見様見真似で書いているという感じ
- Rust に関しては、以下の本で入門した + スクラップにまとめた
ブートローダーの用意
一旦、ブートローダーについては、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
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 により更新する。
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
を呼ぶだけのカーネルを作成する。
#![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
に記載する。
[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 あたりが参考になるだろうか。
{
"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
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種類のみ渡される |
画像描画用のコードを用意する。ここらへんのコードは、以下を大いに参考にしている
まずは、構造体や列挙体を用意しておく。Rust でCポインタを扱うときは、*mut
で定義する必要がある。
この辺の情報は Unsafe Rust に記載がある。
また、repr(C) をアトリビュートで指定することで、C言語の振る舞いになる。こちらについては、以下のドキュメントに記載がある。
#[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 としないとコンパイルが通らない。
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 に応じて、関数を渡す。
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 を利用する必要がある。
#![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
先行する話題6章のUSBドライバについては、同じく C++ のものをそのまま流用する方針としたい