前置き
本記事は「ゼロからの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でカーネルファイルを読み込む部分を作成する.以下の手順を行うだけ.
- ファイルを開く
- ファイル全体を格納できる十分なメモリを確保する
- ファイルの中身を読み取る
EFI_FILE_INFO型で情報を読み取る.型の宣言は,edk2/MdePkg/Include/Guid/FileInfo.h
にある.
FileName
は,\kernel.elf
という文字列を格納するためのもので,それより大きいサイズ分をEFI_FILE_INFOで確保しておく.
GetInfo()
でEFI_FILE_INFO情報を取得し,FileSizeを読み取る.
読み取った大きさをもとに,gBS->AllocatPages()
で必要分のメモリを確保する.
メモリの確保方法は以下の三種類
- AllocateAnyPages : どこでもいいから空いている場所に確保
- AllocateMaxAddress : 指定したアドレス以下で空いている場所に確保
- 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は何となく使っていたので,ちょっと理解が深まった.