ReluxidianOS作成ログ

これを参考にする。

名前はReluxでいいかなって思ってたけどもうすでにそういう名前のサービスがあるらしい
混同するからreluxidianにしよう

まずディレクトリの作成から

cargo new relux --bin

そしてmain.rsstdクレートを無効化する。

#![no_std]
 
fn main() { }

次にパニック関数を実装する。main.rs内に記述する。
no_std環境ではパニックを自身で作成する必要がある。

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

PanicInfoはパニックが発生したファイルと行などが含まれている。
!はnever型である。

次にLanguage Itemを作成する。
これはスタックアンワインドを実装するための関数である。

20241124062020-TalkWithAI-Claude-Log-スタックアンワインドとは何ですか

デフォルトでRustはパニックが起きた際にアンワインドを使用してデストラクタを実行する。
これにより使用されているメモリがすべて解放される。
デストラクタとは人間がコードを書いて、インスタンスを破棄する際に実行されるもの

しかしアンワインドは複雑なため今回は実装しない

アンワインドを無効化するにはcargo.tomlに以下の行を追加すること。

[profile.dev]
panic = "abort"
 
[profile.release]
panic = "abort"

これはdevreleaseの両方でパニックさせないようにするもの
ここでDevとはcargo buildで使用され、releasecargo build --releaseで使用されるもの

次にエントリポイントを定義していく
事実、rustのmain関数は最初に実行される関数ではない
ほとんどの言語にはランタイムシステムが存在し、これはガベージコレクションやスレッド処理などを行う。そういったものはmainの前に実行される必要がある。

Rustではcrt0(C rumtime 0)というランタイムライブラリを事前に実行する
そのあとにRustエントリポイントであるmain関数を実行する。

ここではcrt0にアクセスできないので自身でエントリポイントを定義する。

#![no_main]

ここでmainはランタイムなしでは実行されないので消しておく。

ここでエントリポイントをこのように記述する。

#[no_mangle]
pub extern "C" fn _start() -> ! {
	loop {}
}

pubはこの関数が公開であることを示している。
extern "C"はこの関数がC言語からの呼び出しを可能にするコードである。
-> !はこの関数が何も返さないことを示す。(!はNever型)
#[no_mangle]について以下、引用

Rust コンパイラが _start という名前の関数を実際に出力するように、#[no_mangle] attributeを用いて名前修飾を無効にします。この attribute がないと、コンパイラはすべての関数にユニークな名前をつけるために、 _ZN3blog_os4_start7hb173fedf945531caE のようなシンボルを生成します。次のステップでエントリポイントとなる関数の名前をリンカに伝えるため、この属性が必要となります。

ここで実行すると以下のようなエラーが出力される

PS C:\Users\rerur\.wk\OS\Relux> cargo build
   Compiling Relux v0.1.0 (C:\Users\rerur\.wk\OS\Relux)
error: linking with `link.exe` failed: exit code: 1561
  |
  = note: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\VC\\Tools\\MSVC\\14.40.33807\\bin\\HostX64\\x64\\link.exe" "/NOLOGO" "C:\\Users\\rerur\\AppData\\Local\\Temp\\rustc82ZAAZ\\symbols.o" "C:\\Users\\rerur\\.wk\\OS\\Relux\\target\\debug\\deps\\Relux.6tc9qfm8p7xs31b1gx91aijpn.rcgu.o" "C:\\Users\\rerur\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\x86_64-pc-windows-msvc\\lib\\librustc_std_workspace_core-65178e86c6c71ba8.rlib" "C:\\Users\\rerur\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\x86_64-pc-windows-msvc\\lib\\libcore-fbeb171b69c59b37.rlib" "C:\\Users\\rerur\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\x86_64-pc-windows-msvc\\lib\\libcompiler_builtins-e3a3e7896142045d.rlib" "/defaultlib:msvcrt" "/NXCOMPAT" "/OUT:C:\\Users\\rerur\\.wk\\OS\\Relux\\target\\debug\\deps\\Relux.exe" "/OPT:REF,NOICF" "/DEBUG" "/PDBALTPATH:%_PDB%" "/NATVIS:C:\\Users\\rerur\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\etc\\intrinsic.natvis" "/NATVIS:C:\\Users\\rerur\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\etc\\liballoc.natvis" "/NATVIS:C:\\Users\\rerur\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\etc\\libcore.natvis" "/NATVIS:C:\\Users\\rerur\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\etc\\libstd.natvis"
  = note: LINK : fatal error LNK1561: エントリー ポイントを定義しなければなりません。␍
 
 
error: could not compile `Relux` (bin "Relux") due to 1 previous error

これはリンカのエラーである。
リンカはプログラムがCのランタイムに依存しているとしているが、実際にはしていないのでエラーが出る。

リンカとは生成されたコードを実行可能ファイルに紐づけるプログラムのことである。

RustはデフォルトでOSにあった実行可能ファイルを出力しようとする。
x86_64のWindowsならそれ用のexeファイルをビルドする。

ここで様々な実行環境を表現するためにtarget tripleという文字列を使用する。

rustc --version --verbose

このコマンドを実行するとホストシステムのtarget tripleを確認できる。

PS C:\Users\rerur\.wk\OS\Relux> rustc --version --verbose
rustc 1.82.0 (f6e511eec 2024-10-15)
binary: rustc
commit-hash: f6e511eec7342f59a25f7c0534f1dbea00d01b14
commit-date: 2024-10-15
host: x86_64-pc-windows-msvc
release: 1.82.0
LLVM version: 19.1.1

