📝

「ゼロからのOS自作入門」の リベンジ Part5 🍊

2021/06/19に公開

https://zenn.dev/omiso/articles/820c5b60222a54
の続き

メモを記述

第5章まで進んできたが、何をやっているのかわけわかめになってきたのでひとまず整理をしていく。(第6章へ進もうと思ったが、どこに何を実装すればよいかわけわからんくなった。)

EDK II でブートローダ作成(第2章)

軽く復習

Loader.inf  Main.c  MikanLoaderPkg.dec  MikanLoaderPkg.dsc

Loder.inf ・・・コンポーネント定義ファイル
MikanLoaderPkg.dec・・・パッケージ宣言ファイル
MikanLoaderPkg.esc・・・パッケージ記述ファイル
Main.c ・・・ソースコード

Loder.inf に UefiMain(エントリポイントが記述されている)
→ 要は、main() 関数に相当する。

実装

#include <Uefi.h>
#include <Library/UefiLib.h>

EFI_STATUS EFIAPI UefiMain( EFI_HANDLE image_handle, EFI_SYSTEM_TABLE *system_table) {
  Print(L"Hello, Mikan World!\n");
  while(1);
  return EFI_SUCCESS;
}

メモリマップの実装(第2章)

淡々と実装するべし

カーネルの実装(第3章)

簡易なカーネルを作る。
extern "C" は。C言語形式で関数を定義することを意味する。
名前を修飾を防ぐやくわりなる。副作用として、引数の個数や型が異なるだけの
同名の関数が定義できなくなる。(オーバーロードの機能か)

extern "C" void KernelMain() {
    while(1) __asm__("hlt");
}

カーネルを呼び出すための実装を、ブートローダ側に実装する。
ブートサービスの停止を行う。ブートサービスは裏でひっそり動いて様々な処理をしてくれるが、邪魔なので。

  //stop bootservice
  EFI_STATUS status;
  status = gBS->ExitBootServices(image_handle, memmap.map_key);
  if(EFI_ERROR(status)){
    status = GetMemoryMap(&memmap);
    if(EFI_ERROR(status)) {
      Print(L"failed to get memory map: %r\n", status);
      while(1);
    }
  } 
  status = gBS->ExitBootServices(image_handle, memmap.map_key);
  if(EFI_ERROR(status)){
      Print(L"Could not exit boot service: %r\n", status);
      while(1);
  }

カーネルからピクセルを描く
カーネル側のコードは以下になる。
reinterpret_cast は、整数値からポインタ変数に返すことを意味する

#include <cstdint>

extern "C" void KernelMain(uint64_t frame_buffer_base, uint64_t frame_buffer_size) {
    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");
}

フレームバッファの情報をカーネルに渡す。
エントリポイントを void から変えて、KernelMain に渡せるようにしておく

  typedef void EntryPointType(UINT64, UINT64);
  EntryPointType *entry_point = (EntryPointType*)entry_addr;
  entry_point(gop->Mode->FrameBufferBase, gop->Mode->FrameBufferSize);

ピクセル描画とmake入門(4章)

カーネルで■ものを表示する。

C++ に置き換える

仮想関数を作る

class PixelWriter {
    public:
        PixelWriter(const FrameBufferConfig& config) : config_{config} {}
        virtual ~PixelWriter() = default;
        virtual void Write(int x, int y, const PixelColor& c) = 0;
    protected:
        uint8_t* PixelAt(int x, int y) {
            return config_.frame_buffer + 4 * (config_.pixels_per_scan_line * y + x);
        }
    private:
        const FrameBufferConfig& config_;
};

PixelWriter を継承したクラスを作る
継承というのは、あるクラスを基にして機能の差分を書くやり方。
親クラスから子クラスで上書きすることを override(オーバーライド)という。

class RGBResvv8bitPerColorPixelWriter : public PixelWriter {
    public:
        using PixelWriter::PixelWriter;
        virtual void Write(int x, int y, const PixelColor& c) override {
            auto p = PixelAt(x, y);
            p[0] = c.r;
            p[1] = c.g;
            p[2] = c.b;
        }
};

