3
2
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

ゼロから始めるOS自作入門でOS構築した感想&躓いたところ

Last updated at Posted at 2024-06-30

はじめに

「ゼロから始めるOS自作入門」という本で、0章から30章までやり切り、ゼロからOSを作りました。
その時の感想と躓いたところまとめます。
ちなみに、構築環境はAzure VM上だったのですがその時の環境構築のポイントについて以下の記事にまとめていますので、気になる方は読んでみて頂けると嬉しいです。

きっかけ

元々OSがどのようにハードウェアをコントロールしているかなど、低レイヤの技術に興味があり「30日でできる!OS自作入門」を読んでいました。
しかし、途中で構築を中断してしまいました。当時C言語を書いたことがなく、わからない部分が多かったので読み進めるのに時間がかかっていたことが原因です。
ですので、今回新しく出た「ゼロから始めるOS自作入門」でリベンジしようと思い、購入しました。

全体の感想

ある程度まとまった時間は必要ですが、30章まで構築できた時の達成感はすごいです。

コンテキストの切り替えによる協調的マルチタスクが徐々に組みあがってくるところなど、やっていて勉強になりましたし、楽しかったです。

画像だけだと伝わらないですが、プロセス管理の概念を徐々に実装していきます。
image.png

また、youtubeで著者の方が本と同じ内容を講義されている動画を見つけました。
このYoutubeと本を見ながら進めることでかなり理解の進みが早かったです。

勉強になった部分の紹介

OS周りの技術の基本が理解できた

BIOS、ブートローダ、カーネル、コンパイラ、リンカ、PCIデバイス、割り込み、メモリ管理、ページング、ファイルシステム、システムコールなどについてよく理解できた。

※mikanOSにおいてブートローダとはUEFIに準拠したLoader.efiであり、カーネルとはkernel.elfのことでブートローダから呼び出される。

また、コンピュータの電源をいれたとき、CPUがBIOSに書かれている機械語命令の実行を開始して、BIOSはコンピュータ内部を初期化し、接続されているストレージを探索して、ストレージの中に実行可能ファイルを見つけると、BIOSはそのファイル(OSの中でもブートローダという)をメモリに読みだして展開して…みたいな流れが理解できた。

また、カーネルファイルからメイン関数を探したりしたのが楽しかった。
readelfコマンドからカーネルのヘッダ情報を取得して…

ncbpco130@AZ-foo-pc30:~/mikanos/kernel$ readelf -h kernel.elf 
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:               0x13300c
  Start of program headers:          64 (bytes into file)
  Start of section headers:          4442896 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         5
  Size of section headers:           64 (bytes)
  Number of section headers:         23
  Section header string table index: 21

hexdumpコマンドで見つける!

