借り初めのひみつきち

仮ブログです。

3種のビットマップ

昨日の日記で何故ビットマップクラスが3種類必要になるのか少し補則します。

生ポインタと Box

旧 MYOS のビットマップは生ポインタでデータを管理していました。 これは所有権もライフタイムも Rust によって管理されておらず借用の制限もなかったので C 言語とあまり変わらない危険な状態でした。

TOE のビットマップクラスで旧 MYOS に一番近いのは、 Box でヒープに所有しているビットマップ (BoxedBitmap) です。 所有権とライフタイムを Rust が管理しているのが大きな違いです。

pub struct BoxedBitmap8<'a> {
    inner: Bitmap8<'a>,
    slice: UnsafeCell<Box<[IndexedColor]>>,
}

BoxedBitmap を直接可変借用すれば通常の描画は大抵事足りそうですが、いくつか問題があります。

可変借用

フレームバッファや他のビットマップの一部を切り取った View オブジェクトはデータをヒープに所有していないので BoxedBitmap で扱うことができません。 ウィンドウの描画などは描画する範囲だけ切り取った View を多用します。

Rust ではヒープに確保するクラスとそうではないクラスは通常明確に分かれています。 そこで、普段の描画はビットマップデータを可変借用スライスで扱うクラスを使います。基本の描画クラスになるので名前もシンプルに Bitmap にしました。

pub struct Bitmap8<'a> {
    width: usize,
    height: usize,
    stride: usize,
    slice: UnsafeCell<&'a mut [IndexedColor]>,
}

BoxedBitmap は内部に Bitmap を保持しているのでゼロコピーで借用できます。

不変借用

Rust は可変借用に対して制限が厳しい言語です。 blt のソースに使う画像などは可変借用する必要性がないので不変借用したいです。

また、ソースファイルにパターンデータを記述してそのまま blt するようなプログラムも簡単に記述したいので定数でデータを保持したいですが、 BoxedBitmap や Bitmap は内部で可変のビットマップデータを管理しているので、不変のデータを扱うことができません。

このような状況では、内部で不変スライスを借用した定数ビットマップ (ConstBitmap) を使用します。

pub struct ConstBitmap8<'a> {
    width: usize,
    height: usize,
    stride: usize,
    slice: &'a [IndexedColor],
}

BoxedBitmap や Bitmap は ConstBitmap に変換して借用できます。

いかがでしたか?

このように、状況に応じて3種類のビットマップオブジェクトを定義して使い分けることで、さまざまなケースで効率的にデータを扱うことができるようになります。

ビットマップクラス統合

近代的な OS は一部の組み込み用途をのぞくと画像処理が不可欠です。 MYOS や TOE も画像処理にそれなりの割合を割いています。

MYOS と TOE の大きな違い

MYOS の主要なターゲットは UEFI で、 UEFI では 32bit ARGB 形式が標準です。 また、近代的な画像処理は 32bit ARGB 形式で扱うことが多いので、 MYOS では 32bit ARGB で描画します。

また、 MYOS は Rust を勉強し始めて借用とライフタイムの理解が浅い頃に作り始めてとりあえずビットマップ出力できないと何も進まない状況だったため、ビットマップデータを生ポインタで管理していてあまりお行儀のいい設計ではありませんでした。

pub struct Bitmap {
    base: *mut u32,
    size: Size,
    stride: usize,
    flags: BitmapFlags,
}

一方、 TOE の主要なターゲットは古き良き BIOS 、 PC-9821、FM TOWNS と幅広くなっており、これらの機種で共通して使えて変な癖の少ないモードは 8bit インデックスカラーモードなので、 TOE は 8bit インデックスカラーで描画します *1

また、 TOE を始める頃には借用やライフタイムなどの理解がより深まっていたので、 MYOS 時代よりも Rust らしいクラス設計になっています。

pub struct Bitmap8<'a> {
    width: usize,
    height: usize,
    stride: usize,
    slice: UnsafeCell<&'a mut [IndexedColor]>,
}