自分の環境だとこう
自分はx84_64のWindowsを使用しているのでこういった表記になっている

target tripleとは、コンパイル対象のプラットフォームを指定するための識別文字列です。主に3つの情報(時には4つ以上)を含みます:

  1. CPU アーキテクチャ
  2. ベンダー
  3. オペレーティングシステム
  4. (オプション)ABIやその他の詳細

ちなみにx86とはIntelが開発したプロセッサアーキテクチャのことである。

ホストのtriple用にコンパイルすると、OSの基盤があると仮定してリンカエラーが発生する。
それを回避するために基盤となるOSを使用しない環境でコンパイルする。
ちなみにその環境のことをベアメタル環境という。

その様な環境の例として、thumbv7em-none-eabihf target tripleが存在する。
このtargettripleは組み込み用のものであるが、重要なのはnoneであることから基盤となるOSが存在しないことである。

コンパイルするにはrustupにこれを追加する。

rustup target add thumbv7em-none-eabihf

そして実行する。

cargo build --target thumbv7em-none-eabihf

--target引数はベアメタル用に実行可能ファイルをコンパイルする。
このシステムにはOSがなく、またCのランタイムにリンクしないためリンカエラーが発生しない。

PS C:\Users\rerur\.wk\OS\Relux> cargo build --target thumbv7em-none-eabihf
   Compiling Relux v0.1.0 (C:\Users\rerur\.wk\OS\Relux)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.33s

ここでRustで作成する最初のバイナリ構成が完成する。
次はカーネルを作成していく

最近のx86では二つのファームウェア規格が存在する。
一つはBasic Input/Output System(BIOS)であり、もう一つはUnified Extensive Firmware Interface(UEFI)である。ここではBIOSを使用する。

コンピュータを起動するとROMに保存されたファームウェアのコードを実行する。
そして使えるRAMを探し、CPUとハードウェアを事前に初期化する。その後ブータブルディスクを探し、OSのカーネルを起動する。

ブータブルディスクが見つかると、ブートローダーというプログラムに操作権が移る。
ブートローダーはアセンブリ言語であるため、今回は作成しない。

コンピュータは起動時にマザーボードにある特殊なフラッシュメモリからBIOSを読み込みます。BIOSは自己テストとハードウェアの初期化ルーチンを実行し、ブータブルディスクを探します。ディスクが見つかると、 ブートローダーbootloader と呼ばれる、その先頭512バイトに保存された実行可能コードへと操作権が移ります。多くのブートローダーのサイズは512バイトより大きいため、通常は512バイトに収まる小さな最初のステージと、その最初のステージによって読み込まれる第2ステージに分けられています。
 ブートローダーはディスク内のカーネルイメージの場所を特定し、メモリに読み込まなければなりません。また、CPUを16bitのリアルモードから32bitのプロテクトモードprotected modeへ、そして64bitのロングモードlong mode――64bitレジスタとすべてのメインメモリが利用可能になります――へと変更しなければなりません。3つ目の仕事は、特定の情報(例えばメモリーマップなどです)をBIOSから聞き出し、OSのカーネルに渡すことです。

フリーソフトウェア財団がmultibootという規格を定めている。一番人気であるのはLinuxのGNU GRUBである。

それに準拠するのは、カーネルファイルの先頭にMultiboot headerを挿入するだけである。
しかし、GRUBとmultibootにはいくつか問題がある。以下引用。

  • これらは32bitプロテクトモードしかサポートしていません。そのため、64bitロングモードに変更するためのCPUの設定は依然行う必要があります。
  • これらは、カーネルではなくブートローダーがシンプルになるように設計されています。例えば、カーネルは通常とは異なるデフォルトページサイズでリンクされる必要があり、そうしないとGRUBはMultiboot headerを見つけることができません。他にも、カーネルに渡されるブート情報boot informationは、クリーンな抽象化を与えてくれず、アーキテクチャ依存の構造を多く含んでいます。
  • GRUBもMultiboot標準規格もドキュメントが充実していません。
  • カーネルファイルからブータブルディスクイメージを作るには、ホストシステムにGRUBがインストールされている必要があります。これにより、MacとWindows上での開発は比較的難しくなっています

これらの問題のために今回は使用しない。

最初はHello Worldを出力することを目標にする。
まずターゲットの設定を行う。
x84_64-Relux.jsonファイルを作成し、その中身を記述していく。

{
    "llvm-target": "x86_64-unknown-none",
    "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
    "arch": "x86_64",
    "target-endian": "little",
    "target-pointer-width": "64",
    "target-c-int-width": "32",
    "os": "none",
    "executables": true
}

ベアメタル環境で実行するので、llvm-targetのOSとosの欄を変更している。

そして以下にビルドに関係する項目を追加する。
リンカをOSのものではなく、Rustに付随しているlldリンカを使用する設定を追加する。

"linker-flavor": "ld.lld",
"linker": "rust-lld",

さらにpanicを無効化する。

"panic-strategy": "abort",

カーネルを書くとある程度の割り込み処理をしなければならない。
その際red zoneというスタックポイント最適化を無効化する設定を追加する。
詳しくはこの項を参照

"disable-redzone": true,

