Skip to content

Commit

Permalink
simple_pg: revise
Browse files Browse the repository at this point in the history
Signed-off-by: smallkirby <[email protected]>
  • Loading branch information
smallkirby committed Nov 16, 2024
1 parent 580882e commit b1cea31
Showing 1 changed file with 35 additions and 31 deletions.
66 changes: 35 additions & 31 deletions src/bootloader/simple_pg.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# 簡易版ページテーブル

前回で Ymir の ELF イメージをファイルかリードできるようになりました
前チャプターでは Ymir の ELF イメージをファイルシステムからメモリ上に読み込めるようになりました
本当はそのままカーネルをロードしたいところでしたが、
そのためにはページテーブルを操作して ELF が要求する仮想アドレスをマップする必要があります。
ブートローダである Surtr がページテーブルを操作するのはその目的のためだけのため
ブートローダである Surtr がページテーブルを操作する必要があるのはカーネルのロード時だけであるため
本チャプターでは必要最低限なページテーブルの操作を実装していきます。

## Table of Contents
Expand All @@ -12,12 +12,12 @@

## arch ディレクトリの作成

ページテーブルの構造を始めとして、ページテーブルはアーキテクチャに大きく依存します
本シリーズでは x64 以外をサポートしませんが、それでもアーキテクチャ依存のコードは階層を分けて書いていくことにします。
ページテーブルの構造を始めとして、ページテーブルの構造や操作は CPU アーキテクチャに大きく依存します
本シリーズでは x86-64 以外をサポートしませんが、それでもアーキテクチャ依存のコードは階層を分けて書いていくことにします。

`arch` ディレクトリの中に `x86` ディレクトリを作成し、以下のような構造にします:

```sh
```bash
> tree ./surtr
./surtr
├── arch
Expand All @@ -29,8 +29,8 @@
```

- `arch.zig`: `boot.zig`から直接利用するファイル。
できる限りアーキ依存の概念を隠蔽できるようなAPIを提供し`arch/`以下の不必要なAPIは参照できないようにします
- `arch/x86/arch.zig`: x64 固有のAPIを export するルートファイル。
できる限りアーキ依存の概念を隠蔽できるような API を提供し`arch/`以下のファイルが提供する API を直接参照できないようにします
- `arch/x86/arch.zig`: x86-64 固有の API などを export するルートファイル。

`/arch.zig` では以下のようにターゲットとなるアーキテクチャに応じて `arch` 以下のコードを export します:

Expand All @@ -42,14 +42,14 @@ pub usingnamespace switch (builtin.target.cpu.arch) {
};
```

`builtin.target.cpu.arch``build.zig``cpu_arch`で指定したターゲットアーキテクチャです。
`builtin.target.cpu.arch``build.zig``.cpu_arch` で指定したターゲットアーキテクチャです。
今回は `x86_64` で固定ですが、他のアーキにも対応するようにした場合ターゲットに応じて変化します。
コンパイル時に決定する値であるため、この`switch`文もコンパイル時に評価され、対応するアーキのルートファイルが export されます。

