[読書メモ]『Writing an OS in Rust』.
これは何
Writing an OS in Rust を読んでわからないことをメモします.
day1 [[2022-04-17]]
ベアメタルでのRust
[no_std]
つけるpanic_handler
language item
: コンパイラの内部で必要な関数, 型
#shortcut zellij ctrl+t + d
detach
- スタックアンワインド: 全てのスタックにある変数のデストラクタを実行する unwind: スタックアンワインドをする
- OS特有の機能を必要とする abort: 中止する
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
-
#shortcut/vim A: 末尾まで移動してi
-
#[no_mangle]
manglingしない -
extern "C"
: Cの呼び出し規約(call
の仕方?)を使用する -
target triple:
<arch>-<vendor>-<os>-<abi>
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演算を整数演算でエミュレート
- JSONで独自のターゲット作れる
core
ライブラリをcustom targetでビルドして欲しいので
[unstable]
build-std = ["core", "compiler_builtins"]
VGA
[[VGA]]
セグメンテーション
セグメントレジスタってなんだ. x86-64は仮想pagingでやるので無いっぽい.
- x86 memory segmentation - Wikipedia
- X86アセンブラ/x86アーキテクチャ - Wikibooks
- https://web.stanford.edu/class/cs107/resources/x86-64-reference.pdf #cheatsheet
プロテクトモードが導入され, アクセス制御が実現された. 仮想メモリの前概念.
- 断片化 -> ページテーブル -> 階層化
- 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が異なることが多いので以下のようにする
- いくつかのop(SSEの一部)はalignを要求するのでスタックポインタ(sp)を16byte align
- 割り込みスタック表を使ってスタックを変更する
- 古いspをpush
- RFLAGSをpush
- 命令ptr(rip), コードセグメント(cs)をpush=戻り値を意味する
- エラーコードをあればpush(ex. page faultとか)
- 割り込みハンドラを呼び出す
x86-interrupt
呼出規約はこれらのプロセスを隠蔽してくれる
ブレークポイント例外
- デバッガは
int3
命令で一時停止させている x86_64::instructions::interrupts::int3()
で使う
asm!("int3", options(nomem, nostack))
したらdeadbeaf
直下にあった
AT&T
toIntel
online asm convertorなかった
int3=0xcc
- x86のisntructionのビット長, 可変なのか
x64_64
crateが偉大
double fault
- 先程と同じようにハンドラを登録する
- ダブルフォルトは復帰不可能なので
panic!
?
- ダブルフォルトは復帰不可能なので
- いつ発生するのか
- 特定の例外の処理中に特定の例外が発生したときに発生
- ex. 無効な [[TSS]]
- TSS: Task State Segment 実行中のsp等を示す構造体
- ex. 無効な [[TSS]]
- 特定の例外の処理中に特定の例外が発生したときに発生
- [[ガードページ]]: スタックの底にある特別なページでこれでスタックオーバーフローを検出
- ブートローダが既に設定しているのでスタックオーバーフローは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)
- その後割り込みスタックフレームをスタックにpushしようとするが出来ないのでダブルフォルト
- ブートローダが既に設定しているのでスタックオーバーフローはpage faultが発生
- [[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)
)
- 歴史的経緯でkernel, user modeの設定, TSSの読み込み64bitでも必要
- 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
- page_table.rs.html -- source
0..=8
はフラグ(PageTableFlags
)
/// 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);
}
}
- x86_64 64bit modeではページングが必須で, ブートローダが既にページングをしているらしいのでそこでr/wの設定をしているのかもしれない
恒等マッピング
- ページテーブルを仮想メモリ上のどこに置くかの話
- 今は恒等マッピング
- だと仮想メモリ空間が散らかってしまう: そんなに散らかるのか #todo
- 今は恒等マッピング
固定オフセットのマッピング
- [[xv6]] は固定オフセットのマッピング?
- この固定オフセット値はページテーブル毎に変わるという意味?なら断片化を防げる意味わかる
物理メモリ全体をマップ
- 固定オフセットと何が違うのかが今はわからない #todo
- x86_64はページサイズ2MiBのhuge pageを使えるので
- ex. 32GiB物理メモリ空間は Level3 Table 1個, Level 2 Table 32個で済む (=
(512 * 32 * 1) * 2000KiB = 32GiB
ので132KiBの占有で済む
- ex. 32GiB物理メモリ空間は Level3 Table 1個, Level 2 Table 32個で済む (=
一時的なオフセット
(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
- C++でいう
[ [nodiscard]]
は#[must_use]
- Rust RFC 2071 2071-impl-trait-existential-types - The Rust RFC Book