ゼロからのOS自作入門 メモ #3

# 前置き
本記事は「ゼロからのOS自作入門」のメモになります。
仕事上OSの知識があってもいいなと思って、取り組むことにしました。
各章に対し、1記事をOutputする予定です。
前記事：https://qiita.com/fuji3195/items/df328544ca8e33618352

## QEMUでいろいろ見る．
レジスタの中身を確認
```
(qemu) info registers
RAX=0000000000000000 RBX=0000000000000001 RCX=000000003fb7b1c0 RDX=0000000000000031
RSI=0000000000000400 RDI=000000003fea92a0 RBP=000000000000002e RSP=000000003fea8870
R8 =0000000000000000 R9 =000000003fecc30f R10=0000000000000050 R11=0000000000000000
R12=000000003e67d73e R13=000000003fea8900 R14=000000003fea88b8 R15=000000003f21b920
RIP=000000003e67c411 RFL=00000202 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0038 0000000000000000 ffffffff 00af9a00 DPL=0 CS64 [-R-]
SS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 0000000000000000 0000ffff 00008200 DPL=0 LDT
TR =0000 0000000000000000 0000ffff 00008b00 DPL=0 TSS64-busy
GDT=     000000003fbee698 00000047
IDT=     000000003f306018 00000fff
CR0=80010033 CR2=0000000000000000 CR3=000000003fc01000 CR4=00000668
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
EFER=0000000000000500
FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80
FPR0=0000000000000000 0000 FPR1=0000000000000000 0000
FPR2=0000000000000000 0000 FPR3=0000000000000000 0000
FPR4=0000000000000000 0000 FPR5=0000000000000000 0000
FPR6=0000000000000000 0000 FPR7=0000000000000000 0000
XMM00=0000000000000000 0000000000000000 XMM01=0000000000000000 0000000000000000
XMM02=0000000000000000 0000000000000000 XMM03=0000000000000000 0000000000000000
XMM04=0000000000000000 0000000000000000 XMM05=0000000000000000 0000000000000000
XMM06=0000000000000000 0000000000000000 XMM07=0000000000000000 0000000000000000
XMM08=0000000000000000 0000000000000000 XMM09=0000000000000000 0000000000000000
XMM10=0000000000000000 0000000000000000 XMM11=0000000000000000 0000000000000000
XMM12=0000000000000000 0000000000000000 XMM13=0000000000000000 0000000000000000
XMM14=0000000000000000 0000000000000000 XMM15=0000000000000000 0000000000000000
```
本には0x067ae4c4とあるが，確認するアドレスはRIPで指定されているアドレス．
今回は0x3e67c411を見てみる．なんか中途半端なアドレス...
```
(qemu) x /4xb 0x3e67c411
000000003e67c411: 0xeb 0xfe 0x48 0x83
(qemu) x /2i 0x3e67c411
0x000000003e67c411:  jmp    0x3e67c411
0x000000003e67c413:  sub    $0x28,%rsp
```
本に書いてある内容の通り，逆アセンブルで何か出てきた．
1行目は同じだが，2行目は何かしら異なる．
1行目の部分が`while(1)`の部分に該当する．

## レジスタ
レジスタには汎用レジスタと特殊レジスタがある．
汎用レジスタ：値を記憶するためのもの．DRAMと比べても早く，2GHzで動くCPUなら読み書きは0.5ns程度(DRAMの200倍くらい)．
特殊レジスタ：CPUの設定や，タイマーなどのCPU内蔵の機能を制御するためのレジスタ．組み込み系はこっちをよくいじるイメージ．
x86-64の汎用レジスタは以下の16個で，CPUの演算対象として指定できる．
- RAX, RBX, RCX, RDX, RBP, RSI, RDI, RSP, R8~R15
これはx86-64のもので，MIPSやARMはまた構成が異なる．
演算対象として指定する際は，以下のように記述する．
```
// opecode, operand1, operand2
add rax, rbx // rax += rbxと同じ意味
```
- opecode : アセンブリ命令の名前のこと
- operand1 : 書き込み先オペランド
- operand2 : 読み込み元オペランド