さらに以下のコードも追加

"features": "-mmx,-sse,+soft-float",

featureというのはターゲットの機能を有効化したり、無効化したりするフィールドである。
ここでmmxsseを無効化し、soft-floatを有効化している。

mmxとsseはSIMD命令をサポートするかを決定するかを決めている。
以下引用。

この命令は、しばしばプログラムを著しく速くしてくれます。しかし、大きなSIMDレジスタをOSカーネルで使うことは性能上の問題に繋がります。 その理由は、カーネルは、割り込まれたプログラムを再開する前に、すべてのレジスタを元に戻さないといけないためです。これは、カーネルがSIMDの状態のすべてを、システムコールやハードウェア割り込みがあるたびにメインメモリに保存しないといけないということを意味します。SIMDの状態情報はとても巨大(512〜1600 bytes)で、割り込みは非常に頻繁に起こるかもしれないので、保存・復元の操作がこのように追加されるのは性能にかなりの悪影響を及ぼします。これを避けるために、(カーネルの上で走っているアプリケーションではなく!)カーネル上でSIMDを無効化するのです。

そしてx86_64では浮動小数点計算にSIMDを使用しているので、これが使用不可になる。
それを解決するためにsoft-floatという機能を有効化している。

ここからカーネルをビルドする。
ホストにかかわらず、エントリポイントは_startでなければいけない。

そしてこれでカーネルを、先ほどのファイルを--targetとして渡すことでビルドできるようになった。

PS C:\Users\rerur\.wk\OS\Relux> cargo build --target x86_64-Reluxidian.json
   Compiling Reluxidian v0.1.0 (C:\Users\rerur\.wk\OS\Relux)
error[E0463]: can't find crate for `core`
  |
  = note: the `x86_64-Reluxidian` target may not be installed
  = help: consider downloading the target with `rustup target add x86_64-Reluxidian`
 
For more information about this error, try `rustc --explain E0463`.
error: could not compile `Reluxidian` (bin "Reluxidian") due to 1 previous error

ただしこのままではエラーが発生してしまう。
これはcoreライブラリがないというエラーである。
これはなぜかというと、このライブラリ配布時点で各主要ターゲット向けにコンパイルされた状態で配布されているからである。
それを解消するために、再コンパイルする必要がある。

build-std機能を使用すると標準ライブラリを再コンパイルして使用することができる。
しかしこの機能はnightly版でしか使用できないので以下のコマンドを実行し、nightly版に移行する。

rustup override set nightly

これは実験的な機能を有効化するバージョンである。

そして.cargo/config.tomlに以下を記述する。

[unstable]
build-std = ["core", "compiler_builtins"]

これでcoreを再コンパイルできるようになった。後者はcoreの依存ライブラリである。
再コンパイルには、cargoがRustのソースコードにアクセスできるようにしなければならない。
以下のコマンドを打つ。

rustup component add rust-src

そしてビルドする。

cargo build --target x86_64-Reluxidian.json

これでcoreライブラリを使用することができるようになった。

  • メモリ関係の組み込み関数から続きをやる ✅ 2024-11-29

今、コンパイルしたcompiler_builtinsを使用する。
事前の段階でCのライブラリは無効化されているのでmemsetなどのメモリ操作関数が使用できない
それらを実装していく。

実はcompiler_builtinsにはメモリ操作関数が実装されている。
普段はCのライブラリと競合してしまうため無効化されている。
この無効化を解除する。
これはcargoのbuild-std-featureフラグを"compiler-builtins-mem"も設定するだけである。

# in .cargo/config.toml
 
[unstable]
build-std-features = ["compiler-builtins-mem"]

そして今の状態でビルドするには以下のようなコマンドを打っている。

cargo build --target x86_64-Reluxidian.json

これを毎回--target引数を渡すのは面倒くさいので設定する。

# in .cargo/config.toml
 
[build]
target = "x86_64-Reluxidian.json"

次は画面に出力するコードを記述していく。
今回はVGAテキストモードを使用していく。
_start関数を以下のように書き換える。

static HELLO: &[u8] = b"Hello World!";
 
#[no_mangle]
pub extern "C" fn _start() -> ! {
	let vga_buffer = 0xb8000 as *mut u8;
 
	for (i, &byte) in HELLO.iter().enumerate() {
		unsafe {
			*vga_buffer.offset(i as isize * 2) = byte;
			*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
		}
	}
 
	loop { }
}

まず、このコードについて紐解いていく。

最初の行、

static HELLO: &[u8] = b"Hello World!";

これはHELLOという変数を静的に静的に宣言している。
&[u8]は符号なし8bit変数を変数に格納している。
また&によって参照とされているため、この変数は移動も変更もできない。

let vga_buffer = 0xb8000 as *mut u8;

これは8000番のメモリのアドレスを指す変数である。
*mutはこのアドレス変数を介してその中身を変更できるということを示している。その中身はu8(8ビット符号なし整数)であることも宣言している。

VGAテキストバッファのメモリアドレス(0xb8000)へのポインタを作成します。
このアドレスは、テキストモードでの画面表示に使用される特殊なメモリ領域です。

このコードで実際にメモリ操作を行っている。

for (i, &byte) in HELLO.iter().enumerate() {
	unsafe {
		*vga_buffer.offset(i as isize * 2) = byte;
		*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
	}
}