TOE は基本的にほとんどの部分で MYOS のサブセットとなっていますが、以上の経緯により描画周りに関して全く互換性がありませんでした。

MYOS はアプリ実行環境で 8bit カラー対応する必要があったために別のビットマップクラスを追加し、 TOE は 8bit カラー以外の環境でも動作するように 32bit カラービットマップ描画にも対応する必要が出てきました。

同じようなクラスを何個も作るのはうんざりしますね。

ということで、これらの描画ライブラリをひとつの同じライブラリで統合したいです。 MYOS よりも TOE の方が洗練されているので TOE をベースに統合することになりました。

統合方針

設計方針として、まずは色モードに非依存の統合した抽象クラスを作り、実際のデータ保持や描画を担当する 8bit カラー用のビットマップクラスと 32bit カラー用のビットマップクラスを作ります。 MYOS では多くの場面で引き続きそのまま 32bit クラスを使用し、 TOE は基本的には抽象クラスで受け渡しをして実際に描画するときに 8bit カラーのクラスと 32bit カラーのクラスに分岐することにします。

pub enum Bitmap<'a> {
    Indexed(&'a mut Bitmap8<'a>),
    Argb32(&'a mut Bitmap32<'a>),
}

Rust らしさやパフォーマンスを考慮すると、 blt などで不変借用する Const クラス、描画用に可変借用する Mutable クラス、ウィンドウバッファなどヒープに動的確保するクラスの3種類欲しいです。 ヒープクラスは一時的に借用できますし、可変借用は不変借用に変換できますが、逆方向の変換はできません。

3色3種類で単純に9種類のクラスが必要になることがわかるかと思います。実際はもう少し複雑ですが。

9種類の似たようなクラスがバラバラに存在していると実装効率が悪いので、ある程度同じ特性を持ったクラスを Trait でまとめたり Generics を使って実装します。 また、互換性のあるクラスへのデータ変換が可能なものについては変換用のメソッドも実装します。

これらのことを続けた結果、かなりたくさんの Trait と実装になってしまいましたが、 TOE でも 32bit カラーモードで描画できるようになりました🎉

地獄の統合作業

次はこれを MYOS に移植します。

MYOS のビットマップオブジェクトは先述のように生ポインタを保持していたので、かなりの部分で借用とライフタイムの扱いを誤魔化していました。 過去の過ちを正すときが来ました。

とりあえず単純に移植しただけで100を超えるエラーメッセージの祝福を受けました。

エラーの内容は、クラス構造が変わったことによるメソッド名や型名のエラーと、借用とライフタイム違反のエラーに大別されます。 前半のエラーは対応する別の名前に置き換えれば大抵解決しますが、後者は少しずつ修正してエラーが減ってくると突然新しいエラーがどんどん生まれてきます。

Rust は借用とライフタイムの扱いがかなり厳しいので適当に設計するとコンパイルが通らず正解を見つけるまでに丸1日費やすこともあります。 とはいえ MYOS のコードを TOE に移植するときに大半の問題は解決済みなので、ひとつずつ地道に修正してついにビルドに成功しました🎉

f:id:neriring16:20210316110121p:plain

MYOS のフォント描画クラスがちょっと特殊だったのでまだ完全に移行できていませんが、それ以外は概ね TOE と MYOS で共通の描画コードが使えるようになって保守性が上がりました。

Rust は Borrow Checker 完全理解期と 'lifetime ナンモワカラン期が定期的に繰り返しやってきて難しいですね😿

*1:より高い互換性を考慮すると 4bit カラーモードなどが視野に入ってきますが、 8bit 以下のカラーモードは機種ごとにアクセス方法が異なりコードが複雑難解になるので除外しました

アクティビティモニターの作り方

MYOS や TOE にはアクティビティモニターがあります。

これらは見せ方が若干異なりますが、定期的にスケジューラーが集計したデータをそのまま表示しているだけです。

