ミニOS自作記
環境
rustc 1.61.0-nightly
2021 edition
クレート:x86_64
まずページング関連を実装してアロケータを用意する(これはまっさきにやってたほうが良さそうだなと思ったため)
MikanOSでのInitSegmentation()・InitPaging()・InitMemoryManager()をする
InitSegmentation()はGDT登録・セグメントレジスタ初期化・CSとSSに新たに値を書き込む GDT登録関連はuse x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable};周りに任せれば一瞬で終わる
You do not need to add a null segment descriptor yourself - this is already done internally.
セグメントレジスタ初期化はSetDSAll()で、これは実質DS/ES/FS/GSに0を書き込む関数
CSとSSに書き込む値は、GDT.add_entryの戻り値を使わなければならないらしい
MikanOSと同じようにシフト演算したやつ渡したらフリーズしてしまった
InitPaging()は階層ページング構造を設定(配列を初期化してCR3に設定)
まあpaging.cppでやってるようなことをx86_64の機能使って実装すればいいかな
Cr3Flagsはよくわからないけど、Cr3Flags::all()したらキャッシュの無効化とかされそうだったので、とりあえずemptyに
でも動かなかった
PhysFrameをstatic mutな変数に代入しても変わらなかった
ということで、gdbを使う
kernel.elfをロードし、b <file_name>:<line>でブレークポイント設定
あとは予め用意しておいたqemu起動スクリプトを走らせ、gdbからtarget remote localhost:1234を打ち込んでやれば設定したブレークポイントで止まってくれる
たぶんqemuの起動オプションに-sを付けないといけないと思う
実行速度かなり遅くなったけど、ちゃんと止まってくれた
Cr3Flagsが駄目か?とおもったけど、デフォルトでもemptyだった
スタック領域がOSのものではないのが関係してたりするだろうか
entry.asmを作ってこれのとおりにビルドスクリプト書くだけ これ.oをそのままリンクできないんですかね・・・
そういえば0x083をOR演算するのに相当する処理をしていない気がする
page_directory[a][b]でORしてるのは0x083で、それ以外は0x003
0b1000_0011と0b0000_0011ということ
これをPageTableFlagsに対応させると、0b0011はPRESENT | WRITABLEで、0b1000_0000はHUGE_PAGEらしい
Intelの開発者用ドキュメントをちょっとだけ漁ったけど出てこなかった どうやって検索すれば良いんだろうか
フラグをちゃんと指定すると、qemuの強制再起動はなくなったが、フリーズしてしまった
HUGE_PAGEはまずかったらしい x86_64のstructures/paging/page_table.rsの87行目にあるアサーションでパニックしてる
そもそもset_frameじゃないのでは・・・?なんかアドレス指定してそうだけど
set_addrにしたら動いた
ということで、グローバルアロケータの実装に入る
Writing OS in Rustでも紹介されている、線形リストを用いたメモリマップを実装する
読んでみた感じ効率はさほど悪くなさそうだし、紹介されてるから書きやすいかなと思って採用
まずメモリの開いてる場所にLinkedListのノードを配置、確保されたらそのノードを外して、開放されたらそこにノードを置くだけ
シンプル~
アロケータを実装したら、ブートローダーから与えられているメモリマップを読み込んで、使えそうな領域にノードを配置していけばよい
MikanOSは逆で、「使えない領域を割り当て済みにする」というアプローチだった
管理方法が違うからね
だから、MikanOSと正反対のことをすれば良い
MikanOSではavailable_end < desc.physical_startもしくは!IsAvailable()のときに割り当て済みにしていたから、IsAvailable()かつavailable_end >= desc.physical_startな領域にノードを追加すれば良いんですね
メモリマップの読み込みだけに頼っている(UEFIが確保した領域のうち、自由に使って良い領域のみ確保している)せいか、使える容量がめちゃくちゃ少ない 100MBくらい
というかMikanOSのavailable_end < desc.physical_startという条件、MikanOSの管理方式が連続した領域を確保する(が、その中に使えない領域があったらそれを確保済みにしておく)という方式だから必要なのであって、今回は別に連続している必要がないからいらないのでは・・・?
120MBくらいになったよ!!!!
🤔
Qemuのデフォルトメモリサイズが128MBなので妥当です・・・
-m 1Gを指定すると、1017MBになった
アホみたいな間違いだけど、思ったよりグローバルアロケータがしっかり動いている事がわかってなんとも言えない気持ちになった
メモリ問題は問題として浮上するまで置いておいて(????)、ここでウィンドウマネージャ(仮)の実装をする
グローバルアロケータを実装したおかげでVecもRcも使えるので、もはややるだけになった
フレームというtraitを作り、WindowとFrameContainerに実装、それをFrameManagerに持たせるみたいな感じ
クラス図かフローチャートかなんか書こうと思ったけど難しすぎて断念
そして、キーボード入力を実装する
そういえば、今は画面描画にEFI_GRAPHICS_OUTPUT_PROTOCOLを使ってるけど、EFI_SIMPLE_TEXT_INPUT_PROTOCOLってUEFI抜けても使えるのかな
使えなさそうだったけど、ここまで来たら引けないので、先にブートローダー実装します
OS動作中も利用できるランタイムサービスとしては、UEFI Graphics Output Protocol、UEFIメモリマップ、ACPI、SMBIOS、SMM、日付や時間サービス、NVRAMサービスなど
正直わからん MikanOSをRustで実装している先人の知恵とドキュメントしか頼れない
メモリマップは普通にやるとイテレータが返るらしい
boot_services().memory_map(&mut buffer)で取得できる
bufferは適当に要素数が大きめのu8配列
boot_services().memory_map_size()がおおよそ2400くらいを返すので、それくらいで
で、どうやってカーネルに渡そうか
ヒープに確保していいものなのか?
そういえば、Rustで書いてRustのカーネルに渡せるんだから、自分で使いやすいように加工して良いのかな
MemoryMapが必要になるのはGlobalAllocatorの初期化だけで、これは結局MemoryDescriptorの個数を計算して全部確かめた後、そのDescriptorから物理メモリの開始地点とページ数を見るだけ
じゃあ個数とMemoryDescriptorの配列があればいいよね これで実装しよう
Vec使ったけど大丈夫かな・・・
メモリマップをテキストファイルに保存するやつは飛ばした
絶対いらない 使わない割にコスト(保存専用関数のコード量)がおもすぎる
GOPはそもそもEFI_GRAPHICS_OUTPUT_PROTOCOLとして定義される「プロトコル」
boot_services().locate_protocol::<GraphicsOutput>().unwrap_success().getで取得できる
長過ぎる
そもそもunwrap_success()って何??ドキュメント上だとただのResultなのに・・・
uefi::Resultだった
ここまで来たら、とりあえずカーネルに渡せば良さそうじゃない?
カーネルは最初からELF読み取りを行う どうせ後でやるからね
まずはルートディレクトリを取得
boot_services().get_image_file_system(handle).unwrap_success().interface.get
handleはefi_mainの第一引数
次にカーネルファイルが存在するか確認、ついでにカーネルサイズを取得
さっき取得したルートディレクトリで、ファイル名が適切なものが来るまでread_entry()を繰り返し呼ぶ
そしたらpool作って読み取り
MikanOSに則って、MemoryTypeはLOADER_DATA
で、このpoolどうやって使うの?
RegularFile::read()の引数は&mut [u8]だけど、allocate_pool().unwrap_success()で返ってくるのは*mut u8で、どうやっても[u8]にキャストできない
というかRustの仕様上、[u8]にキャストというのが不可能らしい
そりゃそうだよな コンパイル時に長さわからないと駄目だし
普通にVec使うか・・・
それからELFをパースする
elf_rsが思ったように動いてくれない
from_bytesでBufferTooShortエラーが出る どうして??
Vecを確保するときにwith_capacity()を使っていたんだけど、elf_rsはlenしか見ないのでエラー
というか、そもそもkernel_file.read()も動いてなかったっぽい・・・
vec![value; len]で確保することにより事なき
今回読み取りたいのは各LOADセグメントの開始および終了アドレス
後々のこと(実際のロード時)を考えてVecにぜんぶ入れる
Vec::iter().maxで最大値を取得できる 最小値も同じようにする
このタイミングでカーネル関連の処理が読みづらくなってきたので、ファイルを開く処理を関数分けした
そして、LOADセグメントをメモリにコピーしていく
MikanOSでやったことを、core::ptr::copyでやればOK
そうしたらいよいよブートサービスを終えて、カーネルのエントリーポイントへジャンプする
exit_boot_services()がメモリマップ返してくれるの草 さっきわざわざ受け取ったのに・・・
メモリマップ取得の実装中に考えていたヒープ問題、やっぱり発生してしまった
Vec::leak()なるものがあるらしい staticになる?
ただ、これはvecを配列にするものっぽい
ここでカーネルコピーが正常に行われていないことが判明 まずい
おそらくだけど、物理アドレスにアクセスしていると思ったら仮想アドレスでしたみたいな感じだと思う
コピー元は問題なさそう、コピー先が駄目?
というか、書き込みができてないように見受けられる
printする前にdst書き換えてただけで、コピー自体は普通にできた
Elf::entry_point()をas *const extern "sysv64" fn()してコール
ページフォルトした
printしてみたけど、エントリーポイントは正常に確保できてるし、コピーもちゃんとしてるように見える
例えばentry_pointが0x12345だった場合、これをas *const fn()すると、0x12345にある(本来は機械語の)値を関数のポインタだと思ってしまう
0x12345に関数があると思って良い
fn()は関数ポインタだと思って良い
つまり、0x12345 as *const fn()は関数ポインタへのポインタということである
0x12345 as fn()すれば解決
このキャストは出来ないらしいので、core::mem::transmuteなるものを使う
今度は再起動を繰り返すようになった
どうして???
トリプルフォルトが起きてるらしい
トリプルフォルトが起きたときにダンプしてくれるようにした
カーネル側のcore::mem::copyでコケてるらしい
読んだ記憶がないので、多分ライブラリのどこか
というかメモリ書き込みが駄目なんじゃない?
allocate_pagesを消したら動いた・・・・・・
MikanOSの方でも確認してみたけど、ページ確保しなくても動く
えっ大丈夫?これ
動けばヨシの精神で続けます
それではいよいよキー入力に入りましょう
USBはキツイので、PS/2キーボードに対応することにする
基本的に割り込みで処理するのでIDT登録が先かな
クレートが便利なのでやるだけでした
そしてハードウェア割り込みに移ろうとしたが、キーボード割り込みについての情報が全然見つからないのでとりあえずポーリングによる実装を試みてみる
にハードウェアに関するドキュメントへのリンクが有る どうやら0x60ポートを読めばいいらしい?
読んでみたけどよくわからん aキーを押した時、下位7bitを反転させた値がASCIIの 'a'(0x61)になったので、もしかして!?と思ったけど、そんなことはなかった(それはそう)
表の見方を間違えていたので、キーコードについて何も書いてないかと思った
キーを入力すると、キーボードコントローラーがPIC(Programmable Interrupt Controller)にIRQ1を送信
IRQ:Interrupt ReQuest?割り込み要求らしい
PICはそのIRQをCPUの割り込みベクトルに変換する(??)
で、ハンドラが呼ばれる
PIC息してる??
そもそもタイマーも動かないんだけど
使ってるのはクレート(pic8259)
マジでわからん
IDTの32~128ぜんぶに関数入れても動かない おかしいね
sti(割り込み有効化、x86_64::instructions::interrupts::enable())もしてるし
ここで、MikanOSでいうところのInitInterruptしか実装していないことに気づく
InitLAPICTimer()相当の処理を行うと、タイマー割り込みが発生するようになった
じゃあキーボードも初期化すれば良いのでは?と思ったけど、やり方全くわかりません
30日でできるOS自作本のソースコードを見る(day11a)
キーボードコントローラーがデータ送信可能になるのを待っている
0x64ポートに0x60を書き込み
コードセット1に変更+IBM Personal Computerのキーボード・インターフェースにしてる
0x60ポートに0x47を書き込み
0x07はリザーブビット?と思ったけどリザーブビットは0~3らしい・・・?本当にリザーブビットなら0x0fになるのでは
0x40部分はジャンパーを結線状態にしている(????)
全くよくわからんけどこの通りに実装してみようか
だめです
PICの初期化処理も合わせてみる
これでも動かない
マウスもやってみたけど同様
なんかわからないけど、よくよく見ると起動した瞬間だけキーボード割り込みが発生している
さっきついでレベルで実装したマウスも、起動した瞬間だけ発生している
PICを初期化した瞬間に発生してるっぽい?
interrupt::enable()を呼ぶ前に出てるんだけど・・・
なんで動いているのか全くわからない
PIC関連をいじったおかげなのか
キーボード関連をいじったおかげなのか
何もしてないのに動かなくなった
何もしてないのに動いた
IDT、32がタイマーで33がキーボード、44がマウスらしい
PICの初期化を呼ばなくてもタイマー割り込みが発生する
LAPICなのでそれはそう?
キーボードは割り込み起きない
起動直後だけといっても、2回連続で割り込みが起きてることがある 割り込みから正常に復帰できていないわけではないのか?
なんかめちゃくちゃ良いページあって草
読み取り時の手順ってどうなってるの
アウトプットバッファが空になるまでIOポート0x60を読めばいい
アウトプットバッファについては、IOポート0x64のビット0を読む 0ならempty、1ならfull
ここでの「アウトプット」とはキーボードコントローラー側から見たものであることに注意する
書き込み時は、インプットバッファ(IOポート0x64、ビット1)が空になるのを待ってから書き込めば良い
もうだめそう 8259 PICの使用をやめて、LAPICの使用を試みる
PS/2となると古い情報メイン(つまり8259使用前提の情報)しかないけど、この方法はUEFIブートとか比較的新しいハードで使えない可能性があるのでは・・・?と思ってしまった
BIOSブートなOS、HariboteOSではちゃんと動いた
APICとはAdvanceなPIC 各CPUに搭載されてるLocalなやつがLAPIC 8259を無効化しないといけないらしい かなしいね
PS/2つかうならIOAPIC・・・?
0xfeee_0???にmemory-mapped I/Oがあるので、いい感じに書き込む
どうやら有効化するときは0f0 | 0x100すればいいらしい?しらんけど
今まで動いていなかったの、もしかしてマウスの割り込みハンドラでEOI送信してなかったからか・・・?と思ったけど、送信しても変わらんやんけ
やっぱりIO LAPICのほうらしい
IOAPICレジスタの0x10から0x3fに64ビットのリダイレクトエントリがあるので、いい感じに書き込めばよさそう
リダイレクトエントリのビット0~7が割り込みベクタなので、ここにPS/2キーボードの値33をぶち込めばいいんじゃない?
で、IOAPICレジスタの読み書きはどうすれば良いんですか
ACPI読まなきゃ駄目らしい クソですね
ん?どうやらMikanOSでやったみたいなことをやらなければいけないらしい
RSDPがどうこうみたいなの