HELLO.iter()でこの配列をイテレートしている。がこのままではイテレートできないので.enumerate()を使用する。これは各要素にindexをつけて反復処理を可能にしている。iはインデックス変数であり、&byteによって現在のバイトへの参照を渡している。

unsafeはこの中ではメモリ安全性が担保されないことを示すもの
通常Rust側で制限されるメモリ操作を可能にする。

*vga_buffer.offset(i as isize * 2) = byte;vga_textbuffer変数が示すアドレスに書き込むもの。
offset関数はポインタの位置を移動させる。i as isize * 2は文字サイズが2byteなので2byteずつ移動している。文字のあとに色を指定することができるが、それが*vga_buffer.offset(i as isize * 2 + 1) = 0xb;である。0xbはシアン色を示す。

ここで作成したコードを実行してみる。
QUMUのVM上か実際のハードウェアを使用する。
実行するにはまずブートローダが必要になる。
自前のブートローダを作成するのはとても大変なのでrustのbootloaderクレートを使用する。
このクレートはCに依存していないので使用することができる。

# in Cargo.toml
 
[dependencies]
bootloader = "0.9"

ここで実行してもブータブルディスクを作れるわけではない。
これはコンパイルしたカーネルファイルをブートローダとリンクすることができないからである。
それはcargoがカーネルのビルド実行後に処理を走らせることができないからである。
ここでそれを可能にするツールをインストールする。

cargo install bootimage

これを実行するにはllvm-tools-previewというコンポーネントをインストールする必要がある。

rustup component add llvm-tools-preview

そしてbootloaderを実行する。

cargo bootimage

このツールの動作は以下にて説明されている。

このツールが私達のカーネルをcargo buildを使って再コンパイルしていることがわかります。そのため、あなたの行った変更を自動で検知してくれます。その後、bootloaderをビルドします。これには少し時間がかかるかもしれません。他の依存クレートと同じように、ビルドは一度しか行われず、その都度キャッシュされるので、以降のビルドはもっと早くなります。最終的に、bootimageはbootloaderとあなたのカーネルを合体させ、ブータブルディスクイメージにします。

ちなみに起動するとこのようなフォルダが作成される。

次にこの作成されたイメージをQEMUで実行するには以下のコマンドを打つ。

qemu-system-x86_64 -drive format=raw,file=target/x86_64-Reluxidian/debug/bootimage-Reluxidian.bin

ちなみに実際にUSBから本当のBIOSから実行する場合にはこれを実行する。

dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=[ここにUSBのパス] && sync

もっと簡単にQEMUを使えるようにする。
それにはcargoのrunnerを設定する必要がある。

[target.'cfg(target_os = "none")']
runner = "bootimage runner"

これはosフィールドのターゲットがnoneなすべてのターゲットに適用される。
今回使用しているターゲットのフィールドもnoneである。
runnerにはcargo runを実行されたときの動作を記述する。
ここにはbootimageを実行するコマンドを打っている。

次はVGAテキストバッファについて詳しく行う。
VGAテキストモードにおいて文字を出力するにはVGAテキストバッファに値を書き込まなければならない。
引用URL

書き込むビットは以下の表に対応している。

ビット
0-7ASCII コードポイント
8-11フォアグラウンド(前景)色
12-14バックグラウンド(背景)色
15点滅

色は以下に対応している。

数字数字 + Bright BitBright明るい 色
0x00x8暗いグレー
0x10x9明るい青
0x20xa明るい緑
0x3シアン0xb明るいシアン
0x40xc明るい赤
0x5マゼンタ0xdピンク
0x6茶色0xe黄色
0x7明るいグレー0xf

VGAテキストバッファは0xb8000に対して、RAMではなくVGAのテキストバッファに直接アクセスしている。このアドレスに対して通常のメモリ操作で読み書きすることができる。

ここでRustのモジュールを作成する。

// in src/vga_buffer.rs
 
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
    Black = 0,
    Blue = 1,
    Green = 2,
    Cyan = 3,
    Red = 4,
    Magenta = 5,
    Brown = 6,
    LightGray = 7,
    DarkGray = 8,
    LightBlue = 9,
    LightGreen = 10,
    LightCyan = 11,
    LightRed = 12,
    Pink = 13,
    Yellow = 14,
    White = 15,
}

ここでRustコンパイラは使用されていないenumの要素に対して警告を出すが、#[allow(dead_code)]属性をつけることでその警告を消すことができる。

#[derive(Debug, Clone, Copy, PartialEq, Eq)]は記事内でこのように説明されている。

CopyCloneDebugPartialEq、および Eqderiveすることによって、この型のコピーセマンティクスを有効化し、この型を出力することと比較することを可能にします。

この時、Debugはこの列挙型の値を出力できるようにするもの。
Cloneは列挙型インスタンスに対してclone()メソッドを使用可能にする。
Copyはこの型の値をコピー可能にする。
PartialEq及びEqは等価比較(==)を提供する。

そして実際のカラーコードのフォーマットに合わせるためにColorCode構造体を作成。

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
 
impl ColorCode {
    fn new(foreground: Color, background: Color) -> ColorCode {
        ColorCode((background as u8) << 4 | (foreground as u8))
    }
}

これは上位4bitに背景色を、下位4bitに文字色を設定するものである。

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
    ascii_character: u8,
    color_code: ColorCode,
}
 
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
 