f:id:neriring16:20210311195817p:plain

f:id:neriring16:20210311200443p:plain

では、スケジューラーはどのように集計しているのでしょうか?

CPU時間

スレッドの切り替えのタイミングを知っているのはカーネルのスケジューラーなので、 スケジューラーが実際にスレッドを切り替える瞬間にシステムの時間を計測すればスレッドの実行時間を測定できます。

下の例のようなタイミングでスレッド切り替えをしたとします。(実際のOSは100倍〜1000倍くらい高速に切り替えます)

時間 切り替え
0秒 起動してスレッドAを実行
5秒 スレッドA→Bに切り替え
10秒 スレッドB→Cに切り替え
20秒 スレッドC→Aに切り替え

まずはスレッドAからBに切り替わるときシステム時間が 5 - 0 = 5 秒経過していたのでスレッドAの実行時間は 5 秒、次にスレッドCに切り替わるときも 10 - 5 = 5 秒経過しているのでスレッドBの実行時間は5秒、最後にスレッドCからAに戻ってきたときは 20 - 10 = 10 秒経過していたのでスレッドCの実行時間は10秒ということがわかります。

これらのデータを累算していくことで各スレッドが消費した CPU 時間がわかります。 また、ここで測定したデータが以降のデータの基になります。

各スレッドの CPU 利用率

各スレッドの CPU 利用率は時間の経過とともに変化する値で、一定の時間内にそのスレッドが利用した CPU 利用時間の割合に等しくなります。

先述の CPU 利用時間の累積値を一定の時間ごとにリセットすると、そのスレッドが一定時間のうち実際に実行された CPU 時間がわかります。 これを % で表示したものが各スレッドの CPU 利用率になります。 MYOS や TOE では1秒ごとに集計する専用のスレッド (Statistics) があります。

システム全体の CPU 利用率とアイドル率

他のすべてのスレッドが待機状態のときに HLT 命令を実行して CPU を休ませる専用のスレッドをアイドルスレッドと言います。

アイドルスレッドが実行されている時間は CPU が休んでる時間で、アイドルスレッドが実行されていない時間は CPU が忙しい時間ということになります。 つまり、アイドルスレッド以外のスレッドの CPU 利用率の合計がシステム全体の CPU 利用率になります。

なお、システム全体の CPU 利用率とアイドルスレッドの CPU 利用率を合計すると理論上 100% になるはずですが、集計のタイミングや細かい誤差などによって若干ズレが発生します。

CPU コアごとの CPU 利用率

MYOS は SMP に対応しているのでコアごとの CPU 利用率も計測できます。

MYOS ではコアごとにコア専用のアイドルスレッドがあります。 それぞれのアイドルスレッドとコアが 1:1 で紐づいているので、各アイドルスレッドの CPU 利用率がそのままコアごとのアイドル率になり、それを 100% から引いたものがそのコアの CPU 利用率ということになります。

また、マルチコア環境ではシステム全体の CPU 利用率の合計が 100% を超えることになりますが、 MYOS のアクティビティモニターでは平均値を表示しているので 100% を超えることはありません。

CPU 利用率グラフ

以上の情報を定期的に収集してグラフを描画すれば利用率のグラフになります。

いかがでしたか?

このように集計することで、それぞれのスレッドの負荷を測定したりスケジューラーが正しくスケジューリングしているか確認することができます。

なお、 TOE は時間計測に PIT を利用しているので精度が低い (1ms) ですが、 MYOS は HPET を利用しているので TOE よりもかなり正確に (1us) 測定することができます。

CD から OS を起動する

昔の OS は CDROM で提供されていました。

TOE は標準でフロッピーから起動しますが、さいきん入手難しくなってきたので CD 起動に対応したいと思います。

ロメオとジョリエット