各汎用レジスタはint, charなど必要に応じてサイズが変わるので，その際に命令形式も変わる．
RAX型：0-7bit = AL, 8-15bit = AH, 0-15bit = AX, 0-31bit = EAX, 0-63bit = RAX (RBX, RCX, RDXも同様)
RPB型：0-7bit =BPL,               0-15bit = BP, 0-31bit = EBP, 0-63bit = RBP (RSP, RSI, RDIも同様)
R8型 ：0-7bit =B8B,               0-15bit =R8W, 0-31bit = R8D, 0-63bit = R8  (R8~R15)

特殊レジスタは汎用レジスタと比べてとても多い．代表的なものは以下のもの．
- RIP : CPUが次に実行する命令のメモリアドレスを保持．たとえば，jmpやcallでの移動先アドレスが保存される
- RFLAGS：フラグを集めたレジスタ．各bitで異なる役割を持つ．命令実行に伴って値が変化していく
- CR0：CPUの重要な設定を集めたレジスタ

## カーネル実装
gitのレポジトリをday03aに切り替えると，kernelディレクトリが現れる．
内部は`hlt`命令のみを記述した簡単なもの．
以下のようにコンパイルする．
```
clang++ -O2 -Wall -g \
    --target=x86_64-elf \ // x86_64向けの機械語を生成する．出力ファイル形式をELFとする．
    -ffreestanding \      // フリースタンディング環境向け(=OSがない状態で動く)にコンパイルする
    -mno-red-zone \       // Red Zone機能を無効にする． (OSを作るときにとりあえずつけとくやつ1)
    -fno-exceptions \     // C++の例外機能を使わない．(OSを作るときにとりあえずつけとくやつ2. OSのサポートを必要とするため．)
    -fno-rtti \           // ++の動的型情報を使わない．(OSを作るときにとりあえずつけとくやつ3．OSのサポートを必要とするため．)
    -std=c++17 \          // C++のバージョンをC++17とする．
    -c main.cpp           // コンパイルのみでリンクはしない
// objectファイルから実行ファイルを作成する．
ld.lld \
    --entry KernalMain \    // KernelMain()をエントリポイントとする
    -z norelro \            // relocation情報を要見込み専用にする機能を使わない．
    --image-base 0x100000 \ // 出力されたバイナリのベースアドレスを0x100000とする．
    --static \              // 静的リンクを行う．
    -o kernel.elf \         // 出力ファイルをkernel.elfとする．
    main.o
```

次に，Bootloaderでカーネルファイルを読み込む部分を作成する．以下の手順を行うだけ．
1. ファイルを開く
2. ファイル全体を格納できる十分なメモリを確保する
3. ファイルの中身を読み取る

EFI_FILE_INFO型で情報を読み取る．型の宣言は，`edk2/MdePkg/Include/Guid/FileInfo.h`にある．
`FileName`は，`\kernel.elf`という文字列を格納するためのもので，それより大きいサイズ分をEFI_FILE_INFOで確保しておく．
`GetInfo()`でEFI_FILE_INFO情報を取得し，FileSizeを読み取る．
読み取った大きさをもとに，`gBS->AllocatPages()`で必要分のメモリを確保する．
メモリの確保方法は以下の三種類
1. AllocateAnyPages   : どこでもいいから空いている場所に確保
2. AllocateMaxAddress : 指定したアドレス以下で空いている場所に確保
3. AllocateAddress    : 指定したアドレスに確保

今回は3を採用する．これは，ld.lldですでに`--image-base 0x100000`を指定してしまっているため．
なお，AllocatePagesでの+0xFFFとしているのは整数の切り上げ処理．
さらに，kernelを起動したら，`gBS->ExitBootServices()`で，今まで動いていたUEFI BIOSのBoot Serviceを止めておく．
`ExitBootServices()`はメモリマップの情報`memmap.map_key`を要求する．
刻々と変わっていくので，1回目の時は基本的に失敗する．
`GetMemoryMap()'で失敗するか，2回目の`ExitBootServices()`で失敗した場合は，`while(1)`で停止してしまう．

最後にカーネルを起動する．カーネルを起動する際には，エントリポイントを探して呼び出す．
ELF形式はOffset 24Byteの位置から8byte整数として書かれているため，それをEntryPointType*に入れてしまって，実行すればよい．

なぜか自分の環境ではエントリポイントが若干ずれていた．
```
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x101120 <-- 0x100000のはずなのになっていない...
  Start of program headers:          64 (bytes into file)
  Start of section headers:          1040 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         4
  Size of section headers:           64 (bytes)
  Number of section headers:         14