#[repr(transparent)]
struct Buffer {
    chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}

画面上の文字とバッファを示す構造体を追加する。
これは25行から80行の二次元配列を作成している
#[repr(C)]: Cの構造体レイアウトと互換性のあるメモリレイアウトを保証するもの

Rustにおいて、デフォルトの構造体におけるフィールドの並べ方は未定義なので、repr(C)属性が必要になります。

pub struct Writer {
    column_position: usize,
    color_code: ColorCode,
    buffer: &'static mut Buffer,
}

Writer構造体は画面に書きだすための情報を保持する、
column_positionは現在の位置を保持する。
バッファへの参照はbufferに格納されている。
この時、参照の有効性を&' staticで明示する。

ここでWriter構造体に対してimplを追加する。

impl Writer{
    pub fn write_byte(&mut self, byte: u8){
        match byte {
            b'n' => self.new_line(),
            byte => {
                if self.column_position >= BUFFER_WIDTH {
                    self.new_line();
                }
 
                let row = BUFFER_HEIGHT - 1;
                let col = self.column_position;
 
                let color_code = self.color_code;
 
                self.buffer.chars[row][col] = ScreenChar {
                    ascii_character: byte,
                    color_code
                };
 
                self.column_position += 1;
            }
            
        }
    }
 
    fn new_line(&mut self) { }
}
 

引数が改行コードの場合、何も出力しない。
代わりにnew_lineメソッドを実行する。

改行コードでない場合、現在の縦の行が最大値を超えていない場合、改行する。
そして、バッファ変数に文字コードと色コードを指定して出力するための情報を格納する。

書き込むためには以下のコードを使用する。

pub fn write_string(&mut self, s: &str){
	for byte in s.bytes(){
		match byte {
			0x20..=0x7e | b'\n' => self.write_byte(byte),
			_ => self.write_byte(0xfe),
		}
	}
}

まだバッファメモリにはアクセスしていない。
これは文字列を渡された際にその文字列をイテレートして、変数に格納する。

ここで0xb8000にアクセスする変数を渡す一時的な関数を作成する。

pub fn print_something() {
    let mut writer = Writer {
        column_position: 0,
        color_code: ColorCode::new(Color::Yellow, Color::Black),
        buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
    };
 
    writer.write_byte(b'H');
    writer.write_string("ello ");
    writer.write_string("Wörld!");
}

この関数は先ほどまでに作成したWriter構造体を使用する。
bufferの中にアドレスを代入している。
可変な生ポインタにキャストしている。
O
この状態で文字を表示することには成功した。
だたしこの状態ではBufferに書き込むことはできているが読み取ることはできていない。
このVGAバッファとはいわば副作用なのである。
これに対し、コンパイラはこの副作用に対して最適化を施し、省略してしまうかもしれない。
それらの書き込みをVolatileとして指定することで回避可能である。
Volatileクレートを使用して読み込み、書き込みを取り除かれないようにする。

まず、cargo.tomlに依存関係を書き込む。

[dependencies]
volatile = "0.2.6"

そしてBufferクラスをVolatileクラスでラップする。

use volatile::Volatile;
 
struct Buffer {
    chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
}

Volatile型を使用することによって書き込みの正しさを証明できている。
これにより書き込みはVolatileのwriteメソッドを使用する。

impl Writer {
    pub fn write_byte(&mut self, byte: u8) {
        match byte {
            b'\n' => self.new_line(),
            byte => {
                ...
 
                self.buffer.chars[row][col].write(ScreenChar {
                    ascii_character: byte,
                    color_code,
                });
                ...
            }
        }
    }
    ...
}

次にフォーマットマクロを定義する。
フォーマットマクロを定義することによって整数や浮動小数を簡単に出力することができる。
core::fmt::Writeトレイトを定義する。

use core::fmt;
 
impl fmt::Write for Writer {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        self.write_string(s);
        Ok(())
    }
}

この実装の主な目的は、Writer構造体にfmt::Writeトレイトの機能を追加することです。これにより、write!マクロやformat!マクロなどのフォーマット関連の機能をWriterで使用できるようになります。

このコードにより、Rust組み込みのwrite!writeln!が使用可能となった。

write!(writer, "Lyrics is {}", 58).unwrap();

ここでunwarap()関数が書かれているのは、Result型がエラーだった場合にパニックさせる関数である。通常RustコンパイラはResultが放置されている場合に警告を出す。

次に改行を実装していく。
今のコードでは行に収まらない文字列は無視している。
これを実装していくために、Writerにnew_lineメソッドを作成する。

fn new_line(&mut self) {
	for row in 1..BUFFER_HEIGHT {
		for col in 0..BUFFER_WIDTH {
			let character = self.buffer.chars[row][col].read();
			self.buffer.chars[row - 1][col].write(character);
		}
	}
	self.clear_row(BUFFER_HEIGHT - 1);
	self.column_position = 0;
}
 
fn clear_row(&mut self, row: usize){
	//TODO
}

この関数はバッファ内の文字を一字ずつイテレートしてそれぞれの文字を一字ずつ上に上げる関数である。

このclear_rowを実装していく。

fn clear_row(&mut self, row: usize){
	let blank = ScreenChar{
		ascii_character: b' ',
		color_code: self.color_code
	};
 
	for col in 0..BUFFER_WIDTH {
		self.buffer.chars[row][col].write(blank);
	}
}

