Skip to content

Commit

Permalink
load_kernel: 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 7999f46 commit bfaed41
Showing 1 changed file with 45 additions and 41 deletions.
86 changes: 45 additions & 41 deletions src/bootloader/load_kernel.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# カーネルのロード

前回までで 4KiB ページのマップができるようになりました。
本チャプターでは Ymir カーネルのELFファイルをパースし、要求する仮想アドレスにカーネルをロードしていきます。
前回チャプターでは 4KiB ページのマップができるようになりました。
ページ操作をできるようにしたもともとの理由は、Ymir Kernel をロードする際に Ymir が要求するレイアウトに合わせて仮想アドレスをマップするためです。
本チャプターでは Ymir Kernel のELF ファイルをパースし、要求する仮想アドレスにカーネルをロードしていきます。

## Table of Contents

Expand All @@ -11,29 +12,28 @@

まず最初に Ymir の仮想アドレスレイアウトを決めます。
アドレスレイアウトをどのようにするかには特に決まりがありません。
例えば、Linux ではユーザランドとカーネルごとに利用する仮想アドレス空間を分けています。
カーネルの中でも、ある部分は `.text` がマップ、ある部分は物理アドレスに direct map されていたりします[^1]

例えば、Linux ではユーザランドとカーネルごとに利用する仮想アドレス空間を分けており、
カーネルの中でもある部分は `.text` がマップ、ある部分は物理アドレスに direct map されていたりします[^1]

