Posted on

これは何

Writing an OS in Rust を読んでわからないことをメモします.

day1 [[2022-04-17]]

ベアメタルでのRust

#shortcut zellij ctrl+t + d detach

  • スタックアンワインド: 全てのスタックにある変数のデストラクタを実行する unwind: スタックアンワインドをする
  • OS特有の機能を必要とする abort: 中止する
[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Custom Target

  • blog_osは [[BIOS]] (先発が [[BIOS]] 後発が [[UEFI]])
  • 起動時, CPUはリアルモード(16bit), プロテクトモード(32bit) -> ロングモード(64bit) に変更しなければならない
  • ブートローダを作るのは大抵役に立たないので GitHub - rust-osdev/bootimage: Tool to create bootable disk images from a Rust OS kernel. を使う
  • Custom Targets - The rustc book
    • JSONで独自のターゲット作れる
      • disable-redzone: スタックポインタ最適化を無効化する機能で安全に(=スタックの破損なしに)割り込みを処理できる
      • features: +- 有/無効化, SIMDはレジスタ退避のコストが高いので無効化, soft-float: float演算を整数演算でエミュレート
  • core ライブラリをcustom targetでビルドして欲しいので
[unstable]
build-std = ["core", "compiler_builtins"]

VGA

[[VGA]]

セグメンテーション

セグメントレジスタってなんだ. x86-64は仮想pagingでやるので無いっぽい.

プロテクトモードが導入され, アクセス制御が実現された. 仮想メモリの前概念.

  • 断片化 -> ページテーブル -> 階層化
  • x86-64は512エントリで4層なので9bitのindex(48..=12)とoffset 12bit
    • xv6は4096エントリで2階層, [[20220410_xv6_kernel_paging_code_reading]]
    • テーブルが1page=4KB=12bit, 1エントリ64bitなので2^(12+3)/2^6 = 9bit
  • TLB: テーブルの変換をスキップするためのバッファ
    • cacheがtransparent: 利用者がキャッシュの存在を意識する必要がない
    • カーネルがページテーブルを変更したときは(invlpgで)TLBを更新する

参考文献

day2 [[2022-05-07]]

CPU例外 | Writing an OS in Rust から読んでいく, メモ中の図はこの記事から引用しています.

  • 割り込み例外未実装でpage faultすると [[QEMU]] がチカチカする
    • 真相: page faultの割り込みハンドラが登録されていないのでトリプルフォルトが起きbootループし続けていた
  • x86-64 lea: LEA — Load Effective Address
  • unsafe { *ptr = oxdeadbeef } を探す
    • cargo rustc -- --emit=asm --emit=llvm-ir
      • llvm-ir には存在するが, asm には無い
      • core::ptr::write_volatile() で消えない
  • 例外

CPU例外

  • 例外が発生すると割り込みで例外ハンドラを呼ぶ
  • x86には20種類ある, 抜粋
    • page fault
    • invalid opcode
    • general protection fault: アクセス違反系
    • double fault: 例外ハンドラ中での例外, 例外ハンドラが登録されていない
    • triple fault: double fault handle中に例外が発生すると致命的
  • CPUに [[IDT]]; Interrupt Descriptor Table を登録しておくと使ってくれる

例外発生時

  • opポインタと [[RFLAGS ]] レジストをスタックにプッシュ
  • IDTからエントリを探す, 割り込みゲート(= :40==0) ならハードウェア割り込みを無効に
  • 指定されたGDTセレクタをCSセグメントに読み込む
  • ハンドラにjmp
  • extern "x86-interrupt": x86の関数呼び出し規約
    • 呼び出し規約: パラメータがレジスタに置かれるのかスタックに置かれるのか, 結果をどう返すのか等
      • extern "C": 最初の6つの整数引数はレジスタ, 後はスタック, 結果は %rax, %rdx
        • 構造体を値渡しする時はスタックなのか?
          • 9cc機運, コード生成知りたい #todo
  • 例外は関数呼び出しと非常に似ているがいつでも発生しうる点が違う

呼出規約

  • preservedレジスタ: 呼び出し前後で変化してはいけない(returnまでにもとに戻っていればok)=callee-saved
  • scratchレジスタ: 上書きできる(のでバックアップ必要)=caller-saved
  • 例外発生時は caller-saved レジスタが上書きされてしまうと困るので x86-interrupt 呼出規約を付けることで上書きされるレジスタのみバックアップするコードを生成する

割り込み時のスタックフレーム

(下に伸びる)

  • 関数呼び出しではret addrをjmpする前にプッシュするが例外時はcontextが異なることが多いので以下のようにする
  1. いくつかのop(SSEの一部)はalignを要求するのでスタックポインタ(sp)を16byte align
  2. 割り込みスタック表を使ってスタックを変更する
  3. 古いspをpush
  4. RFLAGSをpush
  5. 命令ptr(rip), コードセグメント(cs)をpush=戻り値を意味する
  6. エラーコードをあればpush(ex. page faultとか)
  7. 割り込みハンドラを呼び出す

  • x86-interrupt 呼出規約はこれらのプロセスを隠蔽してくれる

ブレークポイント例外

  • デバッガは int3 命令で一時停止させている
  • x86_64::instructions::interrupts::int3() で使う

  • asm!("int3", options(nomem, nostack)) したら deadbeaf 直下にあった

  • int3=0xcc
    • x86のisntructionのビット長, 可変なのか
  • x64_64 crateが偉大

double fault

  • 先程と同じようにハンドラを登録する
    • ダブルフォルトは復帰不可能なので panic! ?
  • いつ発生するのか
    • 特定の例外の処理中に特定の例外が発生したときに発生
      • ex. 無効な [[TSS]]
        • TSS: Task State Segment 実行中のsp等を示す構造体
  • [[ガードページ]]: スタックの底にある特別なページでこれでスタックオーバーフローを検出
    • ブートローダが既に設定しているのでスタックオーバーフローはpage faultが発生
      • その後割り込みスタックフレームをスタックにpushしようとするが出来ないのでダブルフォルト
        • なのでトリプルフォルトが起きる
      • 解決法: 割り込みスタックテーブル(IST)を用意する
        • 例外専用のスタックを7個用意して, ISTはそれらのspが入る
        • ISTはspの配列
        • ISTはTSSの一部, 32bit modeではレジスタの状態などを保持していたが64bit ではタスク固有の情報は持たなくなり, 2つのスタックテーブル(特権ST, 割り込みST)を持つように
      • まだページング用意していないので static mut [u8; STACK_SIZE] 使う
        • とりあえず5ページ(20KB), あとで置き換える
        • mut にするのは ro にしないため
        • 最初 start, end は両端指し, end が小さくなっていく(=sp)
  • [[GDT]]: Global Descriptor Table
    • 歴史的経緯でkernel, user modeの設定, TSSの読み込み64bitでも必要
      • 昔はセグメンテーションの為に使われていた
    • これをCPUに教える
      • %cs 再読み込み(x86_64::registers::segmentation::CS::set_reg(cs))
      • TSSをロード(x86_64::instructions::tables::load_tss(tss))
      • IDTエントリを更新(idt.double_fault.set_handler_fn(double_fault_handler))
  • HHKB, delete押しづらい問題
    • 右上2つ
    • delete, esc

次回

pagingの実装に進む, Hardware Interrupts | Writing an OS in Rust (日本語無し)はpagingの実装と関係がなさそうなのでskip

参考文献

day3 [[2022-05-14]]

  • Pagingの解説: [[20220417_writing_an_os_in_rust]] で見た

64bit VA

{
   64..=49 Sign Extension
   48..=40 Level4 Index
   39..=31 Level3 Index
   30..=22 Level2 Index
   21..=12 Level2 Index
   11..=0 Offset
}

PageTableEntry

/// A 64-bit page table entry.
#[derive(Clone)]
#[repr(transparent)]
pub struct PageTableEntry {
    entry: u64,
}

impl PageTableEntry {
  /// Returns the flags of this entry.
    pub fn flags(&self) -> PageTableFlags {
        PageTableFlags::from_bits_truncate(self.entry)
    }
}

bitflags! {
    /// Possible flags for a page table entry.
    pub struct PageTableFlags: u64 {
        /// Specifies whether the mapped frame or page table is loaded in memory.
        const PRESENT =         1 << 0;
        /// Controls whether writes to the mapped frames are allowed.
        ///
        /// If this bit is unset in a level 1 page table entry, the mapped frame is read-only.
        /// If this bit is unset in a higher level page table entry the complete range of mapped
        /// pages is read-only.
        const WRITABLE =        1 << 1;
        /// Controls whether accesses from userspace (i.e. ring 3) are permitted.
        const USER_ACCESSIBLE = 1 << 2;
        /// If this bit is set, a “write-through” policy is used for the cache, else a “write-back”
        /// policy is used.
        const WRITE_THROUGH =   1 << 3;
        /// Disables caching for the pointed entry is cacheable.
        const NO_CACHE =        1 << 4;
        /// Set by the CPU when the mapped frame or page table is accessed.
        const ACCESSED =        1 << 5;
        /// Set by the CPU on a write to the mapped frame.
        const DIRTY =           1 << 6;
        /// Specifies that the entry maps a huge frame instead of a page table. Only allowed in
        /// P2 or P3 tables.
        const HUGE_PAGE =       1 << 7;
        /// Indicates that the mapping is present in all address spaces, so it isn't flushed from
        /// the TLB on an address space switch.
        const GLOBAL =          1 << 8;

    ...
        /// Forbid code execution from the mapped frames.
        ///
        /// Can be only used when the no-execute page protection feature is enabled in the EFER
        /// register.
        const NO_EXECUTE =      1 << 63;
    }
}

ページフォルトハンドラ

  • 今までのページフォルトはハンドラが見つからなくてダブルフォルトになっていた
#[no_mangle]
pub extern "C" fn _start() -> ! {
  ...
  
  unsafe {
    // caused by write
    *(0x207f07 as *mut u64) = 42;
    // PROTECTION_VIOLATION | CAUSED_BY_WRITE 
    
    // caused by read
    let a = *(0x207f07 as *mut u64);
    // 何も起きない
    debug!("{}", a);
  }
}

恒等マッピング

  • ページテーブルを仮想メモリ上のどこに置くかの話
    • 今は恒等マッピング
      • だと仮想メモリ空間が散らかってしまう: そんなに散らかるのか #todo

固定オフセットのマッピング

  • [[xv6]] は固定オフセットのマッピング?
    • この固定オフセット値はページテーブル毎に変わるという意味?なら断片化を防げる意味わかる

物理メモリ全体をマップ

  • 固定オフセットと何が違うのかが今はわからない #todo
  • x86_64はページサイズ2MiBのhuge pageを使えるので
    • ex. 32GiB物理メモリ空間は Level3 Table 1個, Level 2 Table 32個で済む (= (512 * 32 * 1) * 2000KiB = 32GiB ので132KiBの占有で済む

一時的なオフセット

(Lv4, Lv3, Lv2) = (0, 0, 0) はLv1テーブル全体(=2MiB)が恒等マッピングしていることと等価. そこのエントリの Frame に書き込むことによって一時的なマッピングを511個作れる, つまりカーネルは PA 0KiB らへんに書き込むことによってLv1+テーブルの場所がわかる.

再帰的ページテーブル

同じLvのテーブルエントリを示すのを許容するとアクセスする回数によって任意Lvのテーブルを操作できる. x86のページテーブルの方式に強く依存.

ブートローダの修正

ページテーブルを修正したいのでブートローダを修正する. Rustで型レベルで1回しか呼びせないことは表現できない?

L4 entry 0: PageTableEntry { addr: PhysAddr(0x2000), flags: PRESENT | WRITABLE |
 ACCESSED }
L4 entry 2: PageTableEntry { addr: PhysAddr(0x434000), flags: PRESENT | WRITABLE
 | ACCESSED | DIRTY }
L4 entry 3: PageTableEntry { addr: PhysAddr(0x43c000), flags: PRESENT | WRITABLE
 | ACCESSED | DIRTY }
L4 entry 31: PageTableEntry { addr: PhysAddr(0x438000), flags: PRESENT | WRITABL
E | ACCESSED | DIRTY }

VA -> PA

  • 4段階変換するだけ
    • あとで use x86_64::structures::paging::Translate trait の translate_addr で置き換える
  • ページに相当する物理メモリ領域を PhysFrame としている
 VirtAddr(0xb8000) -> Some(PhysAddr(0xb8000))
 VirtAddr(0x201008) -> Some(PhysAddr(0x401008))
 VirtAddr(0x10000201a10) -> Some(PhysAddr(0x27fa10))
 panicked at 'huge pages not supported', src/memory.rs:58:43

参考文献