この関数は最初に空白 のアスキーを格納し、それを横幅いっぱいに書き込むことで行をクリアする。

次にWriterインスタンスを使いまわせるように大域的なインターフェースとして定義する。

pub static WRITER: Writer = Writer{
    column_position: 0,
    color_code: ColorCode::new(Color::Yellow, Color::Black),
    buffer: unsafe {
        &mut *(0xb8000 as *mut Buffer)
    },
};

しかしこのままではエラーが発生してしまう。

PS C:\Users\rerur\.wk\OS\Relux> cargo run
   Compiling Reluxidian v0.1.0 (C:\Users\rerur\.wk\OS\Relux)
error[E0015]: cannot call non-const fn `ColorCode::new` in statics
   --> src\vga_buffer.rs:140:17
    |
140 |     color_code: ColorCode::new(Color::Yellow, Color::Black),
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: calls in statics are limited to constant functions, tuple structs and tuple variants
    = note: consider wrapping this expression in `std::sync::LazyLock::new(|| ...)`
 
For more information about this error, try `rustc --explain E0015`.
error: could not compile `Reluxidian` (bin "Reluxidian") due to 1 previous error

この問題はRustコンパイラがコンパイル時に生ポインタへと参照が変えられないことが原因である。
静的変数static内でconst以外の変数が存在してはいけないことになっている。

この問題を解決するには、lazy_staticクレートを使用する。

[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]

このクレートを追加すると、このlazy_staticによって正義された静的変数はコンパイル時に初期化されるのではなく、変数アクセス時に初期化される。よって、前述のエラーがなくなる。
標準ライブラリは使用できないので、[spin_no_std]オプションをつける。

use lazy_static::lazy_static;
lazy_static!{
    pub static ref WRITER: Writer = Writer{
        column_position: 0,
        color_code: ColorCode::new(Color::Yellow, Color::Black),
        buffer: unsafe {
            &mut *(0xb8000 as *mut Buffer)
        },
    };
}

しかしこのWriter変数はまともに使用できない。
なぜなら不変であるからである。これを可変にしてしまうとあらゆるデータが容易に書き込めるようになってしまう。内部可変性で不変静的変数である型、例えばRefCellやUnsafeCellなどが存在するが、それらはスレッド間で安全に共有できないので静的変数で使用することはできない。

ではそれらの問題を解決する。
スレッド間で同期的な静的変数を使用する場合は標準ライブラリが使用できるならMutexライブラリが存在する。このライブラリはリソースがロックされていた場合にスレッドをブロック(待機状態)にすることで相互排他性を提供する。しかし、今作成しているカーネルにはスレッドもブロッキング機能も存在しないので使用することができない。しかし、古典的なコンピュータの世界ではOSも使用しない方法でmutex機構を使用することができる。それがspinlockである。スピンロックとは、ブロックする代わりにリソースに対して何度もロックを試みることでmutex機構が解放されるまでCPUリソースを食い尽くす手法である。
mutexMutual Exclusion(相互排他)の略で、一度に一つのスレッドのみがリソースにアクセスできるようにするものである。あるスレッドがmutexをロックすると、ほかのスレッドはそのリソースにアクセスすることができなくなる。そしてロックが解放されるまでそのスレッドは待機する。

では実際にこのカーネルにスピンロックを実装していく。
spinクレートを使用すると簡単なのでこれを使用していく。

# Cargo.toml
[dependencies]
spin = "0.5.2"

スピンを使用したmutexが使用可能になったのでWriterに安全な内部可変性を追加することができる。
内部可変性についての詳しい説明は以下のリンクを参照

Mutexでラップする。

use spin::Mutex;
use lazy_static::lazy_static;
lazy_static!{
    pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer{
        column_position: 0,
        color_code: ColorCode::new(Color::Yellow, Color::Black),
        buffer: unsafe {
            &mut *(0xb8000 as *mut Buffer)
        },
    });
}

それをmain関数でも実装していく。

#[no_mangle]
pub extern "C" fn _start() -> ! {
    use core::fmt::Write;
    vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
    write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
 
    loop {}
}

writeを使用するにはWriteトレイトをインポートする必要がある。
ここでWriterに対してlock()関数を使用している。
これはmutexをロックする関数である。共有リソース、この場合はWRITERへの排他的アクセスを提供している。ブロッキング方式ではなく、スピン方式でリソースをロック(=一度に一つのスレッドのみがリソースを操作できる状態)にしている。

そして大域的に使用することができるWriterを手に入れたのでprintlnマクロを作成していく。
標準ライブラリでは以下のような実装になっている。