> [!INFO] usingnamespace
>
> [usingnamespace](https://ziglang.org/documentation/master/#usingnamespace) は、指定した構造体のフィールド全てを現在のスコープに持ってきてくれる機能です。
> 今回の場合、単純に `@import("arch/x86/arch.zig")` すると以下のように利用側では一段余計なフィールドを指定する必要があります:
> 今回の場合、単純に `@import("arch/x86/arch.zig")` すると以下のように利用側で一段余計なフィールドを指定する必要があります:
>
> ```zig
> // -- surtr/arch.zig --
Expand All @@ -76,15 +76,15 @@ pub usingnamespace switch (builtin.target.cpu.arch) {
> someFunction(); // このようなことはできない
> ```
`arch/x86/arch.zig` はアーキ依存のコードに置いて `arch` 以外から利用したいファイルを定義します
`arch/x86/arch.zig` はアーキ依存のコードの内 `/arch` 以上の階層から利用したいファイルを定義します
今回はページテーブルを実装したいため、 `arch/x86/page.zig` を作成したあと、
`arch/x86/arch.zig` から `page.zig` を export します。
```surtr/arch/x86/arch.zig
pub const page = @import("page.zig");
```
これで `boot.zig` から x64 のページングに関する機能を利用できるようになりました
これで `boot.zig` から x64 のページングに関する機能を参照できるようになりました

## ページテーブルエントリ

Expand Down Expand Up @@ -155,8 +155,8 @@ UEFI が用意してくれたページテーブルは仮想アドレスと物理
あまり名前が直感的でないため、本シリーズでは **Lv4**, **Lv3**, **Lv2**, **Lv1** と呼ぶことにします。

まずは4つのエントリそれぞれを表す構造体を定義していきます。
上の画像から分かるとおり、4つのエントリはそれも同じような構造を持っています[^1]
そこで、以下のように `EntryBase()` という関数を定義し、それを使って4つのエントリを定義します:
上の画像から分かるとおり、4つのエントリはどれも同じような構造を持っています[^1]
そこで、以下のように `EntryBase()` という、型を返す関数を定義し、それを使って4つのエントリを定義します:

```surtr/arch/x86/page.zig
const TableLevel = enum { lv4, lv3, lv2, lv1 };
Expand Down Expand Up @@ -238,8 +238,8 @@ pub inline fn address(self: Self) Phys {
ページング操作をする関数では物理アドレスと仮想アドレスを取り違えてしまうミスをしてしまいがち[^3]なため、
それを防ぐために要求するアドレスが物理アドレスと仮想アドレスかのどちらなのかを明示します[^4]
`address()` 関数は、自身の `phys` をシフトして物理アドレスに変換するだけのヘルパー関数です。
返される物理アドレスは、エントリがページをマップする(`.ps==true`)であるならばマップするページの物理アドレスです
ページテーブルを参照する(`.ps==false`)場合には、参照するページテーブルの物理アドレスになります
返される物理アドレスは、エントリがページをマップする場合 (`.ps == true`) はマップするページの物理アドレスです
ページテーブルを参照する場合 (`.ps == false`) は参照するページテーブルの物理アドレスになります

続いて、ページテーブルエントリを作成する関数を定義します。
ページをマップするエントリを作成する場合には簡単です:
Expand All @@ -261,7 +261,7 @@ pub fn newMapPage(phys: Phys, present: bool) Self {
なお、Surtr/Ymir では 512GiB ページはサポートしないため、
もしも `Lv4Entry` (つまり `level == .lv4`) に対してこの関数を呼び出そうとした場合にはコンパイルエラーとします。

同様に、ページテーブルを参照するエントリを作成する関数も定義します:
同様に、ページテーブルを参照するエントリを作成する関数も定義します
この場合の引数は物理ページのアドレスではなく、自分よりも1レベルだけ低いエントリへのポインタにします。
そのためには、「自分よりも1レベル低いエントリの型」を定義してあげる必要があります。
`BaseType()`が返す構造体に以下の定数を持たせましょう:
Expand All @@ -275,7 +275,7 @@ const LowerType = switch (level) {
};
```

自身が`Lv4Entry`ならば `LoterType``Lv3Entry` になります。
自身が`Lv4Entry`ならば `LowerType``Lv3Entry` になります。
`Lv1Entry` よりも下のエントリは存在しないため、`Lv1Entry` の場合は空の構造体を返します。
これを用いると、ページテーブルを参照するエントリを作成する関数は以下のようになります:

Expand Down Expand Up @@ -418,7 +418,9 @@ pub fn map4kTo(virt: Virt, phys: Phys, attr: PageAttribute, bs: *BootServices) P

先程の図で見たように、CR3 からスタートして Lv1 エントリまでページウォークをします。
その過程でページテーブルが存在しない、つまり `lvNent.present == false` である場合には `allocateNewTable()` で新しいページテーブルを作成します。
なお、この関数では対象の仮想アドレスが既にマップされている場合は想定していないため、以下の挙動をします:
なお、この関数では対象の<u>仮想アドレスが既にマップされている場合は想定していません</u>。
言い換えると、この関数は必ず新しいマッピングを作成することしか想定していません。
よって、既に既存のマッピングが存在する場合には以下のような挙動をします:

- 仮想アドレスが既に 4KiB ページにマップされている場合: `AlreadyMapped` エラーを返す。
- 仮想アドレスが既に 2MiB 以上のページにマップされている場合: 既存のマップを上書きする。
Expand All @@ -427,10 +429,10 @@ Lv1 にまでたどり着いたら、事前に定義した `newMapPage()` を使
その際、ページの属性に応じて `rw` フラグを設定します。
`rw == true` の場合は read/write になり、それ以外の場合は read-only になります。

なお、この関数は<u>新たなマッピングを作成することしか想定していない</u>ため、 TLB をフラッシュする必要がありません。
なお、この関数は新たなマッピングを作成することしか想定していないため、最後に TLB をフラッシュする必要がありません。
仮に既存のマップを変更するような場合には、CR3 をリロードするか `invlpg` 等の命令を使って TLB をフラッシュする必要があります。

新たにページテーブルを確保する関数は以下のように実装されています:
新たにページテーブルを確保する関数 `allocateNewTable()` は以下のように実装されています:

```surtr/arch/x86/page.zig
fn allocateNewTable(T: type, entry: *T, bs: *BootServices) PageError!void {
Expand All @@ -450,17 +452,18 @@ fn clearPage(addr: Phys) void {

Boot Services の [AllocatePages()](https://uefi.org/specs/UEFI/2.9_A/07_Services_Boot_Services.html#efi-boot-services-allocatepages) を使って1ページだけ確保します。
この際、メモリタイプは `BootServicesData`[^6] を指定します。
このページテーブルは Ymir が新たにマッピングを作成するまで Surtr から Ymir に処理が移っても使われ続けます。
他のチャプターで説明しますが、`BootServicesData` と指定することで Ymir 側からこの領域はページを新たにマッピングした場合に開放して良い領域であることを明示しています。
このページテーブルは Ymir が新たにマッピングを作成するまでは、Surtr から Ymir に処理が移っても使われ続けます。
つまり、この領域は Ymir が解放して自由に使って良い領域ではありません。
[のちのチャプター](../kernel/page_allocator.md) で説明しますが、メモリタイプを `BootServicesData` にすることで
Ymir は自身の新しいページテーブルを作成するまではこの領域を利用不可能であるということを Ymir に伝えています。

確保したページは `clearPage()` でゼロクリアしています。
最後に、エントリに新たに作成したページテーブルの物理アドレスをセットしています。

## Lv4 テーブルを writable にする

以上で 4KiB のページをマップできるようになりました。
実際に適当の仮想アドレスをマップできることを確認してみましょう。

実際に適当な仮想アドレスをマップできることを確認してみましょう。
`boot.zig` で以下のように適当なアドレスをマップします:

```surtr/boot.zig
Expand Down Expand Up @@ -500,7 +503,7 @@ CR0 - 0000000080010033, CR2 - 000000001FC01FF8, CR3 - 000000001FC01000
```

ページフォルトが発生してしまいました[^7]
ページフォルトが発生したアドレスは CR2 に入っており、今回は `0x1FC01FF8` です。
ページフォルトが発生したアドレスは **CR2** に入っており、今回は `0x1FC01FF8` です。
このアドレスは、CR3 が指すページ、すなわち Lv4 テーブルが入っているページと一致しています。
オフセットの `0xFF8` は指定した仮想アドレス `0xFFFF_FFFF_DEAD_0000` に対応するテーブル内のエントリのオフセットです。
[gef](https://github.com/bata24/gef)`vmmap` コマンドで Lv4 テーブルの様子を見てみます:
Expand All @@ -512,13 +515,14 @@ Virtual address start-end Physical address start-end To
...
```

どうやら **UEFI が提供する Lv4 テーブルは read-only になっている** ようです。
`Flags` から分かるとおり、どうやら **UEFI が提供する Lv4 テーブルは read-only になっている** ようです。
このままでは Lv4 テーブル内のエントリを書き換えることができないため、
Lv4 テーブルを書き込み可能にする必要があります。

Lv4 テーブルが存在するページの属性を変更するためには、ページテーブルエントリを修正する必要があります。
しかし、そのエントリ自体が現在は read-only になっているため、変更を加えることができません。
よって、Lv4 テーブル自体を書き込み可能にするためには、Lv4 テーブル自体をコピーするしかありません。
鶏が先か卵が先かみたいな問題になってしまいました。デッドロックです。
Lv4 テーブル自体を書き込み可能にするためには、Lv4 テーブル自体をコピーするしかありません。
以下のような関数を定義し、Lv4 テーブルを書き込み可能にします:

```surtr/arch/x86/page.zig
Expand All @@ -535,10 +539,10 @@ pub fn setLv4Writable(bs: *BootServices) PageError!void {
}
```

Boot Services を利用して新たにページテーブルを確保し、現在の Lv4 テーブルをスブテコピーします
`setLv4Writable()`Boot Services を利用して新たにページテーブルを確保し、現在の Lv4 テーブルを全てコピーします
最後に `loadCr3()` で CR3 を新しく確保した Lv4 テーブルの物理アドレスにセットします。
CR3 のリロードは全ての TLB をフラッシュするため、以降は新しい Lv4 テーブルが使われるようになります。
新しく作成したページテーブルには read-only の設定がされていないため、これで Lv4 テーブルを書き込み可能にできます
新しく作成したページテーブルには read-only の設定がされていないため、これで Lv4 テーブルが書き込み可能になります

`boot.zig` で 4KiB ページをマップする前に Lv4 テーブルを書き込み可能にしてみましょう:

Expand All @@ -562,14 +566,14 @@ Virtual address start-end Physical address start-end To

ちゃんと指定した仮想アドレス `0xFFFFFFFFDEAD0000` が 物理アドレス `0x100000` にマップされていることがわかります。

## アウトロ
## まとめ

本チャプターでは、4-level paging におけるページテーブルエントリの構造体を定義し、4KiB ページをマップする関数を実装しました。
実際に 4KiB ページをマップする際には、Lv4 テーブルが read-only になっているためページフォルトが発生してしまいます。
そのため、Lv4 テーブルを書き込み可能にするための関数を実装しました。

これで Ymir カーネルを要求されたアドレスにマップするための準備が整いました。
次回は、Ymir を実際にカーネルにロードする処理を実装します
次チャプターでは、Ymir Kernel を実際にロードする処理を実装していきましょう

[^1]: 厳密には各エントリのフィールドには異なるものもありますが、本シリーズでは問題がなく簡単のために同じ構造体を使うことにします。
[^2]: このような構造体を *integer-backed packed struct* と呼びます。
Expand Down

0 comments on commit b1cea31

Please sign in to comment.