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自作入門 メモ #4

Posted at

前置き

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

ようやく勉強方法が定まってきました.
以下のように章を学んでいきます.

  1. まず章を全部読む
  2. 読み返しつつ手を動かす
  3. 書籍に載っている部分はプログラムをコメントアウトして書籍のコードを写経する

学ぶこと

  • 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セグメントをコピーしてやればよい.
その際に,以下の手順をとる.

  1. kernel.elfを一時領域に読み込む
    • gBS->AllocatePool() + kernel_file->Read()
    • kernel_first_rangeとkernel_last_range`を取得
  2. 読み込んだkernelのプログラムヘッダを読み,最終目的地の番地の範囲を取得する
    • CalcLoadAddressRange() + gBS->AllocatePages()
    • Address領域は(kernel_last_range - kernel_first_range + 0xFFF) / 0x1000
  3. 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

\は省略して開業なしにしてもOK.
これで実際に動かすと,下のように真っ白な背景に黄緑の長方形を表示することができる.
image.png

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?