ゼロからのOS自作入門も終わっていませんが、RustでOSを作ってみたかったので作る

  • 環境
    • rustc 1.61.0-nightly
    • 2021 edition
    • クレート:x86_64

まずページング関連を実装してアロケータを用意する(これはまっさきにやってたほうが良さそうだなと思ったため)

  • MikanOSでのInitSegmentation()・InitPaging()・InitMemoryManager()をする
  • InitSegmentation()はGDT登録・セグメントレジスタ初期化・CSSSに新たに値を書き込む
    • 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抜けても使えるのかな
    - 使えなさそうだったけど、ここまで来たら引けないので、先にブートローダー実装します
    - Wikipediaにも書いてあった
    - > 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エラーが出る どうして??
        • elf_rsのGitHubを見てわかった
          • 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なるものを使う
      • 今度は再起動を繰り返すようになった
        • どうして???
        • トリプルフォルトが起きてるらしい
        • https://forum.osdev.org/viewtopic.php?f=1&t=25523
          • トリプルフォルトが起きたときにダンプしてくれるようにした
        • カーネル側のcore::mem::copyでコケてるらしい
          • 読んだ記憶がないので、多分ライブラリのどこか
        • というかメモリ書き込みが駄目なんじゃない?
          • allocate_pagesを消したら動いた・・・・・・
          • MikanOSの方でも確認してみたけど、ページ確保しなくても動く
          • えっ大丈夫?これ
          • 動けばヨシの精神で続けます

それではいよいよキー入力に入りましょう

  • USBはキツイので、PS/2キーボードに対応することにする
  • 基本的に割り込みで処理するのでIDT登録が先かな
    • クレートが便利なのでやるだけでした
  • そしてハードウェア割り込みに移ろうとしたが、キーボード割り込みについての情報が全然見つからないのでとりあえずポーリングによる実装を試みてみる
    • https://osdev-jp.readthedocs.io/ja/latest/general/references.htmlにハードウェアに関するドキュメントへのリンクが有る
      • どうやら0x60ポートを読めばいいらしい?
      • 読んでみたけどよくわからん aキーを押した時、下位7bitを反転させた値がASCIIの ‘a’(0x61)になったので、もしかして!?と思ったけど、そんなことはなかった(それはそう)
      • これについてはOSDev Wikiに書いてあった
        • 表の見方を間違えていたので、キーコードについて何も書いてないかと思った
  • OSDevを参考にまとめる
    • キーを入力すると、キーボードコントローラーが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を無効化しないといけないらしい かなしいね
  • それっぽい質問 https://forum.osdev.org/viewtopic.php?f=1&t=30865
    • PS/2つかうならIOAPIC・・・?
  • 0xfeee_0???にmemory-mapped I/Oがあるので、いい感じに書き込む
    • どうやら有効化するときは0f0 | 0x100すればいいらしい?しらんけど
  • 今まで動いていなかったの、もしかしてマウスの割り込みハンドラでEOI送信してなかったからか・・・?と思ったけど、送信しても変わらんやんけ
  • やっぱりIO LAPICのほうらしい
    • IOAPICレジスタの0x10から0x3fに64ビットのリダイレクトエントリがあるので、いい感じに書き込めばよさそう
    • リダイレクトエントリのビット0~7が割り込みベクタなので、ここにPS/2キーボードの値33をぶち込めばいいんじゃない?
    • で、IOAPICレジスタの読み書きはどうすれば良いんですか
      • https://wiki.osdev.org/IOAPIC
      • ACPI読まなきゃ駄目らしい クソですね
      • UEFIを抜けてもACPIは使えるらしいので、ブートローダーに戻って、ACPIを読みましょう
      • ん?どうやらMikanOSでやったみたいなことをやらなければいけないらしい
      • RSDPがどうこうみたいなの