class BGRResvv8bitPerColorPixelWriter : public PixelWriter {
    public:
        using PixelWriter::PixelWriter;
        virtual void Write(int x, int y, const PixelColor& c) override {
            auto p = PixelAt(x, y);
            p[0] = c.r;
            p[1] = c.g;
            p[2] = c.b;
        }
};

指定された pixel_format に基づいて2つの子クラスの適する方のインスタンス生成
new 関数を追加する
一般の new は指定したクラスのインスタンスのヒープ領域を使う。
これは配置の new になる。ヒープ領域を使うためにはメモリ管理が必要だが、まだ機能がない。
そのため、配置 new を使う。 配置 new は、メモリ領域を確保しない。
配列の機能を使う。
本当は、delete はいらないのだが、delete がないとコンパイルエラーになる。

void* operator new(size_t size, void* buf) {
    return buf;
}

void operator delete(void* obj) noexcept {
}

ローダを改良する

カーネルを読み込むためのメモリを確保する部分で、メモリの大きさを計算する処理が間違っている。

ページ単位の読み込みからバイト単位の読み込みに変更する
一時的な領域にカーネルファイルを読み込みたいだけ。

  status = gBS->AllocatePool(EfiLoaderData, kernel_file_size, &kernel_buffer);
  if(EFI_ERROR(status)) {
    Print(L"failed to allocate pool: %r", status);
    Holt();
  }
  kernel_file->Read(kernel_file, &kernel_file_size, kernel_buffer);
  if(EFI_ERROR(status)) {
    Print(L"error: %r", status);
    Holt();
  }

文字を書く(第5章)

フォントデータの作成
先頭に 0b とついているものは、2進数を意味する

const uint8_t kFontA[16] = {
    0b00000000,
    0b00011000,
    0b00011000,
    0b00011000,
    0b00011000,
    0b00100100,
    0b00100100,
    0b00100100,
    0b00100100,
    0b01111110,
    0b01000010,
    0b01000010,
    0b01000010,
    0b11100111,
    0b00000000,
    0b00000000,
};

このフォントデータを使用して、文字を書きていく。
ビット演算を使用して、塗りつぶすかしないかを決めていく。

void WriteAscii(PixelWriter& writer, int x, int y, char c, const PixelColor& color) {
    if(c != 'A') {
        return;
    }
    for(int dy = 0; dy < 16; ++dy) {
        for(int dx = 0; dx < 8; ++dx) {
            if((kFontA[dy] << dx) & 0x80u) {
                writer.Write(x + dx, y + dy, color);
            }
        }
    }
}

※ この 80u の u は unsigned を表す。
ビット演算メモ: & は、AND演算子を指す。
なので、以下の計算になる。
例) 0b11000001(0xC1) の場合
kFontA[dy] << dx
0b11000001 << 0 ビット左にずらすが変化なし。
0xC1 & 0x80 → 0x80 (塗る)

分割コンパイル

まぁ見ればわかるの飛ばす

フォントを増やす

hanakaku.txt を入手し、以下のように実行する。

../tool/makefont.py -o hankaku.bin hankaku.txt
objcopy -I binary -O elf64-x86-64 -B i386:x86-64 hanakaku.bin hanbaku.o

hankaku.o のデータを変数として参照する
参照する場合は、extern を使用する。これは、どこか他のオブジェクトファイルの変数を参照するという意味。

extern const uint8_t _binary_hankaku_bin_start;
extern const uint8_t _binary_hankaku_bin_end;
extern const uint8_t _binary_hankaku_bin_size;

文字列描画

まぁここは見ればわかるからいいや

コンソールクラスの実装

コンソールとは、制御卓という意味になる。
static は、OSがメモリにロードされてから終了するまで待つ

        static const int kRows = 25, kColumns = 80;

console クラスのインスタンス生成

Console console{*pixel_writer, {0, 0, 0}, {0, 255, 0}};

printk の実装

Linux にはカーネル内部からメッセージを出すために printk() という関数がある。
この関数はカーネル内のどこでも使える。それを実装していく。
printk の実装で、 ...(ドット3つ)は可変長引数。
va_list を使って受け取る。

int printk(const char* format, ...) {
    va_list ap;
    int result;
    char s[1024];

    va_start(ap, format);
    result = vsprintf(s, format, ap);
    va_end(ap);

    console->PutString(s);
    return result;
}

Discussion