前置き
本記事は「ゼロからのOS自作入門」のメモになります。
仕事上OSの知識があってもいいなと思って、取り組むことにしました。
各章に対し、1記事をOutputする予定です。
前記事:https://qiita.com/fuji3195/items/e8f79f30574a585840a7
ようやく勉強方法が定まってきました.
以下のように章を学んでいきます.
- まず章を全部読む
- 読み返しつつ手を動かす
- 書籍に載っている部分はプログラムをコメントアウトして書籍のコードを写経する
学ぶこと
- makefile
- ピクセル描画を実装する
- ピクセル描画をC++のクラス/仮想関数を使った記述に書き直す
- vtable
- ブートローダを改良する <-- 3章でうまく動かなかった部分が動くようになるはず
Makefileによるルール作成
今まで主導でやってきたclang++やld.lldをMakefile内に記述する.
これによりコマンドはmakeだけでOKになる.
makefileの変数は以下の通り
| 変数名 | 意味 |
|---|---|
| TARGET | このMakefileが生成する最終成果物 |
| OBJS | TARGETを作るのに必要なオブジェクトファイル軍 |
| CXXFLAGS | コンパイルオプション |
| LDFLAGS | リンクオプション |
.PHONYは偽物のTargetを指定するもの.
Makefileは基本的には以下の構成になっている.
ターゲット:必須項目
レシピ
必須項目があれば,それに対してのルールを再帰的に実行していく.
allに対する動作手順は以下の通り
all: kernel.elf
--> 必須項目 (kernel.elf)のルール(kernel.elf: main.o Makefile)を実行
--> 必須項目 (main.o)のルール(main.o: main.cpp Makefile)を実行
-->必須項目 (main.cpp)のルールを実行しようとするが,何もないので行わない
-->必須項目 (Makefile)のルールを実行しようとするが,何もないので行わない
-->レシピを実行.clang++でmain.oを生成.
--> 必須項目 (Makefile)のルールを実行しようとするが,何もないので行わない
--> レシピを実行.ld.lldでkernel.elfを生成
-->レシピなし.何もしない.
また,Makeの記号の意味も多少載せておく.
| 変数名 | 意味 |
|---|---|
| $< | 必須項目の先頭一つ.all:kernel.elfならkernelが該当 |
| $^ | 必須項目すべてをスペース区切りで並べたもの |
| $@ | ターゲット (拡張子含む) |
| $* | パターンルールにおける幹(stem). ざっくりいうと,%.oの%に該当する部分 |
ピクセル描画を実装する.
好きな位置に好きな色を描画する機能を実装する.
frame_buffer_config.hppフレームバッファの構成情報を表す構造体を宣言する.
UEFIの規格で,ピクセルデータにはデータ形式が4種類ある.
- PixelRedGreenBlueReversed8BitPerColor
- PixelBlueGreenRedReversed8BitPerColor
- PixelBitMask // 描画プログラムが複雑になりがちなのでパス
- PixelBltOnly // メモリに描いた絵を一気にコピーすることにより描画する方式.大変なのでパス
ブートローダがUEFIのGOPから取得した情報をOS本体に渡す.
KernelMainでWritePixelを使って描画する.WritePixelも実装する.
実際に写経するとわかるが,各PixelごとにWritePixelを使っており,50万回Loopがはしる.
これを解決するため,C++のクラスを活用する.
ピクセル描画をC++のクラス/仮想関数を使った記述に書き直す
クラス/純仮想関数を導入する
純仮想関数はWrite()メソッドで,宣言の際は=0で純仮想関数であることを示す.
純仮想関数は,インターフェース(=実際の処理の内容は決まっていないが,戻り値と引数の仕様,関数名は決まっている関数のこと)を表現できる.
PixelWriter::PixelWriterでフレームバッファの構成 (FrameBufferConfig構造体のこと)をクラスに保持する.
これにより,Write()で構成情報を逐一渡す必要がなくなる.
(WritePixel()の時は1pixel描画するたびに構成情報を渡す必要があった.)
インターフェースに対して,実装を行う.
枠組みはPixelWriterで,それを継承してRGBResv8BitColorPixelWriterクラスとBGRResv8BitColorPixelWriterクラスを定義する.
Write()はOverrideすることで,もとのメソッドを上書きする.
コンストラクタ/デストラクタは,using PixelWrite::PixelWriter;の記述で,親クラスのものをそのまま使うことができる. <--書かない場合は,親が呼ばれた後に子が呼ばれる.
配置newについて
通常のnewは指定したクラスのインスタンスをヒープ領域に生成する.
int a = new int(0); // intクラスを生成する.その際に(0)を引数に持つコンストラクタを実行する.
ただし,これが使えるのは,OSがメモリを管理できるようにならないと使えない.
そのため,OSに依存しないnew (=配置new)を使う.
配置newはメモリ領域の確保はせず,メモリ領域へのポインタだけを決めて返す.
実装はお手軽で書いてある通り.
実際に実行してみようと記述されているが,ブートローダの改良が済んでいないので確認はできない.
vtable
vtableとは,仮想関数(virtual function)のポインタ表(table)のこと.
仮想関数を含んでいる場合,クラスのインスタンスの先頭にvtableへのポインタが埋め込まれる.これはUserではなく,コンパイラが参照するもの.
仮想関数をすべてテーブルにしておく.
overrideするときは,このテーブルを親クラスから子クラスのポインタへと書き換える.
これによって,子クラスのインスタンスを親クラスとして扱う場合でも,vtable自体は子クラスのvtableが利用される.
ややこしくて難しい...
ブートローダを改良する
readelf -l kernel.elfでkernelの中身を確認する.
先頭のProgram Headerは,セグメントと呼ばれるまとまり.それごとにOffsetを持つので,この情報をもとにローダはファイルをメモリ上に読み込む.
その中でも,LOADセグメントがローダにおいて重要になる.
書籍とは異なるので,LOADの部分だけ手で動かした際の結果を記載する.
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000100000 0x0000000000100000 ; (1)
0x00000000000001a8 0x00000000000001a8 R 0x1000
LOAD 0x00000000000001b0 0x00000000001011b0 0x00000000001011b0 ; (2)
0x00000000000001c9 0x00000000000001c9 R E 0x1000
LOAD 0x0000000000000380 0x0000000000102380 0x0000000000102380 ; (3)
0x0000000000000000 0x0000000000000018 RW 0x1000
これは以下のような情報になっている.
| offset | virtual Addr | file size | memory size | flag |
|---|---|---|---|---|
| 0x0000 | 0x100000 | 0x01a8 | 0x01a8 | R |
| 0x01b0 | 0x1011b0 | 0x01c9 | 0x01c9 | R E |
| 0x0380 | 0x102380 | 0x0000 | 0x0018 | RW |
offsetの値が前から詰められているのが,もともとのものとは異なっている.
3つ目のLOADにおいてfilesizeが0になっているのは,内部に.bss(=初期値なしのGlobal変数が配置されるセクション)を含んでいるから.
初期値がないためfile上のsizeは0でも,領域として確保しておくためにmemory sizeは存在する.
この三つのLOADセグメントをコピーしてやればよい.
その際に,以下の手順をとる.
- kernel.elfを一時領域に読み込む
-
gBS->AllocatePool()+kernel_file->Read() -
kernel_first_rangeとkernel_last_range`を取得
-
- 読み込んだkernelのプログラムヘッダを読み,最終目的地の番地の範囲を取得する
-
CalcLoadAddressRange()+gBS->AllocatePages() - Address領域は
(kernel_last_range - kernel_first_range + 0xFFF) / 0x1000
-
- LOADセグメントを一時領域から最終目的地へコピーし,一時領域を削除する
-
CopyLoadSegments()+gBS->FreeBool()
-
CalcLoadAddressRange()とCopyLoadSegments()は,書籍内で実装例がある.以下は概要説明のみ.
- CalcLoadAddressRange() :
Ehdrから'phdr'配列を取得し,そのうちPT_LOADTypeの間で最初と最後の値を確認する - CopyLoadSegments() : ehdr + phdr[i].p_offsetからp_vaddrが指す最終目的地へデータをコピーし,セグメント時のメモリ上のサイズがファイルサイズより大きければ0埋めする
最後にエントリポイントを取得する.これも書籍の通り.
これでローダが完成したので,osbook_day04cのkernelを実際に動かしてみる.
この時,qemuを動かすシェルスクリプトにはkernelを指定する必要がある.
(3章でやっているのだが,次の日にはすっかり忘れていた.)
$HOME/osbook/devenv/run_qemu.sh \
Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi \
$HOME/workspace/mikanos/kernel/kernel.elf