#[macro_export]
macro_rules! println {
    () => (print!("\n"));
    ($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}

この実装では引数がない場合では改行を出力するだけである。
引数がある場合にはこれをprintマクロに引き渡し、改行を出力している。
#[macro_export]は定義したマクロを外部で使用することを許可する属性である。

ここprint!マクロの中身も観てみる。

#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}

以下、引用

このマクロはioモジュール内の_print関数の呼び出しへと展開しています。$crateという変数は、他のクレートで使われた際、stdへと展開することによって、マクロがstdクレートの外側で使われたとしてもうまく動くようにしてくれます。
format_argsマクロが与えられた引数からfmt::Arguments型を作り、これが_printへと渡されています。libstdの[_print関数]はprint_toを呼び出すのですが、これは様々なStdoutデバイスをサポートいているためかなり煩雑です。ここではただVGAバッファに出力したいだけなので、そのような煩雑な実装は必要ありません。

VGAバッファに出力する場合はそれらをコピーしVGA用に改変するだけである。

#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
 
#[macro_export]
macro_rules! println {
    () => ($crate::print!("\n"));
    ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
 
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
    use core::fmt::Write;
    WRITER.lock().write_fmt(args).unwrap();
}

通常では外部クレート(libstd)の_print関数を使用しているが、それをWriterのWriteを使用する関数に変更している。 

ここで#[doc(hidden)]属性とは、cargo docによって生成されるドキュメントに生成されないようにするものである。

そしてようやくmainprint!マクロを使用することができるようになった。

#[no_mangle]
pub extern "C" fn _start() -> ! {
	println!("HELLO WORLD{}", "!");
 
	loop { }
}

パニックメッセージの詳細もこれにて実装することができる。

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
	println!("{}", info);
	loop{}
}

次はRustで行うテストを作成していく
ただ、Rustでは標準でテストクレートが組み込まれており、#[text]属性を関数につけると、cargo testを実行した際に、自動的に実行される。

ただし今回では標準ライブラリをすべて無効化している環境なので別の方法で行うことになる。
そこで今回はcustom_test_frameworks機能を使用して標準のテストフレームワークを置き換えて行う。

独自のテストを作成するためにmain.rsに以下を記述する。

#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
 
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
    println!("Running {} tests", tests.len());
    for test in tests {
        test();
    }
}

ここで以下のようなエラーが発生した。

PS C:\Users\rerur\.wk\OS\Relux> cargo run
   Compiling Reluxidian v0.1.0 (C:\Users\rerur\.wk\OS\Relux)
error: an inner attribute is not permitted in this context
  --> src\main.rs:8:1
   |
8  |   #![feature(custom_test_frameworks)]
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
11 | / pub fn test_runner(tests: &[&dyn Fn()]) {
12 | |     println!("Running {} tests", tests.len());
13 | |     for test in tests {
14 | |         test();
15 | |     }
16 | | }
   | |_- the inner attribute doesn't annotate this function
   |
   = note: inner attributes, like `#![no_std]`, annotate the item enclosing them, and are usually found at the beginning of source files
 
error: an inner attribute is not permitted in this context
  --> src\main.rs:9:1
   |
9  |   #![test_runner(crate::test_runner)]
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 |   #[cfg(test)]
11 | / pub fn test_runner(tests: &[&dyn Fn()]) {
12 | |     println!("Running {} tests", tests.len());
13 | |     for test in tests {
14 | |         test();
15 | |     }
16 | | }
   | |_- the inner attribute doesn't annotate this function
   |
   = note: inner attributes, like `#![no_std]`, annotate the item enclosing them, and are usually found at the beginning of source files
 
error: could not compile `Reluxidian` (bin "Reluxidian") due to 2 previous errors

これは#![]属性は必ずファイルの一番上に書かなければいけないことに起因するエラーである。

そしてこのようなエラーも発生した。

duplicate lang item

これはCargo.toml内のpanic=abortの文を削除することで正常に動作するようになる。

今度は正常に動作するようになるが、cargo testのコマンドを実行しても_startの中身が実行されてしまう。これは依然としてプログラムのエントリポイントが_startに設定されているためである。
独自のフレームワーク機能はtest_runnerを呼び出すmain関数を生成するが、作成したカーネルはno_mangleを使用して独自のエントリポイントを定義しているのでこれが無視される。

これを解決するにはreexport_test_harness_main属性を使用して、cargo testのエントリポイントをmainとは違うものに変更する必要がある。

そしてそれで改名された関数を_startから呼び出して完成する。

#![reexport_test_harness_main = "test_main"]
 
#[no_mangle]
pub extern "C" fn _start() -> ! {
    println!("Hello World{}", "!");
 
    #[cfg(test)]
    test_main();
 
    loop {}
}

これはテストフレームワークのエントリポイントをtest_mainに設定しているものである。

これでようやくtest_runnerが実行されるようになった。
ここでテストを作成してみる。

#[test_case]
fn trivial_assertion() {
    print!("trivial assertion... ");
    assert_eq!(1, 1);
    println!("[ok]");
}

これはassert_eq関数を使用してテストを実行しているものである。asser_eq関数は二つの引数の値が同じかどうか判定し、違う場合はパニックを出す関数である。
試しに第二引数を2に変更すると以下のような出力がされる。

okなら高出力される。

このtest_runnerだが、テスト実行後はtest_main関数から_start関数にリターンする。
しかし、_startは最後にloopで無限ループしている。しかし、テスト実行後は終了してほしい。
QEMUを手動で終了させないでOSを適切にシャットダウンする方法はかなり複雑で、APMACPIというパワーマネジメント標準規格へのサポートを実装する必要がある。

ただし、QEMUにはゲストシステムからQEMUをシャットダウンできる方法が提供されている。
Cargo.tomlpackage.metadata.bootimage.test-args設定キーを追加することで行うことができる。