CDROM のファイルシステムはたくさんの規格が複雑にからんでいます。 CDROM は特定の OS に依存せず他機種間でデータ交換が容易になるよう最大公約数的な仕様が決められたので、元の最小規格では FAT の 8.3 形式よりも厳しいファイル名の制限がありました。 また、興味深い特徴としてリトルエンディアンとビッグエンディアン両方のシステムに対応するために多くのフィールドはペアで用意されています。

しかし、それでは色々不便だったので拡張規格が登場しました。 主に Windows で使われている Joliet と、 un*x 系OSで使われている Rock Ridge などが主要な拡張規格です。 また、過去には Romeo という拡張規格もあったようですが現在ではマイナーな気がします。

Joliet は Volume Descriptor 自体が標準と別に用意されてディレクトリやパステーブルも全部独自になっていること、ファイル名が UTF16 になっていることなど、 IPL では扱いにくい特徴を持っているので今回は対応しません。

El Torito

PC で CDROM から OS を起動するには El Torito という規格に従います。

El Torito では起動情報の書かれた boot.catalog というファイルが必要ですが、 mkisofs が自動的に処理してくれるので通常気にする必要はありません。 用意する必要があるのは実際に起動するファイルだけになります。

El Torito ではハードディスクエミュレーション、フロッピーエミュレーション、エミュレーションなしの3種類の起動方法があります。

フロッピーエミュレーションモードは DOS など元々 CD 起動に対応してない OS を起動するためにイメージファイルを使って仮想フロッピーを作ります。 HD エミュレーションもイメージファイルを使って仮想ディスクを作りますが、実際に使われているのを見たことはないです。 エミュレーションなしは多くの OS で使われていて直接 CD から起動できます。

今回はエミュレーションなしで行きます。 mkisofs のオプションでいうと -no-emul-boot -b cdboot.bin と指定します。

CD 起動 IPL の書き方

CDROM のセクタサイズは通常 2KB なのでエミュレーションなしの起動ファイルのサイズも 2KB になります。 フロッピーやハードディスクの 512 バイトに比べると大きいですが、そんなに複雑な処理をする余裕もないのでディスクからシステムを読み込んで実行するだけのシンプルな IPL にします。

起動してレジスタの初期値を設定したら、まずは 16 番セクタの Primary Volume Descriptor を読み出します。

ISO9660 ではなぜか Volume Descriptor の開始セクタが 16 になっていて、それ以前のセクタは通常利用しません。

f:id:neriring16:20210228142820p:plain

Volume Descriptor は最初の1バイトが種類を指してその次に "CD001" というシグネチャがあります。シグネチャが "CD001" 以外になる規格もあります(DVD など) Primary Volume Descriptor にはルートディレクトリのディレクトリエントリが埋め込まれているので +0x009E と +0x00A6 にあるルートディレクトリの開始セクタとサイズの情報を取得します。

次に、取得した情報をもとにルートディレクトリを読み出します。

f:id:neriring16:20210228143156p:plain

ディレクトリを読み込んだらエントリを1つずつチェックしてシステムファイルを探します。 ディレクトリエントリ自体は可変長で最初の1バイト目がエントリサイズで、ファイル名は +0x20 、開始セクタは +0x02 、ファイルサイズは +0x0A からそれぞれ見つけることができます。

システムファイルを見つけたら該当するセクタからファイルを読み込んで実行します。

これで起動しました。(フロッピー起動と同じ見た目なのでスクリーンショット省略します)

FM TOWNS

FM TOWNS も CDROM から起動することができますが、 El Torito 規格ができる前だったので仕様が違います。

ISO9660では最初の 16 セクタを使用しませんが、 FM TOWNS は最初のセクタを IPL として読み込みます。 起動時点で El Torito と読み込むファイルが全く違うので機種専用の IPL になります。

基本的な処理は PC 用と同じ流れになります。 BIOS 呼び出す部分が違う程度です。

というわけで FM TOWNS でも CD 起動できました。

f:id:neriring16:20210228144302p:plain

UEFI