また、BitVisor では以下のような仮想アドレスのレイアウトになっているようです[^2]:
また、[BitVisor](https://www.bitvisor.org/) は以下のような仮想アドレスのレイアウトになっているようです[^2]:

| 仮想アドレス | 説明 |
| --- | --- |
| 0x0000000000 - 0x003FFFFFFF | プロセス |
| 0x0040000000 - 0x007FFFFFFF | カーネル |
| 0x00F0000000 - 0x00FEFFFFFF | 物理アドレスの動的割当て |
| 0x8000000000 - 0x8FFFFFFFFF | 物理アドレスの静的割当て |
| `0x0000000000` - `0x003FFFFFFF` | プロセス |
| `0x0040000000` - `0x007FFFFFFF` | カーネル |
| `0x00F0000000` - `0x00FEFFFFFF` | 物理アドレスの動的割当て |
| `0x8000000000` - `0x8FFFFFFFFF` | 物理アドレスの静的割当て |

Ymir では特に理由はなく Linux のレイアウトに近いものを採用します。
Ymir の仮想アドレスレイアウトは以下のようになります:

| 仮想アドレス | 説明 |
| --- | --- |
| 0xFFFF888000000000 - 0xFFFF88FFFFFFFFFF (512GiB) | Direct Map. 物理アドレスの 0 にマップされる。ヒープもここ。 |
| 0xFFFFFFFF80000000 - | Kernel Base. |
| 0xFFFFFFFF80100000 - | Kernel Text. |
| `0xFFFF888000000000` - `0xFFFF88FFFFFFFFFF` | Direct Map. 物理アドレスの `0` にマップされる。ヒープもここ。 |
| `0xFFFFFFFF80000000` - | Kernel Base. |
| `0xFFFFFFFF80100000` - | Kernel Text. |

これらのレイアウトを実現するため、リンカスクリプトを書きます:
これらのレイアウトを実現するため、以下のリンカスクリプトを用意します:

```ymir/linker.ld
KERNEL_VADDR_BASE = 0xFFFFFFFF80000000;
Expand All @@ -53,16 +53,18 @@ SECTIONS {
.data ALIGN(4K) : AT (ADDR(.data) - KERNEL_VADDR_BASE) {
*(.data)
*(.ldata)
}
.bss ALIGN(4K) : AT (ADDR(.bss) - KERNEL_VADDR_BASE) {
*(COMMON)
*(.bss)
*(.lbss)
}
}
```

このリンカスクリプトによって全てのセクションは仮想アドレスの `0xFFFFFFFF80100000` 以降に配置されるようになります。
このリンカスクリプトによって、全てのセクションは仮想アドレスの `0xFFFFFFFF80100000` 以降に配置されるようになります。
また、それらのセクションは仮想アドレスから `0xFFFFFFFF80000000` を引いた物理アドレスにマップされます。

> [!NOTE] 詳しくは...
Expand Down Expand Up @@ -106,11 +108,11 @@ Program Headers:
>
> Ymir では Linux に近いレイアウトを採用しました。
> 特に理由はないですが、Linux をある程度触ったことがある人にとってはなんとなく直感的であるような気がしたからです。
> しかし、Linux に近いレイアウトを採用することは寧ろ良くない選択かもしれません
> しかし、**Linux に近いレイアウトを採用することは寧ろ良くない選択** かもしれません
>
> というのも、Linux に近いレイアウトを採用した場合、あるアドレスが Ymir のアドレスなのかゲスト Linux のアドレスなのかが分かりにくくなります
> `0xFFFFFFFF80100000` というアドレスに breakpoint を設定した場合、Ymir が実行した場合もゲスト Linux が実行した場合もどちらもヒットしてしまいます
> Breakpoint にヒットしたあとで、それが Ymir かゲスト Linux かを判断するのは少し面倒です
> というのも、Linux に近いレイアウトを採用した場合、あるアドレスが Ymir のアドレスなのかゲスト Linux のアドレスなのか」が分かりにくくなります
> `0xFFFFFFFF80100000` というアドレスに breakpoint を設定した場合、Ymir が実行した場合もゲストが実行した場合もどちらもヒットしてしまいます
> Breakpoint にヒットしたあとで、それが Ymir かゲストかを判断するのは少し面倒です
> そうであるならば、最初から Linux と絶対に被らないような領域を使う方が、デバッグする上では好ましいのかもしれません。
## カーネル用のメモリの確保
Expand All @@ -119,7 +121,8 @@ Ymir カーネルのレイアウトが決まったので、今度はカーネル
具体的には、カーネルをロードするメモリとして必要なサイズを計算し、その分だけメモリを確保します。

カーネルのサイズは ELF ファイルをパースして得られるカーネルのメモリマップから計算します。
まずは ELF のセグメントヘッダのイテレータを作成します:
まずは ELF の **セグメントヘッダ (プログラムヘッダ)** のイテレータを作成します。
Zig の標準ライブラリにはセグメントヘッダのイテレータが既に実装されているため、それを使います:

```surtr/boot.zig
const Addr = elf.Elf64_Addr;
Expand All @@ -132,7 +135,7 @@ var iter = elf_header.program_header_iterator(kernel);

カーネルを配置する物理アドレスの最小と最大のを記録する変数、及び仮想アドレスの最小を記録する変数を用意します。
セグメントヘッダのイテレータは、`std.elf.Header.program_header_iterator()` で作成できます。
このイテレータを使ってセグメントヘッダを辿り、最小・最大アドレスに配置されるセグメントのアドレスを計算していきます:
このイテレータを使ってセグメントヘッダを辿り、カーネルが要求する最小・最大アドレスを計算します:

```surtr/boot.zig
while (true) {
Expand All @@ -147,7 +150,7 @@ while (true) {
}
```

セグメントのタイプが `PT_LOAD`[^3] であるセグメントにおいて、現在分かっている最小・最大のセグメントのアドレスを更新する場合には、そのアドレスを記録します。
セグメントのタイプが `PT_LOAD`[^3] であるセグメントが現在分かっている最小・最大のセグメントのアドレスを更新する場合には、そのアドレスを記録します。

続いて、必要なメモリサイズを計算します:

Expand All @@ -171,8 +174,8 @@ log.info("Kernel image: 0x{X:0>16} - 0x{X:0>16} (0x{X} pages)", .{ kernel_start_
[INFO ] (surtr): Kernel image: 0x0000000000100000 - 0x0000000000100003 (0x1 pages)
```

現在 Ymir のセグメントサイズは `kernelEntry()` の 3byte のみであるため、歪な出力になっていますね
しかし、`readelf` でセグメントヘッダを呼んだ時の結果と一致しており
現在 Ymir のセグメントサイズは `kernelEntry()` の 3byte のみであるため、一見すると変な出力になっていますね
しかし、`readelf` でセグメントヘッダを読んだ時の結果と一致しており
必要なページサイズが 1 ページであることも正しく計算できているようです。

最後に、計算したページ分だけメモリを確保してあげます。
Expand All @@ -186,14 +189,14 @@ if (status != .Success) {
log.info("Allocated memory for kernel image @ 0x{X:0>16} ~ 0x{X:0>16}", .{ kernel_start_phys, kernel_start_phys + pages_4kib * page_size });
```

全チャプターと同様に、 Boot Services の [AllocatePages()](https://uefi.org/specs/UEFI/2.9_A/07_Services_Boot_Services.html#efi-boot-services-allocatepages) を使ってページを確保します。
ただし、ここでは**第1引数の `alloca_type` として `.AllocateAddress` を指定することで、第4引数で指定したアドレス丁度にメモリを確保**させます。
このアドレスは、先程計算したセグメントの開始アドレスです
もしも指定された物理アドレスからメモリを確保できなかった場合にはエラーが返ります
前チャプターと同様に、 Boot Services の [AllocatePages()](https://uefi.org/specs/UEFI/2.9_A/07_Services_Boot_Services.html#efi-boot-services-allocatepages) を使ってページを確保します。
ただし、ここでは **第1引数の `alloca_type` として `.AllocateAddress` を指定することで、第4引数で指定したアドレス丁度にメモリを確保** させます。
このアドレスは先ほど計算したセグメントの開始アドレスです
もしも指定された物理アドレスからメモリを確保できなかった場合にはエラーを返すことにします

## 仮想アドレスのマップ

カーネルが要求する物理アドレスにメモリを確保できたため、次は要求する仮想アドレスを確保した物理アドレスにマップします
カーネルが要求する"物理アドレス"にメモリを確保できたため、次は要求する"仮想アドレス"を確保した物理アドレスにマップします
[簡易ページテーブルのチャプター](simple_pg.md) で 4KiB ページをマップする関数を実装したため、それを使ってページをマップします:

```surtr/boot.zig
Expand All @@ -211,8 +214,8 @@ for (0..pages_4kib) |i| {
log.info("Mapped memory for kernel image.", .{});
```

確保した 4KiB ページの枚数分(`pages_4kib`)だけマップを繰り返します
本当であれば、セグメントヘッダが要求する属性(読み込み専用等)でマップするべきですが、ここでは簡単のために全て `.read_write` でマップします。
確保した 4KiB ページの枚数分 (`pages_4kib`) だけページのマップを繰り返します
本当であれば、セグメントヘッダが要求する属性 (読み込み専用等) でマップするべきですが、ここでは簡単のために全て `.read_write` でマップします。

実行してメモリマップを確認すると以下のようになります:

Expand All @@ -229,11 +232,12 @@ Virtual address start-end Physical address start-end To

## カーネルの読み込みとロード

最後に、用意したメモリにカーネルをロードしていきます。
[カーネルのパースのチャプター](./parse_kernel.md) では、Ymir の ELF ヘッダ部分しかメモリに読み込んでいませんでした。
最後に、用意したメモリにカーネルのセグメント全体をロードしていきます。

### セグメントの読み込み

まずは、先ほど必要なメモリサイズを計算したときと同様なセグメントヘッダのイテレータを作成するところから始めます:
まずは、先ほど必要なメモリサイズを計算したときと同様にセグメントヘッダのイテレータを作成するところから始めます:

```surtr/boot.zig
log.info("Loading kernel image...", .{});
Expand All @@ -250,7 +254,7 @@ while (true) {
```

ロードする必要があるのは、やはり `PT_LOAD` セグメントだけです。それ以外の場合にはスキップします。
続いて、セグメントをファイルからメモリに読み出します:
続いて、セグメントを FS からメモリに読み出します:

```surtr/boot.zig
status = kernel.setPosition(phdr.p_offset);
Expand Down Expand Up @@ -286,15 +290,15 @@ log.info(
> なお、カーネルをロードしようとしているメモリは先ほど新たに仮想アドレスをマップしました。
> しかし依然としてストレートマップも有効のままです。
> そのため、この物理アドレスに対しては2通りの仮想アドレスを介してアクセスできます。
> 試しに上のコードの `phdr.p_vaddr`(新しく作成したマップ)を `phdr.p_paddr`(ストレートマップ)に変更してみてください。
> 試しに上のコードの `phdr.p_vaddr` (新しく作成したマップ) `phdr.p_paddr` (ストレートマップ) に変更してみてください。
> 問題なく動くはずです。
### BSS セグメントの初期化

[`.bss` セクション](https://en.wikipedia.org/wiki/.bss) は、ゼロ初期化されるセクションです
[`.bss` セクション](https://en.wikipedia.org/wiki/.bss) は、ロード時にゼロ初期化されるセクションです
ゼロ初期化されることが分かっているため、ELF ファイル中には `.bss` セクションのデータは含まれていません。
セグメントをロードする際に、`.bss` セクションのサイズだけメモリを確保し、ゼロで初期化します
既にメモリは確保してあるため、ここではゼロ初期化を行いましょう:
セグメントをロードする際に、`.bss` セクションのサイズだけメモリを確保し、ゼロで初期化する必要があります
既にメモリは確保してあるため、ここではゼロ初期化だけを行いましょう:

```surtr/boot.zig
const zero_count = phdr.p_memsz - phdr.p_filesz;
Expand All @@ -308,13 +312,13 @@ if (zero_count > 0) {
これで `.bss` セクションの初期化は完了です[^4]
まぁ、今の Ymir には `.bss` セクションは存在しないので実際には何も起こらないんですが...。

## アウトロ
## まとめ

本チャプターでは Ymir カーネルをロードするのに必要なメモリサイズを計算し、その分だけ物理メモリを確保しました。
本チャプターでは Ymir Kernel をロードするのに必要なメモリサイズを計算し、その分だけ物理メモリを確保しました。
その後、カーネルが要求する通りに仮想アドレスをマップし、そこにカーネルをロードしました。

これでいよいよ Ymir を実行する準備ができました。
すぐに Ymir にジャンプしてもいいですが、次回1チャプターは少しお片付けをして、それからカーネルに飛び込むことにしましょう。
すぐに Ymir にジャンプしてもいいですが、次チャプターは少しお片付けをして、それからカーネルに飛び込むことにしましょう。

[^1]: [Complete virtual memory map with 4-level page tables - kernel.org](https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt)
[^2]: [BitVisorの仮想メモリーマップ](https://qiita.com/hdk_2/items/6c7aaa72f5dcfcfda342)
Expand Down

0 comments on commit bfaed41

Please sign in to comment.