【OS自作記録 3章】初めてのカーネルとピクセル描画

「ゼロからのOS自作入門」を初めて三日目。3章の「画面表示とブートローダ」と4章の「ピクセル描画とmake入門」の「4.5ローダを改良する」の手前まで実装が完了した。ビルドやプログラムの実行で大きく詰まったところはなかったが内容の理解が追いつかない部分が出てきた。とりあえずやってみる精神で100%理解できていなくても実装は進めようと思う。

コードはgithubにあげてる
https://github.com/takumi-pro/os_developmenthttps://github.com/takumi-pro/os_development

レジスタ

レジスタには汎用レジスタと特殊レジスタがある。それぞれ以下のような特徴を持つ。

  • 汎用レジスタ
    • 主目的は値の記録
    • 全て8バイト
    • 汎用レジスタの一部を小さなレジスタとしてアクセスできる
  • 特殊レジスタ
    • 値を記録する
    • 値を書く行為が何らかの動作のトリガーになる
      • CR0のビット0に1を書く→保護モードに遷移
      • CR0のビット31に1を書く→ページングが有効化

初めてのカーネル

ここでは初めてのOS本体(カーネル)を開発した。本書はブートローダとカーネルを別々のファイルとして開発して、「ブートローダからカーネルを呼び出す」という形式にしている。ブートローダは本書のgithubのソースコードを使用し、カーネルのコードは本書を写経する形で進めていく予定だ。
初めてのカーネルとして以下のような無限ループするプログラムを作成した。

main.cpp
extern "C" void KernelMain() {
  while (1) __asm__("hlt");
}
  • インラインアセンブラ記法
    • __asm__("hlt")の部分でプログラム中にアセンブリ言語命令を埋め込んでいる

この関数がブートローダから呼び出され(エントリポイント)実行される。main.cppを作成したら、コンパイルとリンクを行いELF形式の実行ファイルを生成しブートローダから起動する。
簡単なプログラムだが、ブートローダがカーネルファイルを読み込み、カーネルが起動するという一連の流れをつかむことができたためOS起動の全体像が少し見えた気がする。Linucでもブートに関する知識が問われていたため参考書を読み返して復習しようと思う。

3章ではピクセル描画も行なった。ブートローダからのピクセル描画とカーネルからのピクセル描画の2種類の描画方法を実装した。UEFIにはGOP(Graphics Output Protocol)というピクセル描画に必要な情報を提供してくれる機能があるためそれを活用する。ピクセル描画に必要な情報を以下にまとめる。

  • フレームバッファの先頭アドレス
    • ピクセルを敷き詰めたメモリ領域
  • 解像度
    • フレームバッファの幅と高さ
  • フレームバッファの非表示領域を含めた幅
  • 1ピクセルのデータ形式
    • 1ピクセル何バイトか

カーネルからピクセル描画をする場合、ブートローダからカーネルへ必要な情報を渡す必要がある。ブートローダから情報を渡す以外にもカーネルでその情報を受け取る処理をエントリポイントとなる関数で行う。上記のプログラムで言うとKernelMain関数の引数を追加し情報を受け取る。

main.cpp
#include <cstdint>

extern "C" void KernelMain(
  // ブートローダからのフレームバッファの先頭アドレス
  uint64_t frame_buffer_base,
  // ブートローダからのフレームバッファのサイズ
  uint64_t frame_buffer_size
) {
  // frame_buffer_baseをuint8_tのポインタ型に変換
  uint8_t* frame_buffer = reinterpret_cast<uint8_t*>(frame_buffer_base);
  for (uint64_t i = 0; i < frame_buffer_size; ++i) {
    frame_buffer[i] = i % 256;
  }
  while (1) __asm__("hlt");
}

上記のカーネルを実行すると画面に模様が描画できる。

3.7 ポインタとアセンブリ言語では、ポインタをアセンブリ言語の視点で説明しており、内部的な動作が通常の変数とポインタ変数で差異がないことが分かった。