今回は対応しませんが、 UEFI で CD 起動する場合は通常のメディアと同じように /EFI/BOOT/BOOTX64.EFIブートローダーを置いておけば同じようにたぶん起動します。

いかがでしたか?

以上のように CDROM から TOE を起動することができるようになりました。 これでフロッピーが入手できなくなっても安心できそうな気がします。

MEGFS

古の MEG-OS は MEGFS という独自ファイルシステムをサポートしていました。

それ、 FAT でよくね?

MEGFS はファイル管理にファイルアロケーションテーブル (いわゆる FAT) を採用するなど FAT によく似た特徴を備えており、 FAT と MEGFS の違いは FAT と exFAT の違いと同程度のものでした。

細かい仕様は失われてしまったので筆者もよく覚えていませんが、 ファイルの読み書きをするために専用のツールが必要で面倒なだけでとくにメリットもなかったのですぐに使われなくなりました。

もうひとつの特徴

MEGFS では論理フォーマット以上に興味深い特徴として、特殊な物理フォーマットを採用していました。

実は、素のフロッピーディスクは普段使っているよりも多くの情報を格納することができます。

例えば、 2HD は 1.2MB もしくは 1.4MB というイメージがあるかと思いますが、ディスク本体は 2MB くらいのビットを書き込める磁性体が使われています。 両者の容量のギャップはどうして存在するかというと、物理メディアを回転してデータを読み書きするため、回転によるムラやヘッドのズレでデータが失われないようにする緩衝帯などの役割があります。

初期のドライブは回転ムラも大きかったようで、標準的なフロッピーのフォーマットは緩衝帯を大きめにとって容量が決められました。 やがてドライブの性能が向上してくると緩衝帯が過剰になってきました。

また、当時日本の PC でよく使われていた 1.2MB の 2HD では本来ドライブもメディアも 80 シリンダまで使える状態であえて 77 シリンダに制限して使われていました。*1

そこで、 MEGFS はフォーマット時のパラメータを調整して PC-98 で標準的に 1.2MB になるディスクを 1.4MB で使えるようになっていました。

IBM PC 2HD NEC PC98 2HD MEGFS 1.4M
容量 1440KB 1232KB 1440KB
バイト/セクタ 512 1024 1024
RPM 300 360 360
C 80 77 80
H 2 2 2
R 18 8 9
N 2 3 3

IBM PC の 1.4MB と MEGFS 1.4MB は容量が同じですが物理フォーマットが違います。*2 このように比較すると NEC PC98 の 2HD をベースに 1.4MB 使えるように拡張したことがわかるでしょう。

ゆめのあと

このように頑張って容量稼ぎをしていた MEGFS ですが、本格的に IBM PC の時代がやってくると普通に 1.4MB 使えるようになり、フロッピーの時代の終わりも見えていた当時フロッピー以上の大容量のメディアでは FAT 同様に問題があることが既に分かっていたファイルシステムを発展させるモチベーションも乏しく、 MEG-OS の開発終了とともに MEGFS もしずかに終了しました。

*1:5インチだったか8インチだったかで既に使われていたフォーマットと合わせたと言われています

*2:地味ですが、容量が同じだったので論理フォーマットレベルでは両者を区別できず、イメージファイルは同じものが使えました

新しい自作 OS 始めました

前回の日記で作り始めたもの、それは新しい OS でした。

概要欄

今のところ特徴を myos と比較すると以下のようになります。 myos のサブセット的な感じになっていて一部ソースを流用しています。

MYOS TOE
コードネーム myos toe
アーキテクチャ x86-64 x86
プラットフォーム PC IBM PC, PC-98, FM Towns
動作モード ロングモード プロテクテッドモード
ページング 部分的 なし
セグメンテーション 32bit App のみ 未定
ブートモード UEFI Legacy BIOS
SMP サポート なし
カラーモード ARGB32 8bit インデックスカラー
透過 アルファブレンディング クロマキー

主な特徴