ncbpco130@AZ-foo-pc30:~/mikanos/kernel$ hexdump -C -s 0x33000 -n 256 kernel.elf
00033000  0f 20 d0 c3 0f 22 df c3  0f 20 d8 c3 48 bc 60 a0  |. ..."... ..H.`.|
00033010  2f 00 00 00 00 00 e8 15  c6 ff ff f4 eb fd 48 89  |/.............H.|
00033020  46 40 48 89 5e 48 48 89  4e 50 48 89 56 58 48 89  |F@H.^HH.NPH.VXH.|
00033030  7e 60 48 89 76 68 48 8d  44 24 08 48 89 46 70 48  |~`H.vhH.D$.H.FpH|
00033040  89 6e 78 4c 89 86 80 00  00 00 4c 89 8e 88 00 00  |.nxL......L.....|
00033050  00 4c 89 96 90 00 00 00  4c 89 9e 98 00 00 00 4c  |.L......L......L|
00033060  89 a6 a0 00 00 00 4c 89  ae a8 00 00 00 4c 89 b6  |......L......L..|
00033070  b0 00 00 00 4c 89 be b8  00 00 00 0f 20 d8 48 89  |....L....... .H.|
00033080  06 48 8b 04 24 48 89 46  08 9c 8f 46 10 66 8c c8  |.H..$H.F...F.f..|
00033090  48 89 46 20 66 8c d3 48  89 5e 28 66 8c e1 48 89  |H.F f..H.^(f..H.|
000330a0  4e 30 66 8c ea 48 89 56  38 0f ae 86 c0 00 00 00  |N0f..H.V8.......|
000330b0  ff 77 28 ff 77 70 ff 77  10 ff 77 20 ff 77 08 0f  |.w(.wp.w..w .w..|
000330c0  ae 8f c0 00 00 00 48 8b  07 0f 22 d8 48 8b 47 30  |......H...".H.G0|
000330d0  8e e0 48 8b 47 38 8e e8  48 8b 47 40 48 8b 5f 48  |..H.G8..H.G@H._H|

「48 bc 60…」がKernelMain()である(どこだかわかりますか?)。

gBSなどのツールキットについて

現代のBIOSはUEFIという仕様に従って作られているUEFI BIOSが多い。
OS側からしても「メモリからメモリマップを取得する」「メモリの値を基にスクリーンに色をつける」などはEDKのライブラリやUEFIのAPIを呼び出して実行している。

メモリマップを取得する例

EFI_STATUS GetMemoryMap(struct MemoryMap* map) {
  if (map->buffer == NULL) {
    return EFI_BUFFER_TOO_SMALL;
  }

  map->map_size = map->buffer_size;
  return gBS->GetMemoryMap(
      &map->map_size,
      (EFI_MEMORY_DESCRIPTOR*)map->buffer,
      &map->map_key,
      &map->descriptor_size,
      &map->descriptor_version);
}

「gBS->GetMemoryMap」がUEFIのAPIをOS側から呼び出している部分。
※gBS(global Boot Services)がUEFIアプリケーションを開発するときの基本的なツールキットである。これにより、アプリケーションはHWと直接やり取りを行うことができる。例えば、メモリの確保や解放、ファイルやディレクトリのアクセス、デバイスI/O、イベントやタイマーなどの機能を使うことができる。

スクリーンに色を塗る例

// #@@range_begin(gop)
  EFI_GRAPHICS_OUTPUT_PROTOCOL* gop;
  OpenGOP(image_handle, &gop);
  Print(L"Resolution: %ux%u, Pixel Format: %s, %u pixels/line\n",
      gop->Mode->Info->HorizontalResolution,
      gop->Mode->Info->VerticalResolution,
      GetPixelFormatUnicode(gop->Mode->Info->PixelFormat),
      gop->Mode->Info->PixelsPerScanLine);
  Print(L"Frame Buffer: 0x%0lx - 0x%0lx, Size: %lu bytes\n",
      gop->Mode->FrameBufferBase,
      gop->Mode->FrameBufferBase + gop->Mode->FrameBufferSize,
      gop->Mode->FrameBufferSize);

  UINT8* frame_buffer = (UINT8*)gop->Mode->FrameBufferBase;
  for (UINTN i = 0; i < gop->Mode->FrameBufferSize; ++i) {
    frame_buffer[i] = 255;
  }
  // #@@range_end(gop)

UEFIのGOP(Graphics Output Protocol)という機能を使うと色を塗るのに必要な情報を取得できる。
VRAMというメモリ領域に全ピクセルの色情報が格納されており、そのメモリ領域の開始アドレスを(UINT8*)gop->Mode->FrameBufferBase;の部分で取得してframe_buffer変数にポインタを格納している。
そして上記では、その変数に255を敷き詰めることで全て白で塗りつぶしている。
動作確認
image.png

なので、OS側から見ても本当のハードの操作の部分は隠ぺいされているのだな、と思った。
学習前はアプリとハードウェアの橋渡しがOSなんだな、くらいにしか思ってなかったが、解像度が上がった。

C、C++の勉強になった

元々C、C++をあまり書いたことがなかったので最初は辛かった。「構造体」や「ポインタ」の意味がわからなかった。
合わせて以下も見ていた。
苦しんで覚えるC言語

マニュアル眺めてみた

UEFIの規格書やIntelのマニュアルも見てみたが、思っていたよりも難しかった。
UEFIの規格書

Intelのマニュアル

躓いたところ

15bのところ

TaskBが一瞬表れてから消えてしまう現象が発生した。

main.cpp

InitializeLayer();
  InitializeMainWindow();
  InitializeTextWindow();
  InitializeTaskBWindow();
  layer_manager->Draw({{0, 0}, ScreenSize()});
  active_layer->Activate(task_b_window_layer_id);

  acpi::Initialize(acpi_table);
  InitializeLAPICTimer();

  const int kTextboxCursorTimer = 1;
  const int kTimer05Sec = static_cast<int>(kTimerFreq * 0.5);
  timer_manager->AddTimer(Timer{kTimer05Sec, kTextboxCursorTimer});
  bool textbox_cursor_visible = false;

  InitializeTask();
  Task& main_task = task_manager->CurrentTask();
  const uint64_t taskb_id = task_manager->NewTask()
    .InitContext(TaskB, 45)
    .Wakeup()
    .ID();

  usb::xhci::Initialize();
  InitializeKeyboard();
  InitializeMouse();

  char str[128];

mouse.cpp

void InitializeMouse() {
  auto mouse_window = std::make_shared<Window>(
      kMouseCursorWidth, kMouseCursorHeight, screen_config.pixel_format);
  mouse_window->SetTransparentColor(kMouseTransparentColor);
  DrawMouseCursor(mouse_window->Writer(), {0, 0});

  auto mouse_layer_id = layer_manager->NewLayer()
    .SetWindow(mouse_window)
    .ID();

  auto mouse = std::make_shared<Mouse>(mouse_layer_id);
  mouse->SetPosition({200, 200});
  layer_manager->UpDown(mouse->LayerID(), std::numeric_limits<int>::max());

  usb::HIDMouseDriver::default_observer =
    [mouse](uint8_t buttons, int8_t displacement_x, int8_t displacement_y) {
      mouse->OnInterrupt(buttons, displacement_x, displacement_y);
    };

  // #@@range_begin(set_mouse_layer)
  active_layer->SetMouseLayer(mouse_layer_id);
  // #@@range_end(set_mouse_layer)

マウスレイヤがセットされるのはInitializeMouse()であるにも関わらず、 InitializeMouse();よりも前に

  active_layer->Activate(task_b_window_layer_id);

をしていることで、Activate()の中でマウスレイヤよりも一段低くTaskBのレイヤを設定しようとするため、マウスレイヤが存在しないことを表す-1からさらにマイナス1され、TaskBの重なり順は-2になり非表示となってしまうことが原因であった。

公式のIssueに上げようと思ったら余裕で既に上がってて悔しかった…。

おわりに

低レイヤって最高だ!

3
2
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
3
2