[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit, iobase=0xf4, iosize=0x04"]

bootimage runnerは、test-argsをすべてのテスト実行可能ファイルの標準QEMUコマンドに追加します。通常のcargo runのとき、これらの引数は無視されます。
デバイス名 (isa-debug-exit) に加え、カーネルからそのデバイスにたどり着くための I/Oポート を指定するiobaseiosizeという2つのパラメータを渡しています。

CPUと周辺機器が通信するにはmemory-mappedメモリマップされた I/O と port-mappedポートマップされた I/Oの2種類ある。ただし、memory-mappedにはすでにVGAバッファで使用しており、0xb8000を介してVGAデバイスのメモリにアクセスしている。

メモリマップIOとは周辺機器のレジスタやメモリをCPUのメモリアドレス空間の一部として扱う方式である。アドレス0xb8000にアクセスすると、実際のRAMではなくVGAデバイスのメモリにアクセスする。

ポートマップIOとは、周辺機器用に別個のIOアドレス空間を用意するものである。これは通常のメモリアドレス空間とは独立している。

ここでisaの機能を説明する。
これはvalueという値がiobaseオプションによって指定されたI/Oポートに書き込まれた段階でQEMUは終了ステータスを(value << 1) | 1にして終了する。

よってvalueが1なら(1 << 1) | 1 = 3で終了する。

I/Oポートへの書き込みはinoutという特別なCPU命令を使用する必要がある。
ここではx86_64クレートで提供されている抽象化されたPort構造体を使用して行う。
まずは依存関係を追加する。

[dependencies]
x86_64 = "0.14.2"

このクレートから提供されているPort型を使用してQEMUを終了する関数を作成する。

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
    Success = 0x10,
    Failed = 0x11,
}
 
pub fn exit_qemu(exit_code: QemuExitCode) {
    use x86_64::instructions::port::Port;
 
    unsafe {
        let mut port = Port::new(0xf4);
        port.write(exit_code as u32);
    }
}

この関数はオプションで指定したiobase(0xf4)に渡された終了コードを書き込むものである。
ここでiosize=0x04と指定しているのでu32で4バイトで指定している。

終了コードはこちら側で実装する。
ここでQEMUの終了コードとかぶってしまわないように0x100x11を使用している。
もしQemuExitCode0x00を指定すると、これはQEMUが実行に失敗したときのコードなのでよくない。

これでテストが実行された後にQEMUが終了するようにコードを修正する。

fn test_runner(tests: &[&dyn Fn()]) {
    println!("Running {} tests", tests.len());
    for test in tests {
        test();
    }
    /// new
    exit_qemu(QemuExitCode::Success);
}

これでccargo testを実行すると以下のようなエラーが返ってくる。

warning: `Reluxidian` (bin "Reluxidian" test) generated 3 warnings (run `cargo fix --bin "Reluxidian" --tests` to apply 1 suggestion)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.63s
     Running unittests src\main.rs (target\x86_64-Reluxidian\debug\deps\Reluxidian-d5409b96fdc17d83)
Building bootloader
   Compiling bootloader v0.9.29 (C:\Users\rerur\.cargo\registry\src\index.crates.io-6f17d22bba15001f\bootloader-0.9.29)
    Finished `release` profile [optimized + debuginfo] target(s) in 2.19s
Running: `qemu-system-x86_64 -drive format=raw,file=C:\Users\rerur\.wk\OS\Relux\target\x86_64-Reluxidian\debug\deps\bootimage-Reluxidian-d5409b96fdc17d83.bin -no-reboot -device isa-debug-exit, iobase=0xf4, iosize=0x04`
qemu-system-x86_64: -device isa-debug-exit, iobase=0xf4, iosize=0x04: Property 'isa-debug-exit. iosize' not found
error: test failed, to rerun pass `--bin Reluxidian`
 
Caused by:
  process didn't exit successfully: `bootimage runner C:\Users\rerur\.wk\OS\Relux\target\x86_64-Reluxidian\debug\deps\Reluxidian-d5409b96fdc17d83` (exit code: 1)
note: test exited abnormally; to see the full output pass --nocapture to the harness.

これの問題はcargo testが0以外の終了コードを全てエラーと認識してしまうことにある。今回の成功コードは0x10なのでcargo側ではエラーと認識されてしまう。

今回、bootimageには指定された終了コードを0へとマップする機能がついているのでそれを利用する。

[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33

33という数字は、以下のような計算で導き出されている: (0x10 << 1) | 1
これを分解すると:

  1. 0x10は16進数で16
  2. << 1は1ビット左シフト(つまり16 * 2 = 32)
  3. | 1は1とのビット単位OR演算(32 | 1 = 33)

これでエラーが出なくなり、正常にQEMUを終了することができるようになった。

これをカーネルからデータを送信して、ホストシステム(今回の場合はQEMUを実行しているWindows)のコンソールで表示する方法として、今回はシリアルポートを使用する。
これを実装しているチップはUARTと呼ばれるもの。
今回はuart_16550クレートを使用する。

[dependecies]
uart_16550 = "0.2.0"

このクレートにはUARTレジスタを表現するSerialPort構造体が存在する。このインスタンスをまず作成する。
main.rsに依存関係を追加する。

mod serial;

そして新しく作成したserial.rsファイルに以下を記述する。

use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
 
lazy_static! {
    pub static ref SERIAL1: Mutex<SerialPort> = {
        let mut seral_port = unsafe {
            SerialPort::new(0x3F8)
        };
        seral_port.init();
        Mutex::new(seral_port)
    };
}

lazy_staticとスピンロックを使用してstaticなレジスタインスタンスを作成している。