Day 2【ゼロからのOS自作入門】


とある事情で06/30までに「[ゼロからのOS自作入門](http://zero.osdev.jp/)」を読み終えたいと思うようになりました。
自律のため毎日勉強したことを投稿しようと思うのでよろしくお願いします。

<div align="center">
    <img src="http://zero.osdev.jp/book-cover.png" width="500px">
</div>

-----

# EDK II入門

- EDK IIはUEFI BIOS自体の開発やその上で動くアプリケーションを開発できる開発キット
- 環境構築が済んでいれば`$HOME/edk2/`にある


# EDK IIでハローワールド（osbook_day02a）

```bash
$ cd $HOME/workspace/mikanos/
$ git checkout osbook_day02a
$ cd MikanloaderPkg/
$ ls
total 16K
-rw-r--r-- 1 ...  217 Jun  3 09:24 MikanLoaderPkg.dec # パッケージ宣言
-rw-r--r-- 1 ... 1.4K Jun  3 09:26 MikanLoaderPkg.dsc # パッケージ記述
-rw-r--r-- 1 ...  217 Jun  3 09:26 Main.c # ソースコード
-rw-r--r-- 1 ...  498 Jun  3 09:26 Loader.inf # コンポーネント定義ファイル
```

- `Loader.inf`内の`ENTRY_POINT`には、起動時に最初に実行される関数を書く
- これを**エントリポイント**という（C/C++の`main()`のようなもの）
- EDK IIではUEFIアプリケーションごとに名前を変えられる
- `Main.c`は前回の`hello.c`よりスッキリしている（EDK IIが提供しているライブラリを`#include <Uefi.h>`でインクルードしているから）
- UEFIでは`Print(L"Hello, Mikan World!\n");`のように**ワイド文字**で文字列を表示する

```bash
$ cd $HOME/edk2/
$ ln -s ../workspace/mikanos/MikanLoaderPkg ./ # edk2/にMikanLoaderPkg/を指すシンボリックリンクをはる
$ ls -s
...
lrwxrwxrwx  1 ...  35 Jun  3 11:36 MikanLoaderPkg -> ../workspace/mikanos/MikanLoaderPkg/
$ source edksetup.sh # Conf/target.txtができる
$ # Conf/target.txtを本書に従って編集（第1版だと表に誤植があったので注意）
$ cd ~/edk2/
$ build # edksetup.shを読み込むことで使えるコマンド
```

ビルドが終わると`$HOME/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi`ができたのでこれを実行してみると確かに文字列が表示された。

# メインメモリ、メモリマップ

- CPUの**メモリコントローラ**のおかげで、ソフトウェアの観点では多数のバイトが直線的に並んでいるように見える（よく見るメモリの絵）
- メインメモリのどこからどこまでが何に使われているかを表す**メモリマップ**というのがある
- アメリカの番地の付け方のように、1次元的にアドレスと用途が対応付けられている
- メモリ領域の大きさを**ページ**で表す。UEFIでは1ページ=4 KiB
- メモリマップには歯抜けの部分（空き地みたいなやつ）があることにも注意。
- メモリを使うならこの空き地を探す必要がある（既存のやつを上書きすると誤動作が生じてしまう）
- ということで、UEFIの機能を使ってメモリマップを取得するプログラムを作る

# （ところで）

今までMikanOSのリポジトリは`git clone`して使っていた。

```bash
$ mkdir $HOME/workspace
$ cd $HOME/workspace
$ git clone https://github.com/uchan-nos/mikanos.git
```

ただファイルを変更してタグを`git checkout`しようとするとエラーになる（これは本書にも書いてある）。
自分のリポジトリとしてプッシュしたかったので色々調べたが、どれもいまいちうまく行かなかった。

そこで今までのローカルの変更は破棄して、本家のリポジトリをForkして使うことにした。
方法は[作者のuchan-nosさんが丁寧に書いてくれていた](https://github.com/uchan-nos/mikanos/blob/master/docs/how-to-send-pull-request.md)のでこれに従った。

```bash
$ cd $HOME/workspace
$ rm -rf mikanos # 今までのローカルの変更は破棄する
$ # how-to-send-pull-request.mdを見てforkする
$ git clone git@github.com:<my_user_name>/mikanos.git
$ cd mikanos
$ git remote -v # 自分の名前になっている
$ git log -l # tagも元来のものが見えている
```

早くに気づいてよかった。Forkするのもはじめてだったので、いい経験ができた。

# メモリマップの取得（osbook_day02b）

```bash
$ cd $HOME/workspace/mikanos/
$ git checkout -b my-changes
$ git branch # 現在のブランチがmy-changesになっていることを確かめた
$ git checkout osbook_day02b
$ git describe --tags # 現在のタグがosbook_day02bになっていることを確かめた
```

- `MikanLoaderPkg/Main.c`内の`GetMemoryMap()`がメモリマップを取得する
- `gBS`は**ブートサービス**のグローバル変数（EDK IIではグローバル変数に`g`をつける）

[UEFI Specification, Version 2.8](https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_final.pdf)の164ページに`GetMemoryMap()`の仕様が書いてあった。

```c
typedef EFI_STATUS(EFIAPI * EFI_GET_MEMORY_MAP)(
    IN OUT UINTN                 * MemoryMapSize,
    IN OUT EFI_MEMORY_DESCRIPTOR * MemoryMap,
    OUT UINTN                    * MapKey,
    OUT UINTN                    * DescriptorSize,
    OUT UINT32                   * DescriptorVersion
);
```

- 関数の説明が難しかったので、先に実際に動かしてみた。`disk.img`をマウントするとたしかに中に`memmap`があり、開くと次のようになっていた。

```csv
Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute
0, 3, EfiBootServicesCode, 00000000, 1, F
1, 7, EfiConventionalMemory, 00001000, 9F, F
2, 7, EfiConventionalMemory, 00100000, 700, F
3, A, EfiACPIMemoryNVS, 00800000, 8, F
4, 7, EfiConventionalMemory, 00808000, 8, F
5, A, EfiACPIMemoryNVS, 00810000, F0, F
6, 4, EfiBootServicesData, 00900000, B00, F
7, 7, EfiConventionalMemory, 01400000, 3AB36, F
8, 4, EfiBootServicesData, 3BF36000, 20, F
9, 7, EfiConventionalMemory, 3BF56000, 270F, F
10, 1, EfiLoaderCode, 3E665000, 2, F
11, 4, EfiBootServicesData, 3E667000, 217, F
12, 3, EfiBootServicesCode, 3E87E000, B6, F
...
```

- 難しかったので次のポインタの説に進むことにした


# ポインタ入門（１）：アドレスとポインタ


```cpp
int i = 42;
int* p = &i; // ポインタ変数、初期値をiのアドレス（&i）にした
```

```cpp
int r1 = *p; // pの指す変数、つまりiの値で初期化
*p = 1; // pの指す変数、つまりiの値を1に変更
int r2 = i; // 1になっている
```

実際にやってみた。

```cpp
#include <iostream>
#include <iomanip>
#include <stdint.h>

int main() {
    int i = 42;
    int* p = &i;

    int r1 = *p;
    *p = 1;
    int r2 = i;

    std::cout << "p = " << p << std::endl;
    std::cout << "r1 = " << r1 << std::endl;
    std::cout << "r2 = " << r2 << std::endl;
    return 0;
}
```

```
$ ./a.out
p = 0x7ffe61025df4
r1 = 42
r2 = 1
```

- C++ではポインタと整数は互いに変換できる。
- 異なるポインタを整数に変換したら、その整数同士は同じになることはない
- 逆も然り




```cpp
#include <iostream>
#include <iomanip>
#include <stdint.h>

int main() {
    int i = 42;
    int* p = &i;

    int r1 = *p;
    *p = 1;
    int r2 = i;

    std::cout << "p = " << p << std::endl;
    std::cout << "r1 = " << r1 << std::endl;
    std::cout << "r2 = " << r2 << std::endl;

    uintptr_t addr = reinterpret_cast<uintptr_t>(p); // convert a pointer into an integer
    int* q = reinterpret_cast<int*>(addr);  // convert an integer into a pointer

    std::cout << "addr = " << addr << std::endl;
    std::cout << "q = " << q << std::endl;

    return 0;
}
```

```
$ ./a.out
p = 0x7ffea1e93f04
r1 = 42
r2 = 1
addr = 140731614838532
q = 0x7ffea1e93f04
```

- たしかに`p`と`q`は同じになっている。この140731614838532というのが変換されたあとの整数らしい。
- というか10進数に変換しただけにみえる。

![Screenshot from 2021-06-04 21-38-18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/686583/0325aab5-1168-377f-00e4-616e4fadcb22.png)


- しかし実際には`p`→`addr`では**ポインタから変数の型情報を消して変換している**
- `addr`→`q`では**変数に型情報を加えて変換している**

# ポインタとアロー演算子

- C/C++には***構造体***と**メンバ**という概念がある
- 構造体の例

```c
struct MemoryMap {
    UINTN buffer_size; // offset: 0 Byte
    VOID* buffer; // offset: 8 Byte
    UINTN map_size;
    UINTN map_key;
    UINTN descriptor_size;
    UINT32 descriptor_version;
};
```

- メンバは宣言順にメモリに配置される
- 構造体の先頭から各メンバの先頭までの距離をオフセットという
- この例では`sizeof(struct MemoryMap)` = 48 Byte

ここで

```c
struct MemoryMap m;
struct MemoryMap* pm = &m;
```

とすると`m`は48 Byte, `pm`は8 Byteの領域を持つ。（ポインタ変数は相手の型によらず一定のバイト数を持つ。あくまでアドレスだから）

`pm`から`m`の各メンバにアクセスするときにアロー演算子が使われる。

```c
pm->map_size = 0;
(*pm).map_size = 0;
m.map_size = 0;
```

どれも同じ処理を表す。

# ポインタのポインタ

- `memmap_file`は「ファイル情報が書かれるメモリ領域」の**アドレス**を表すポインタ変数
- さらに`memmap_file`のポインタを`ptr_ptr`としている
- `ptr_ptr`が使われているのは、**`memmap_file`が指しているアドレスを変更**するため


# 写経

メモリマップを取得するコードが理解できなかったので、写経して読み解いてみることにした。


理解は後回しで、なんとなくわかればいいのかな…


-----

# 感想

- 「メモリマップの取得」とところからいきなり難しくなったなぁ
- 第1版は誤字が多いので正誤表は要確認
- Raspberry Piで起動させてみようと思ったがだめらしい

> Raspberry Pi でも動作する？
> いいえ、できません。 本書で作る OS は x86-64 アーキテクチャ専用の OS です。 x86-64 を採用した Intel Core シリーズや AMD Ryzen などの CPU が搭載されたパソコンが必要です。
>
> Raspberry Pi は ARM という CPU が搭載されており、MikanOS は動作しません。


# 実験

自分のOSを作ってみるということで、`$HOME/test/myOS/`配下に`$HOME/workspace/mikanos/MikanLoaderPkg/*`をコピーし、`Main.c`を次のように変更した。

```c
#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/UefiBootServicesTableLib.h>
#include  <Library/PrintLib.h>
#include  <Protocol/LoadedImage.h>
#include  <Protocol/SimpleFileSystem.h>
#include  <Protocol/DiskIo2.h>
#include  <Protocol/BlockIo.h>


EFI_STATUS EFIAPI UefiMain(){

    Print(L"Hello World!");
    while (1);
    return EFI_SUCCESS;
}

```

使わないライブラリがインクルードされているのは目をつぶっておいてください。

- `MikanLoader.dec`などの名前も`myOS.dec`などにした。
- またそれらの中に書いてある`MikanLoader`という言葉を`myOS`にした
- `~/edk2/`にシンボリックリンクをはった
- `source edk2setup.sh`をして、`Conf/target.txt`を変更した

```diff
-- ACTIVE_PLATFORM       = MikanLoaderPkg/MikanLoaderPkg.dsc
++ ACTIVE_PLATFORM       = myOS/myOS.dsc
```

- `build`すると`Build/`にmyOSのものができていたので、`run_qemu.sh`するとちゃんとハローワールドが出た











-----

# 参考、使ったリンク

- MikanOS ソースコード：https://github.com/uchan-nos/mikanos
- MikanOS 開発環境：https://github.com/uchan-nos/mikanos-build
- osdev-jp：https://osdev.jp/
- 著者（uchan）のTwitter：[@uchan_nos](https://twitter.com/uchan_nos)
- [「ゼロからの OS 自作入門」サポートサイト](http://zero.osdev.jp/)
  - SlackやDiscordでサポート、勉強会（2021年3月27日から）を行っているらしい！　途中からかもだけど参加してみよっと。

- [【誤植】表2.1の1行目の網掛けは無いほうが良いです](https://github.com/uchan-nos/os-from-zero/issues/3)
- [cloneしたリポジトリを自分のリモートリポジトリで作業できているか確かめたい](https://teratail.com/questions/301760)
- [git tagの使い方まとめ](https://qiita.com/growsic/items/ed67e03fda5ab7ef9d08)
- [UEFI Specification, Version 2.8](https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_final.pdf)