借り初めのひみつきち

仮ブログです。

Rust 自作 OS 日記/Part 3 ブートローダー

最近の大きな変更点は、カーネルブートローダーを分離しました!

f:id:neriring16:20200815140338p:plain

UEFI の便利なところはカーネルを直接起動できるところです。 一方、カーネルUEFI のコードが含まれることによる問題も少なからずあるので、 ブートローダーを分離することになりました。

これにより以下のような拡張の余地が生まれます(対応するとは言ってない( ー`дー´)キリッ

  • UEFI 以外のファームウェアからの起動
  • ビット数が異なる(32bit) UEFI からの起動
  • 自由なページング、自由なカーネルの配置、ASLR
  • バイナリ形式の変更(ELFなど) 、圧縮など
  • ローダーとカーネルで独立したメモリアロケーター

移行手順としては、まず以前 C で作ったブートローダーから起動できるようにカーネルの起動部分を調整、その後 Rust でほぼ同等機能のブートローダーを書き直し、両者の IF を調整しながらカーネルから UEFI 依存コードを排除という手順で移行しました。

qemu で動くようになっていざ実機で試したら NX bit のせいで起動しませんでした。 UEFI は NXE が有効になってないケースがあるようです。 でも、独自のブートローダーならカーネルの起動前にページングを操作できるのでカーネルのページ属性も自由に操作できます。

やろうと思えばここからメニューを出したりさらに高機能なブートローダーにすることもできないことはないのですが、高機能ブートローダーはいずれ別に作ることとし、カーネルを配置するだけのブートローダーはあまり前面に出てこない方がいいと思ってますヽ(•̀ω•́ )ゝ✧

Rust で ARM64-UEFI Hello World

以前書いた日記の続きになります。

neriring.hatenablog.jp

Rust ではさまざまなターゲットがあり、比較的容易にクロスコンパイルできます。 一方 UEFI も PC に依存しないように設計されていて、 x86 系とは全く別のアーキテクチャである ARM 系 CPU などにも対応しています。

つまり、 x64 用に書いた UEFI アプリは再コンパイルすればほぼそのまま ARM64(aarch64) でも動くはずです。

しかし Rust のターゲットには x64-uefi や aa64-windows があるのに aa64-uefi がありません。 Rust 自体は ARM には対応しているので、カスタムターゲットという仕組みを使えばサポートできるのでは・・・?

やってみた

まずは、 ARM64-windows のターゲット定義ファイルを取得します。

% rustc +nightly -Z unstable-options --print target-spec-json --target aarch64-pc-windows-msvc

次に、 x64-uefi のターゲット定義ファイルを取得します。

% rustc +nightly -Z unstable-options --print target-spec-json --target x86_64-unknown-uefi

これで aarch64-pc-windows-msvc.jsonx86_64-unknown-uefi.json ができました。 この2つを悪魔合体させて aarch64-unknown-uefi.json を作成します。

とりあえずビルドが通ったサンプルを置いておきます。細かい設定間違ってるかもしれないので自己責任でお願いします。

gist.github.com

完成したら、ハローワールドのフォルダにコピーして以下のようなコマンドでビルドします。

% rustup run nightly cargo xbuild --target aarch64-unknown-uefi.json --release

しばらくパソコンが頑張って、 target/aarch64-unknown-uefi/release/uefi-rs-hello.efi というファイルが出来あがれば完成です。 mnt/EFI/BOOT/BOOTAA64.EFI というファイル名に変更したら以下のようなコマンドを実行してみます。

% qemu-system-aarch64 -M virt -cpu -cortex-a57 -bios var/ovmfaa64.fd -s -drive format=raw,file=fat:rw:mnt -monitor stdio

なお、 OVMF を持ってない人は https://github.com/retrage/edk2-nightly から RELEASEAARCH64_QEMU_EFI.fd などをダウンロードして var/ovmfaa64.fd というファイルで置いておいてください。

f:id:neriring16:20200808031657p:plain

起動しました!

まとめ

UEFI アプリは再コンパイルするだけで他の CPU でもかんたんに動くことがわかりました。

ところで ARM の UEFI の将来性は?

多分これが一番無駄だと思います。

EFER.LME と EFER.LMA

EFER.LME と EFER.LMA をご存知ですか? これらのフラグは x64 CPU でロングモードの遷移に関わるフラグです。

では、2つのフラグの役割の違いをご存知ですか?

ロングモードの遷移

ロングモードの遷移についておさらいしてみましょう。 *1

まずは、 CR0 レジスタの PE フラグをセットします。 リアルモードからプロテクトモードに遷移します。

次に、 CR4 レジスタの PAE フラグをセットします。 PAE と呼ばれるページング拡張機能を有効にすることを宣言します。(しかし実際にはこの機能は使われません!)

次に、 MSR 内の EFER レジスタの LME フラグをセットします。 ロングモードを有効にすることを宣言します。しかし、この段階はまだプロテクトモードです。まだロングモードではありません。

最後に CR0 レジスタの PG フラグをセットします。 通常この操作はページングを有効にしますが、上記のフラグが全てセットされてる場合に限りロングモードに遷移します。 そしてこの時 EFER レジスタの LMA フラグが自動的にセットされます。

なお、ロングモードからプロテクトモードに戻る場合は、 CR0 レジスタの PG フラグをクリアすれば LMA フラグが自動的にクリアされてプロテクトモードに戻ることができます。

PG フラグの ON/OFF でロングモードに遷移しちゃうなら通常のページングはどうやって使えばいいのかというと、 EFER レジスタの LME フラグをクリアすれば PG フラグを再びセットした時にロングモードへの遷移が行われず通常のページングモードになります。

モード遷移するだけで何個のシステムレジスタ操作すればいいんや!!!

というのは置いておいて、以上の手順でプロテクトモードとロングモードを切り替えることができます。

さて、 LME は上記のように初期化時にシステムプログラムが書き込むわけですが、 LMA に直接書き込んでモード遷移したりできないのでしょうか?

ここで分厚いマニュアルを見てみます。

AMD の場合

f:id:neriring16:20200805235750p:plain

LME/LMA とも R/W になっているので LMA を直接書き換えることができるような感じがします。 しかし、各フラグの詳細を見てみると以下のようにあります。

f:id:neriring16:20200805235803p:plain

LMA は現在の設定値から直接書き換えると GP (一般保護例外) と明記されています。 R/W だけど勝手に書き換えはできません。

Intel の場合

f:id:neriring16:20200806000948p:plain

なんということでしょう、 LMA はリードオンリーです。 経験上書き込んでも無視されます。

けつろん

LME はロングモードを有効にするかどうか宣言するフラグ、 LMA は実際にロングモードが有効になっていることを示すフラグです。

また、 LMA は自由に書き換えることができませんが、 IntelAMD で仕様が違います。

AMD64 と Intel64 はこのように細かい違いがあります。

*1:このほかにページテーブル等の準備が必要ですが、順番が厳密に定まっているわけではないのでここでは既に準備済みとします。

Rust 自作 OS 日記/Part 2

前回の日記からまた間が空いてしまったので進捗をば。。

最近多忙につき、あまり開発時間が取れてないですが、 ウィンドウシステムっぽいものを実装始まりました

f:id:neriring16:20200726231109p:plain

ちなみに以前作ってたアレがこんな感じ

f:id:neriring16:20200726231145p:plain

まるで進化してない

見た目はだいぶそれっぽいになりましたが、 実際はウィンドウの重ね合わせとかがまだ未実装なのでマウス動かすとこんなことになっちゃいます。

f:id:neriring16:20200726231129p:plain

次の日記を書く頃にはもっと進捗出していきたい・・・ヽ(•̀ω•́ )ゝ✧

Rust 自作 OS 日記/Part 1 my new gear...

前回の日記でお察しかと思いますが、さいきん Rust + UEFI で OS がどのくらい作れるのか検証していました。

TL; DR

結論から言うとマルチスレッドが動く程度までは動いて、今後も継続して開発していこうと思います。💪(๑╹ω╹💪๑ )

Rust で OS

Rust 特有の苦労した点はスレッドの所有権周りで、よくわからないタイミングでスレッドが解放されてしまうバグに若干悩まされました。 最終的にはスレッドプールがスレッドを所有し、スケジューラーは一時的に借用する形で落ち着きました。

とはいえ、おかしなコードを書こうとするとだいたい叱ってくれるので Rust は優しいかな、と思います。

じんせいはロックフリー

Rust 固有の問題ではないところでとてもハマっていて、ロックフリーのキューを作るのに苦労しました。 当初は以前 C で作ってなんとなく動いていたものを移植したのですが、考慮漏れでデータ破壊が起きることが発覚し、 その後リストで実装し直したものも安定してるとは言えない状態です。

SMP のスケジューラのキューは複数のプロセッサから同時に読み書きされるためロックフリーで実現するのはかなり要求仕様が厳しく、 ロックフリーキューを何度か作り直して結局安定させることができませんでした。

しかし、実は同じコア内ではデータ競合が決して起こらないので、ロックしてしまえば簡単に実装できます。当面はロックするバージョンを使おうと思います。

Rust で UEFI + ACPI

uefi-rs はとりあえず Rust で UEFI プログラミングするには便利ですが、中にはちゃんと対応してない API もあります。 今のところ致命的な問題にはぶつかっていないですが、 MP protocol が non-blocking mode で使えないのがちょっと気になりました。 そもそも MP protocol のちゃんとした仕様が手に入らず SMP の初期化に使っていいのかどうかも怪しかったので、 SMP の初期化には以前書いた IPI を使ってリアルモードから起動する方法をベースに作ることにしました。

同じところで提供されている ACPI ライブラリの対応は深刻で、 FADT の内容がパース後のオブジェクトからほとんど参照できず、テーブルの自由な検索などもできず、したがって BGRT を表示するような使い方もできず、 AML パーサーは qemu でいくつかのメソッド呼び出しは成功したものの実機ではエラーでうまくパースできないという状況で、ちょっと実用に耐えないかなと思います。 いまどきの PC の OS では ACPI が必須で、とりあえず簡単に動かすためにこのライブラリを使いましたが、今後別のライブラリの導入を検討したり自作する必要がありそうです。

まとめ

そんな感じでいろいろ勉強しながら、マルチタスクでキーボードやマウスの入力ができる程度の完成度になりました。

f:id:neriring16:20200616225253p:plain

単体のプロジェクトとして名前も決まってないし当面は練習や勉強がメインになるのでレポジトリの宣伝はしません。 もう少し形になってきたら、以前作りかけた例の OS の後継としてリリースすることになりそうな気がします。

Rust で UEFI のハローワールド

さいきん Rust のべんきょうはじまりました!

Rust とは

安全に低レベルプログラミングができるナウい言語っぽいです。 メモリ管理が厳しいので初学者はコンパイルを成功させるだけでも一苦労です。

UEFI とは

2000年ごろに BIOS を代替する目的で開発された PC 向けの新しいファームウェアです。 当初はマイナーな存在でしたが Window 8 の登場とともに市販の PC に広く利用されるようになり、今後は PC の BIOS は廃止されて UEFI が搭載されることになっています。

UEFIHello World

さて、 Rust で UEFI のプログラミングするにはどうすればいいでしょうか。

まずは公式から Rust をインストールしましょう。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

インストールが終わったらシェルを再起動しましょう。

次に、 UEFI でプログラミングするには cargo-xbuild というツールを使ってクロスビルドが必要ですのでこれもインストールします。

$ cargo install cargo-xbuild

また、ベアメタルプログラミング全般で nightly のコンパイラが必要になることが多いらしいのでこれも使えるようにしておきます。

$ rustup install nightly

これでツールの用意ができました。

つぎにプロジェクトを作成します。

$ cargo new uefi-rs-hello

この時点で以下のようなツリー構造になっているはずなので uefi-rs-hello フォルダに移動します。

.
`-- uefi-rs-hello
    |-- Cargo.toml
    `-- src
        `-- main.rs

Rust で UEFI のプログラミングをするには uefi-rs というライブラリを使うのが簡単です。 Cargo.toml の [dependencies] に以下のように記述します。

[dependencies]
uefi = { git = "https://github.com/rust-osdev/uefi-rs.git" }

いよいよ src/main.rs を書いていきます。

#![feature(abi_efiapi)]
#![no_std]
#![no_main]
use uefi::prelude::*;
use core::panic::PanicInfo;
use core::fmt::Write;

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

#[entry]
fn efi_main(_handle: Handle, st: SystemTable<Boot>) -> Status {

    writeln!(st.stdout(), "Hello, world!").unwrap();

    loop {}
    // Status::SUCCESS
}

最初の3行は Rust で UEFI のプログラミングに必要なおまじないです。 #![feature(abi_efiapi)]EFIAPI を使うことを指定してるみたいです。 #![no_std]#![no_main] は通常のライブラリ(std)を使わないベアメタル環境という指定と通常のmain関数から始まらないことを指定してます。

use uefi::prelude::*;uefi-rs の基本的な定義を取り込むのに必要です。

use core::panic::PanicInfo;panic の定義まわりは Rust のエラー処理に必要です。 通常 panic は std の中に定義されていますが、今回は no_std を指定しているので自前実装が必要です。 Hello World 程度のプログラムでは panic は不要なのでただの無限ループにしてます。

use core::fmt::Write;writeln! マクロを呼び出すためのおまじないです。

#[entry]efi_main 関数がエントリポイントであることを指定します。

fn efi_main(_handle: Handle, st: SystemTable<Boot>) -> Status がメイン関数に相当する定義になります。通常の UEFI と基本的には同じ内容です。

_handle: Handle はイメージハンドルです。今回は使わないので名前の先頭に _ を付けてます。

st: SystemTable<Boot>EFI_SYSTEM_TABLE を受け取る指定です。 EFI_SYSTEM_TABLEUEFI のプログラミングにおいて最も重要な構造体で、 uefi-rs ではシステムテーブルが SystemTable<Boot>SystemTable<Runtime> の2種類あります。これはそもそも UEFI が Boot Services 環境と Runtime Services 環境の2種類あるためで、 efi_main の呼び出し時は SystemTable<Boot>st.exit_boot_services 呼び出し後は SystemTable<Runtime> が使用できます。 Rust の所有権の仕組みを利用して st.exit_boot_services 後は元の SystemTable<Boot> に誤ってアクセスできないようになっています。*1

writeln!(st.stdout(), "Hello, world!").unwrap(); がハローワールドの本体です。ここまで来るまで長かった・・・

writeln! は Rust 標準のマクロで、あとに続く文字列をフォーマットして出力します。 Rust では名前の最後に ! がつく関数っぽいものは全てマクロです。 st.stdout()EFI_SYSTEM_TABLE の中にある EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL を取得する処理です。 EFI_SYSTEM_TABLE の構造が若干 stdin/stdout を意識したような構造になっているのでこのようなメソッド呼び出しになっているのだと思います。 最後に付いてる unwrap()writeln! の出力が Result になっているためアンラップしてます。この処理がなくても警告が出るだけで一応動作します。

最後の行の loop {} は無限ループしています。コメント化している Status::SUCCESS の方に書き換えるとシェルに戻る御行儀の良い UEFI アプリケーションになりますが、ハローワールドでは必要ないでしょう。

main.rs ができたら以下のようなコマンドでビルドできます。

$ rustup run nightly cargo xbuild --target x86_64-unknown-uefi --release

初回はちょっと時間がかかり、無事ビルドが成功すると /target/x86_64-unknown-uefi/release/uefi-rs-hello.efi として出力されます。

これを qemu からファイルアクセスできるようにして実行します。

f:id:neriring16:20200517001646p:plain

いかがでしたか。

多分これが一番簡単だと思います。

*1:しかしこの仕組みだと exit_boot_services の失敗時のリカバリができないのでは...?

AssemblyScript の配列

さいきん AssemblyScript を触ってました。*1

AssemblyScript とは、 WebAssembly にコンパイルできる TypeScript のサブセット言語です。
WebAssembly の世界と JavaScript の世界は別の世界なので結構勝手が違いますが、 AssemblyScript を使うと同じ TypeScript ファイルを参照したりできるようになり、両者の溝が若干低くなります。

WebAssembly を使う時は大量のデータを高速に処理する必要がある場面が多いと思うので、配列の受け渡しは重要な要件です。しかし、実は容易ではありません。

WebAssembly.Memory

現在の WebAssembly にはオブジェクトの概念がありません。実はポインタすらありません。

あるのは int や double のような少数のスカラ値と、 WebAssembly.Memory という謎のオブジェクトだけです。

WebAssembly.Memory は内部に ArrayBuffer を持っていて、 WebAssembly 内部でメモリアクセスする命令はこの ArrayBuffer を対象とします。
現在の WebAssembly では1モジュールあたりひとつの Memory を扱うことができ、この中身をどう利用するかは WebAssembly 内部のコードの責任となっています。
WebAssembly に対応するコンパイラはこの ArrayBuffer を適当に配分してオブジェクトを割り当て、 ArrayBuffer の中の何バイト目かを示すインデックス値 (wasm32 の場合は int32) をポインタのように扱うことでオブジェクトを表現しています。意外と原始的ですね。

このインデックス値を WebAssembly の外の世界から見るとただの整数にしか見えないので、オブジェクトに変換するには WebAssembly.Memory の内部の ArrayBuffer から探さなくてはなりません。

AssemblyScript の配列

AssemblyScript の場合、exportされたクラスの情報はライブラリで吸収して JS のクラスとほとんど同じように扱うことができます。
しかし、それ以外のオブジェクトはそう単純ではありません。

オブジェクトを入出力する関数は JS から見ると number の入出力に見えます。
これは先述のように WebAssembly.Memory の内部の ArrayBuffer のインデックス値となっています。

文字列は getString みたいなユーティリティー関数でアクセスできますが、それ以外のオブジェクトはメモリから直接読み書きしないといけないようです。*2

メモリ上のオブジェクト構造は以下のページに情報があります。

Memory - The AssemblyScript Book

f:id:neriring16:20200401205336p:plain

AssemblyScript には JavaScript の Array は存在せず、実質全部 TypedArray であることがわかります。
また、通常の配列は ArrayBuffer と ArrayBufferView の二段構成になってることがわかります。

これを参考に以下のような関数を作ると AssemblyScript の配列に JavaScript からアクセスできるようになります。
this.wmem は先述の WebAssembly.Memory のことで、 instantiate 時に前もって作成するかまたは instantiate の戻り値のオブジェクトの memory というキーにあります。

getArray(ptr: number): Uint8Array {
    const buffer = this.wmem.buffer
    const view = new DataView(buffer)
    const base = view.getUint32(ptr + 4, true)
    const size = view.getUint32(ptr + 8, true)
    return new Uint8Array(buffer, base, size)
}

これで、大量のデータの入出力も ArrayBuffer や TypedArray 経由で高速にやりとりできるようになりました💡

なお、 JavaScript のメモリ管理は通常 GC を使いますが AssemblyScript は独立したリファレンスカウント方式のため、グローバル変数で固定的に確保されてないオブジェクトの扱いはサポート関数の __retain や __release で管理しないといけなくなってちょっと面倒になります💦

*1:といってもこの原稿を書き始めて既に一月近く経って成果物は公開前に開発中止しそうな勢いですが...

*2:バージョンによって違うかもしれません