はじめに
「ゼロから始めるOS自作入門」という本で、0章から30章までやり切り、ゼロからOSを作りました。
その時の感想と躓いたところまとめます。
ちなみに、構築環境はAzure VM上だったのですがその時の環境構築のポイントについて以下の記事にまとめていますので、気になる方は読んでみて頂けると嬉しいです。
きっかけ
元々OSがどのようにハードウェアをコントロールしているかなど、低レイヤの技術に興味があり「30日でできる!OS自作入門」を読んでいました。
しかし、途中で構築を中断してしまいました。当時C言語を書いたことがなく、わからない部分が多かったので読み進めるのに時間がかかっていたことが原因です。
ですので、今回新しく出た「ゼロから始めるOS自作入門」でリベンジしようと思い、購入しました。
全体の感想
ある程度まとまった時間は必要ですが、30章まで構築できた時の達成感はすごいです。
コンテキストの切り替えによる協調的マルチタスクが徐々に組みあがってくるところなど、やっていて勉強になりましたし、楽しかったです。
画像だけだと伝わらないですが、プロセス管理の概念を徐々に実装していきます。
また、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を敷き詰めることで全て白で塗りつぶしている。
動作確認
なので、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に上げようと思ったら余裕で既に上がってて悔しかった…。
おわりに
低レイヤって最高だ!