今まで筆者の作った OS の中ではじめてページングを使わない純粋なプロテクトモードで動作します。 これはメモリ保護に MMU を使うのは辞めてみようというという myos の考え方の延長線上にあります。

また、ページングに対応していない拙作 PC エミュレーターで動かしたかったという理由もあります。 *1 この OS を開発したおかげで拙作 PC エミュレーターのバグもいくつか発見・修正されています。笑

f:id:neriring16:20210206215627p:plain

MEG-OS Lite を Rust で再実装したものに近い感じとなっていて、 オリジナルの MEG-OS Lite と同様に NEC PC98 や FM Towns でもほぼそのまま動作します。 (実機の動作保証はありません)

f:id:neriring16:20210206220355p:plain

f:id:neriring16:20210206220403p:plain

386 と 486 の壁

x86 32bit モードは 386 で完成したと思われがちですが、実は 386 と 486 には結構違いがあって 386 には近代的な同期の命令が不足しています。 ターゲットを 486 以降にした方が簡潔になる一方、 486 命令を正しく実装していないエミュレーターもいくつか発見していて、 最終的なターゲットを 386 にするか 486 にするか判断が難しいところです。

myos はどうなるの?

myos を実装始めた頃は Rust を覚えたてのよくわからない時に設計したので、今考えると未熟な部分もあります。 toe は myos に近いアーキテクチャで Rust の理解がより深まって洗練されている部分もあるので、いずれは myos にバックポートしようと思います。

今後

現状は実質 myos のサブセットとなっていますが、最終的にどんな方向に落ち着くか若干まだ未定となっています。

toe のますますの発展をお祈りします。

*1:件のエミュレーターはページング前提の構造になっておらず、もしページングに対応するなら最初から作り直した方が早いと思ってます

妖精さんとなかよくなるほうほう

年初まで x64 や WebAssembly で Rust を色々いじってましたが、少しお休みして x86 でコードを書いてみようと思います。

動かす環境はベアメタル ʕ•ᴥ•ʔ になりますが、 Rust はベアメタルの適切なターゲットがありません。 CPU さえ合ってれば OS はどうでも良さそうだったのでメジャーなターゲットでいくつか試してみました。 しかし、どうやらターゲットごとに適切なリンカーが必要になるようです。

リンカーに困ったら LLVM

ということで、 LLVM をインストールして LLD を読み込むように指定しました。

具体的には .cargo/config に

[target.i586-unknown-linux-gnu]
linker = "/opt/homebrew/opt/llvm/bin/ld.lld"

みたいな記述を加えます。 これ、環境依存ですよね・・・

いかがだったでしょうか?

ということで、 ELF 形式のバイナリ出力できるようになったのでローダーを作るための準備をします。

Program Header と Section Header

ELF には Program Header と Section Header というよく似た2種類のヘッダーがあります。何が違うでしょうか?

ELF というのは Executable and Linking Format の略で、アセンブルコンパイルの出力である .o ファイルも、それらをリンクした実行ファイルも両方サポートしちゃう規格だぜということになります。

Section Header は主に .o ファイルの状態で使うヘッダーで、セクションの役割ごとに細かく分かれています。 Program Header はリンクされた実行可能ファイルだけに存在するヘッダーで、 Section Header に比べると実行時にメモリに読み込むための最低限の情報だけにまとまっています。 つまり、通常の ELF ローダーは Program Header だけ見れば OK ということになります。

Section Header では別のセクションに分かれていても、アドレスがすぐ近くにあって属性が同じ場合 Program Header では同じセグメントにまとまっていることがよくあります。

Program Header には、セグメントの属性、ファイル上の位置とサイズ、メモリ上のアドレスとサイズ、などの情報があるので、それらをパースして実行に必要なアドレスにセグメントの内容をコピーしていきます。

リロケーションがなければこれだけでロードできます。簡単ですね。

GOT - 神ではない

ELF には GOT というテーブルがあります。これは PIC/PIE というリロケーショナルなコードの実行に必要な ELF 特有のデータ構造となります。

