0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

前置き

本記事は「ゼロからの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は何となく使っていたので,ちょっと理解が深まった.
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?