```

どうやらエントリポイントがずれているのは，lldのバージョンが新しいかららしい．
ELFローダを4章で実装するようなので，いったんうまくいかないのはスルーすることにする．
https://github.com/uchan-nos/os-from-zero/issues/134

## ピクセルを描く
`git checkout osbook_03b`ではcheck outできなかった．自分だけ？
### BootLoaderから
`OpenGOP()`を使って，全部1にすると，白い画面になる．
ここで，`gop->Mode->Info->PixelFormat`で色のRGPをちゃんと指定すると，それ相応の色になる，らしい．

### kernelから
エントリポイントがずれているので，やっても意味がないと思い読むだけ．
基本的にkernelを実行する部分は引数を持つ以外同じ．
`frame_buffer`がどのようにkernelで起動させた画面に反映されるかは，よくわからなかった．

## エラー処理
エラー処理は，正常でない動作を要求したときに，見かけ上は問題ないが内部で破壊が起こってしまうのを防ぐ．
例として，`gBS->AllocatePages()`でエラー処理を行っている．
UefiMainからreturnするのではなく，Haltで無限ループすることで，エラーだと知らせる仕組みにしている．
これは，エラーと認識してもすぐにログが消えてしまうのを防ぐため．

## アセンブリについて
関数に飛ぶときのアセンブラの典型的な例を示している．
```
push rbp       ;  RSP add -=8 --> RBPをRSPにいれる． (RSPはスタックポインタ．
mov  rbp, rsp  ;  rbp = rsp; 基準点としてRSPのアドレスを覚えておく．
mov  dword ptr[rbp - 4], 42  ;  int i = 42; rbpから4byte分マイナスしたアドレスに，42を書き込んでおく．
; int * p = %i
lea  rax, [rpb - 4]   ; lea = メモリアドレスを計算した値をレジスタに書き込む．本例では，raxに先ほど宣言したiのアドレスをraxに入れている．
mov  qword ptr [rbp - 16], rax  ; 先ほど計算したraxのアドレス値を，rbp-16のメモリ領域に書き込む．qwordは8byteという意味．

; int r1 = *p;
mov  rax, qword ptr [rbp - 16]  ; pに書かれた値を読み，raxに覚えておく．
mov  ecx, dword ptr [rax]       ; raxが指すメモリ領域から4byteを読み出す．
mov  dword ptr [rbp-20], ecx    ; 変数r1にraxから取り出した値 (=ecx)を書き込む．

; *p = 1;
mov  rax, qword ptr [rbp - 16]  ; rbp-16(pに書かれた値)をraxに読み出す．
mov  dword ptr [rax], 1         ; raxのアドレス先 ( = i)に1を入れる．

; int r2 = i;
mov  ecx, dword ptr [rbp - 4]   ; rbp-4のポインタから4byte読みだしたもの ( = i )を，ecxに読み出す．
mov  dword ptr [rbp - 24], ecx  ; ecxを rbp-24の位置に書き込む．(rbp-20からさらに-4している)

; uintptr_t addr = reinterpret_cast<uintptr_t>(p);
mov  rax, qword ptr [rbp - 16]  ; pointer rbp-16 (= p)をraxに読み出す．
mov  qword ptr [rbp - 32], rax  ; raxをrbp-32のアドレスに書き込む．この時，8byte分使っている．

; int* q = reinterpret_cast<int*>(addr);
mov  rax, qword ptr [rbp - 32]  ; raxに先ほど書き込んだaddrを読み出す．
mov  qword ptr [rbp - 40], rax  ; rbp-40に同様に書き込む．型は違うが，この時もアドレスなので8byte分

pop  rbp                        ; あらかじめ保存しておいたrbpを呼び戻す．
ret                             ; 元の関数部分に戻る．

# 感想
ブートローダからカーネルを呼び出す部分は理解できた．
lldが異なって再現できないのが若干気になるが，それは次の章で実装できるらしい．
reinterpret_castは何となく使っていたので，ちょっと理解が深まった．