ゼロからのOS自作入門 作業記録 #
内田公太氏の「ゼロからのOS自作入門」でのOS作成作業を記録として記す。
2/28 第1章 #
開発環境の構築
自作OSの開発に利用した環境
- ThinkCentre M720s Small
- Ubuntu 20.04 Focal Fossa
動作確認は、仮想マシンのqemuを使用する。 BOOTX64.EFIというファイルにバイナリを打ち込んで、起動する。gitに完成済みのEFIファイルがあるが、初めての作業なのでoctetaで一つずつ16進数を打ち込むことにした。 qemuで「Hello, workd」を表示できた。
C言語で同じ動作をするEFIファイルを作成する。 Cプログラム→(コンパイラ clang)→オブジェクトファイル→ (リンカ lld-link) → EFIファイル(hello.efi)
3/1 第2章 #
EDKⅡを利用してアプリケーション開発ができるようにする。 まずはEDKでのハローワールドから始める。
EDKⅡ:UEFI BIOS上で動作するアプリケーション開発キット
2章を読んだのみで実装は進まなかった。
3/4 第2章 #
EDKでハローワールド KosLoaderPkg/以下にLoad.inf, Main.c, KosLoaderPkg.dec, KosLoaderPkg.decの4ファイルを作成し、EDKを用いてビルドを行う →Loader.efiが生成されるため、これをBOOTX64.EFIとして保存する。(run_qemu.shで実行できる)
実行結果:edkでハローワールド(めっちゃ誤字している)
今後このアプリケーションをブートローダとして拡張していく
まずはメモリマップの取得を行う。
edkで用意されている機能gBS->GetMemoryMap
を使用する。
EFI_STATUS GetMemoryMap(
IN OUT UINTN *MemoryMapSize,
IN OUT EFI_MEMORY_DSCRIPTOR *Memorymap,
OUT UINTN *MapKey,
OUT UINTN *DescriptorSize,
OUT UINT32 *DescriptorVersion
);
- MemoryMapSize:MemoryMapのバッファサイズ(出力は実際のメモリサイズ)
- MemoryMap: メモリマップ書き込み先のメモリ領域の先頭ポインタ
- MapKey:メモリマップを識別するための値を書き込む変数を指定する
- DescriptorSize: メモリマップの個々の行を表すメモリディスクリプタのバイト数
- DescriptorVersion: メモリディスクリプタ構造体のバージョン番号(使用しない)
メモリマップの読み込み成功。下図はメモリディスクリプタの各要素csvファイルとして出力している。
Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute
0, 3, EfiBootServicesCode, 00000000, 1, F
1, 7, EfiConventionalMemory, 00001000, 9F, F
2, 7, EfiConventionalMemory, 00100000, 700, F
3, A, EfiACPIMemoryNVS, 00800000, 8, F
4, 7, EfiConventionalMemory, 00808000, 8, F
5, A, EfiACPIMemoryNVS, 00810000, F0, F
6, 4, EfiBootServicesData, 00900000, B00, F
7, 7, EfiConventionalMemory, 01400000, 3AB36, F
...
第二章おわり!
3/5 第3章 #
第3章レジスタ
QEMUモニタの使い方(GDBと同じか?) wikibooks
info registers
x/2i 0x067ae4c4
初めてのkernel作成
ブートローダとkernelは別ファイルとして開発する
カーネルのコンパイル時にエラーが出た
# kernel/main.cpp のコンパイル
~/kos/kernel$ clang++ -O2 -Wall --target=X86_64-elf -ffreestanding -mno-red-zone -fno-exceptions -fno-rtti -std=c++17 -c main.cpp
clang: warning: argument unused during compilation: '-mno-red-zone' [-Wunused-command-line-argument]
error: unknown target triple 'X86_64---elf', please use -triple or -arch
--target
フラグなしでコンパイルを実行して解決- コンパイラがmain.oを生成する
- 次にリンカを実行して、main.oから実行可能ファイル(elf)を作成する
~/kos/kernel$ ld.lld --entry KernelMain -z norelro --image-base 0x100000 --static -o kernel.elf main.o
main.cpp -> (clang++でコンパイル) -> main.o -> (ld.lldでリンク) -> kernel.elf
- 次に、ブートローダを拡張してカーネルファイルを読み込む機能を追加する
- 謎のオフセット24バイト(下記コード)
- → ELFの仕様で、64bit用のEFLのエントリポイントアドレスは、オフセット24バイトの位置から8バイト整数で書かれている。
// Boot kernel
UINT64 entry_addr = *(UINT64*)(kernel_base_addr + 24);
typedef void EntryPointType(void);
EntryPointType* entry_point = (EntryPointType*)entry_addr;
entry_point();
kernelのロード+起動成功
画面の色をいじっていく
まずはブートローダでピクセルを描く(UEFIのGOP機能)
openProtocol関数でgopを取得する
gBS->OpenProtocol(
gop_handles[0],
&gEfiGraphicsOutputProtocolGuid,
(VOID**)gop,
image_handle,
NULL,
EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL
);
UEFIでピクセル色指定をした結果
- 次にカーネルでピクセルを描く+エラー処理
- gBSの各関数の戻り値(EFI_STATUS型)をチェックしてエラーの場合には、メッセージ表示+hltを行う。
3/8 第4章 #
第4章 make入門
- kernelのコード (main.cpp) にピクセルを描画する処理を追加する。
- なぜか黄色が表示される。。。
- 原因はフレームバッファにred要素を書いていなかったことだった。
p[2] = c.r
の記述がない。
// main.cpp WritePixel関数内
} else if (config.pixel_format == kPixelBGRResv8BitPerColor) {
uint8_t* p = &config.frame_buffer[4 * pixel_position];
p[0] = c.b;
p[1] = c.g;
} else {
これでピクセルの描画を楽に記述できるようになった。
カーネルローダの改良
- カーネルの読み込み処理で、メモリ上に確保するメモリサイズを計算する処理が実は間違っているので修正する必要がある。
- kernel.elfの情報 (ELFプログラムヘッダのLOAD部分) を見て、正しいサイズのメモリを確保するように修正する。
上記について書き換えたが、なぜか以下のエラーが出てkernelが実行されない。
failed to allocate pages: Not Found
- 原因:上記は取得しようとしているページ数が大きすぎたことで発生したエラー
- num_pagesの計算で、カーネルのサイズに
0xfff
を乗算しているのがミス(以下のコード部分) - 本来は、カーネルのサイズに
0xfff
を加算する - ちなみに、この部分はカーネルをメモリ上に配置するために、カーネルサイズをページ単位(0x1000)で計算している。0xfffを加算する理由は、カーネルサイズを0x1000で割った際に必要なページ数を正しく計算するため。つまり以下を計算するためである。
カーネルの配置に必要なページ数 = ceil(カーネルサイズ(bytes) / 0x1000)
- num_pagesの計算で、カーネルのサイズに
// 誤→"* 0xfff"
// 正→"+ 0xfff"
//
UINTN num_pages = (kernel_last_addr - kernel_first_addr * 0xfff) / 0x1000;
status = gBS->AllocatePages(
AllocateAddress,
EfiLoaderData,
num_pages,
&kernel_first_addr
);
3/9 第5章 #
第5章 文字表示とコンソールクラス
ピクセルの描画はできるようになっているので、それを用いて文字を描画する
参照とポインタの使い分け
- ポインタ:C言語からある機能。
nullptr
とか書くだけで簡単にNULLポインタを作成できる。 - 参照:C++で追加された機能。NULL参照が作りにくい。引数で参照を指定することでNULLではないものを渡してほしいという意思表示として使える。
- ポインタ:C言語からある機能。
フォントのファイルをカーネルファイルに組み込む
ELFなどではないバイナリも、実行可能ファイルにリンクしてプログラムから変数として見えるようにすることができる。これは知らなかったためかなり勉強になった。
// hankaku.bin(フラットバイナリ)→hankaku.o(ELFオブジェクトファイル)
$ objcopy -I binary -O elf64-x86-64 -B i386:x86-64 hankaku.bin hankaku.o
$ file hankaku.o
hankaku.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
ASCII文字列を一通り利用できるようになった。
文字の折返し、スクロールなどを行うconsoleクラスの実装 完了
3/11 第6章 #
第6章 マウス入力とPCIe
- UIのアップデート。背景に色をつけ、画面下にタスクバーを模した長方形を描画することでそれらしい見た目になった。
- ホストマシン (ubuntu) を再起動すると、環境変数がリセットされたことでkernelをビルドするためのmakeコマンドが失敗した。
- 起動時に環境変数が自動で設定されるように、$HOME/.bashrcに下記を追記する
# .bashrc
source $HOME/osbook/devenv/buildenv.sh
USBホストドライバを実装する
USB (Universal Serial Bus) ←地味に知らなかった
ドライバの実装がどんなのものなのか知らなかったので、何よりもこの章が楽しみだった。
→ドライバの実装には解説はないらしい。残念。
PCIデバイスの読み取り
IOアドレス空間:メモリアドレス空間とは別のアドレス空間。周辺機器用のアドレス空間。
PCIコンフィグレーション空間(周辺機器にある)にアクセスするためにIOアドレス空間を利用する。
ScanAllBus関数では、PCIデバイスをすべて探索している
bus=0, device=0, function=0から検索する
- device: 1つのバスに最大32個まで
- function: 1つのデバイスに最大8個まで
なぜfunctionで探索している?
PCIファンクションがPCI-to-PCIブリッジ(2つのPCIバスをつなぐブリッジ)である場合、ブリッジの下流側のバスに対して、PCIデバイスを探索する。
PCI-to-PCIブリッジは、PCIデバイスの最大接続数を増やすために利用される。
Error ScanAllBus() {
...
for (uint8_t function = 1; function < 8; ++function) {
if (ReadVendorId(0, 0, function) == 0xffffu) {
continue;
}
// ScanBusの引数はbus番号のはずだが、function番号として1-8を引数に入れている。なぜ?
if (auto err = ScanBus(function)) {
return err;
}
}
return Error::kSuccess;
}
- PCIデバイスの探索+表示に成功
ポーリングでマウス入力を読み取る
6.4節のコードをビルドする際にエラーが発生した。
エラーの要点と思われる箇所はこちら:
ld.lld: error: cundefined symbol: _exit
$ make
ld.lld -L/home/user/osbook/devenv/x86_64-elf/lib --entry KernelMain -z norelro --image-base 0x100000 --static -o kernel.elf main.o graphics.o mouse.o font.o hankaku.o newlib_support.o console.o pci.o asmfunc.o libcxx_support.o logger.o usb/memory.o usb/device.o usb/xhci/ring.o usb/xhci/trb.o usb/xhci/xhci.o usb/xhci/port.o usb/xhci/device.o usb/xhci/devmgr.o usb/xhci/registers.o usb/classdriver/base.o usb/classdriver/hid.o usb/classdriver/keyboard.o usb/classdriver/mouse.o -lc -lc++
ld.lld: error: cundefined symbol: _exit
>>> referenced by abort.c
>>> lib_a-abort.o:(abort) in archive /home/user/osbook/devenv/x86_64-elf/lib/libc.a
ld.lld: error: undefined symbol: kill
>>> referenced by signalr.c
>>> lib_a-signalr.o:(_kill_r) in archive /home/user/osbook/devenv/x86_64-elf/lib/libc.a
ld.lld: error: undefined symbol: getpid
>>> referenced by signalr.c
>>> lib_a-signalr.o:(_getpid_r) in archive /home/user/osbook/devenv/x86_64-elf/lib/libc.a
make: *** [Makefile:25: kernel.elf] Error 1
newlib_support.cにライブラリ関数を追加するのを忘れていた。みかん本に挑戦している別の方の ページの記述から気づくことができた。
カーソルを動かすことに成功した。デバッグにかなり長い時間(4時間ほど)かかってしまった。。
3/18 第7章 #
第7章 割り込みハンドラ
- 特殊なアドレス空間
- アドレス空間の中には、メインメモリに配置されている領域だけではなく、CPUレジスタに領域が配置されている範囲もある。
- 0xfee00000から0xfee00400までのアドレス (1024バイト) は、メインメモリではなくCPUのレジスタに配置されている。
- 特に0xfee000b0番地への書き込みを行うことで、割り込み処理の終了をCPUに伝えることができる。
- C++の記法
__attribute__((interrupt))
修飾子:割り込みハンドラであることを伝える。これがついている関数には、コンパイル時にコンテキストの保存と復帰処理が挿入される。__attribute__((packed))
修飾子:コンパイラは本来自動で変数のアラインメントを行うが、これがついている変数に対しては、変数のアラインメントを行わない。reinterpret_cast<型>
:ポインタ型もしくは整数型(int, longなど)を、任意の型のポインタに変換する。 参考あくまでコンパイラに型情報を伝えるためなので、生成されるコードは普通変わらない。
union InterruptDescriptorAttribute {
// このdata変数は、bits変数のサイズ(16bit)をコンパイラに伝えるためだけに宣言している
// 実際には使用しない
uint16_t data;
struct {
uint16_t interrupt_stack_table : 3;
uint16_t : 5;
DescriptorType type : 4;
uint16_t : 1;
uint16_t descriptor_privilege_level : 2;
uint16_t present : 1;
} __attribute__((packed)) bits; // 16bitのメモリ領域を[3bit, 5bit, 4bit, 1bit, 2bit, 1bit]に分けてアクセスすることができる
} __attribute__((packed)); // アラインメントを行わない