アーキテクチャによっては GOT がないとまともに動かせない場合もあったりした気がしますが、 x86 で配置固定のコードを書く場合は全く必要ありません。 むしろ GOT へアクセスするために無駄な機械語が生成されてメモリ消費的にも実行速度的にも無駄です。

ということで、 GOT 使わないコードに書き換えましょう。

.cargo/config に

[build]
rustflags = ["-C", "relocation-model=static"]

のような記述を追加すると配置が完全に固定されたオブジェクトになる代わりに GOT を一切使わなくなります。

配置アドレス決まってるしこれでいいのです。

おめーのせきねぇです?・ヮ・

このようにして ELF さんと仲良くなりましたが、そもそも ELF って割といろんなメタデータが含まれていてバイナリが大きくなります。 デバッグには便利なメタデータですが、実行時にはセクションの配置情報とコード・データセグメントの内容以外必要ありません。 今回のターゲットは割とメモリをタイトにしたいので無駄なメタデータは削減したいです。

ということでバイナリを変換します。

objcopy でバイナリに変換した場合はメモリ上に展開したイメージそのまま出力されて ELF の状態から失われる情報量が多すぎます。 それに実はセグメントの隙間のパディングとか bss もそのままバイナリに出力されるので効率もあまりよくないです。 一方、 strip した場合は ELF として汎用的な情報が多く残っているのでもうちょっと削減したいです。

折半案として独自のツールを作ってセグメントの配置情報と内容だけ別ファイルに抜き出して独自のバイナリ形式にすることにします。 何年か前に似たようなことやったな・・・

ロードに必要な情報をまとめてみましょう。

セグメントの属性

全てのセグメントが読み書き実行可能でその他のメタデータなどがない場合は不要ですが、今回は念の為最低限の情報を残します。

ファイル上のセグメントデータの位置

ファイル上のどこからセグメントの内容を読み込めばいいのか判断するために必要ですが、 最初のセグメントがヘッダの直後につづき、2つ目以降のセグメントは以前のセグメントデータの直後に続いてるというルールがある場合は不要です。

ファイル上のセグメントデータのサイズ

実際に何バイトコピーすればいいのかわからないので必要な情報です。

メモリ上のアドレス

メモリ上のどこに配置すればいいのかわからないので必要な情報です。

メモリ上のサイズ

bss のように初期値 0 のデータを含む場合はファイル上のセグメントサイズとメモリ上のセグメントサイズが異なるので必要になりますが、 全てのセグメントが連続していてファイルヘッダーで bss のサイズがわかる場合、この情報は不要です。

アライン情報など

仮想メモリを使ってストレージ上にスワップする場合、リロケーションをする場合などにセグメントのアライン情報が必要になる場合があります。 現状必要ないし、今後も必要になる可能性が低いのでカットします。

自分の足を撃ち抜く方法

以上のように必要な情報だけを抜き出した単純なヘッダに変換したバイナリができました。 いよいよローダーを作ります。

世の中には ブートローダーをほぼ Rust だけで作ってしまう強者 もいるようです。 しかし、低レベルすぎる操作は高級言語だと逆に使いづらい面もあるかと思ったので、ローダー部分はアセンブリで書くことにしました。

    movzx edx, byte [ebp + N_SECS]
    lea ebx, [ebp + OFF_SECHDR]
    mov esi, edx
    shl esi, 4
    add esi, ebx
.loop:
    mov al, [ebx]
    and al, 0x07
    jz .no_load
    mov ecx, [ebx + S_FILESZ]
    jecxz .no_load
    mov edi, [ebx + S_VADDR]
    rep movsb
.no_load:
    add ebx, SIZE_SECHDR
    dec edx
    jnz .loop

結構シンプルですね。 出来上がったバイナリを自作 PC エミュレーターで動かしてみます。

f:id:neriring16:20210119232112p:plain

これで x86 のベアメタルで Rust が使えるようになりました💪