とある事情で06/30までに「ゼロからのOS自作入門」を読み終えたいと思うようになりました。
自律のため毎日勉強したことを投稿しようと思うのでよろしくお願いします。
EDK II入門
- EDK IIはUEFI BIOS自体の開発やその上で動くアプリケーションを開発できる開発キット
- 環境構築が済んでいれば
$HOME/edk2/
にある
EDK IIでハローワールド(osbook_day02a)
$ cd $HOME/workspace/mikanos/
$ git checkout osbook_day02a
$ cd MikanloaderPkg/
$ ls
total 16K
-rw-r--r-- 1 ... 217 Jun 3 09:24 MikanLoaderPkg.dec # パッケージ宣言
-rw-r--r-- 1 ... 1.4K Jun 3 09:26 MikanLoaderPkg.dsc # パッケージ記述
-rw-r--r-- 1 ... 217 Jun 3 09:26 Main.c # ソースコード
-rw-r--r-- 1 ... 498 Jun 3 09:26 Loader.inf # コンポーネント定義ファイル
-
Loader.inf
内のENTRY_POINT
には、起動時に最初に実行される関数を書く - これをエントリポイントという(C/C++の
main()
のようなもの) - EDK IIではUEFIアプリケーションごとに名前を変えられる
-
Main.c
は前回のhello.c
よりスッキリしている(EDK IIが提供しているライブラリを#include <Uefi.h>
でインクルードしているから) - UEFIでは
Print(L"Hello, Mikan World!\n");
のようにワイド文字で文字列を表示する
$ cd $HOME/edk2/
$ ln -s ../workspace/mikanos/MikanLoaderPkg ./ # edk2/にMikanLoaderPkg/を指すシンボリックリンクをはる
$ ls -s
...
lrwxrwxrwx 1 ... 35 Jun 3 11:36 MikanLoaderPkg -> ../workspace/mikanos/MikanLoaderPkg/
$ source edksetup.sh # Conf/target.txtができる
$ # Conf/target.txtを本書に従って編集(第1版だと表に誤植があったので注意)
$ cd ~/edk2/
$ build # edksetup.shを読み込むことで使えるコマンド
ビルドが終わると$HOME/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi
ができたのでこれを実行してみると確かに文字列が表示された。
メインメモリ、メモリマップ
- CPUのメモリコントローラのおかげで、ソフトウェアの観点では多数のバイトが直線的に並んでいるように見える(よく見るメモリの絵)
- メインメモリのどこからどこまでが何に使われているかを表すメモリマップというのがある
- アメリカの番地の付け方のように、1次元的にアドレスと用途が対応付けられている
- メモリ領域の大きさをページで表す。UEFIでは1ページ=4 KiB
- メモリマップには歯抜けの部分(空き地みたいなやつ)があることにも注意。
- メモリを使うならこの空き地を探す必要がある(既存のやつを上書きすると誤動作が生じてしまう)
- ということで、UEFIの機能を使ってメモリマップを取得するプログラムを作る
(ところで)
今までMikanOSのリポジトリはgit clone
して使っていた。
$ mkdir $HOME/workspace
$ cd $HOME/workspace
$ git clone https://github.com/uchan-nos/mikanos.git
ただファイルを変更してタグをgit checkout
しようとするとエラーになる(これは本書にも書いてある)。
自分のリポジトリとしてプッシュしたかったので色々調べたが、どれもいまいちうまく行かなかった。
そこで今までのローカルの変更は破棄して、本家のリポジトリをForkして使うことにした。
方法は作者のuchan-nosさんが丁寧に書いてくれていたのでこれに従った。
$ cd $HOME/workspace
$ rm -rf mikanos # 今までのローカルの変更は破棄する
$ # how-to-send-pull-request.mdを見てforkする
$ git clone git@github.com:<my_user_name>/mikanos.git
$ cd mikanos
$ git remote -v # 自分の名前になっている
$ git log -l # tagも元来のものが見えている
早くに気づいてよかった。Forkするのもはじめてだったので、いい経験ができた。
メモリマップの取得(osbook_day02b)
$ cd $HOME/workspace/mikanos/
$ git checkout -b my-changes
$ git branch # 現在のブランチがmy-changesになっていることを確かめた
$ git checkout osbook_day02b
$ git describe --tags # 現在のタグがosbook_day02bになっていることを確かめた
-
MikanLoaderPkg/Main.c
内のGetMemoryMap()
がメモリマップを取得する -
gBS
はブートサービスのグローバル変数(EDK IIではグローバル変数にg
をつける)
UEFI Specification, Version 2.8の164ページにGetMemoryMap()
の仕様が書いてあった。
typedef EFI_STATUS(EFIAPI * EFI_GET_MEMORY_MAP)(
IN OUT UINTN * MemoryMapSize,
IN OUT EFI_MEMORY_DESCRIPTOR * MemoryMap,
OUT UINTN * MapKey,
OUT UINTN * DescriptorSize,
OUT UINT32 * DescriptorVersion
);
- 関数の説明が難しかったので、先に実際に動かしてみた。
disk.img
をマウントするとたしかに中にmemmap
があり、開くと次のようになっていた。
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
8, 4, EfiBootServicesData, 3BF36000, 20, F
9, 7, EfiConventionalMemory, 3BF56000, 270F, F
10, 1, EfiLoaderCode, 3E665000, 2, F
11, 4, EfiBootServicesData, 3E667000, 217, F
12, 3, EfiBootServicesCode, 3E87E000, B6, F
...
- 難しかったので次のポインタの説に進むことにした
ポインタ入門(1):アドレスとポインタ
int i = 42;
int* p = &i; // ポインタ変数、初期値をiのアドレス(&i)にした
int r1 = *p; // pの指す変数、つまりiの値で初期化
*p = 1; // pの指す変数、つまりiの値を1に変更
int r2 = i; // 1になっている
実際にやってみた。
#include <iostream>
#include <iomanip>
#include <stdint.h>
int main() {
int i = 42;
int* p = &i;
int r1 = *p;
*p = 1;
int r2 = i;
std::cout << "p = " << p << std::endl;
std::cout << "r1 = " << r1 << std::endl;
std::cout << "r2 = " << r2 << std::endl;
return 0;
}
$ ./a.out
p = 0x7ffe61025df4
r1 = 42
r2 = 1
- C++ではポインタと整数は互いに変換できる。
- 異なるポインタを整数に変換したら、その整数同士は同じになることはない
- 逆も然り
#include <iostream>
#include <iomanip>
#include <stdint.h>
int main() {
int i = 42;
int* p = &i;
int r1 = *p;
*p = 1;
int r2 = i;
std::cout << "p = " << p << std::endl;
std::cout << "r1 = " << r1 << std::endl;
std::cout << "r2 = " << r2 << std::endl;
uintptr_t addr = reinterpret_cast<uintptr_t>(p); // convert a pointer into an integer
int* q = reinterpret_cast<int*>(addr); // convert an integer into a pointer
std::cout << "addr = " << addr << std::endl;
std::cout << "q = " << q << std::endl;
return 0;
}
$ ./a.out
p = 0x7ffea1e93f04
r1 = 42
r2 = 1
addr = 140731614838532
q = 0x7ffea1e93f04
- たしかに
p
とq
は同じになっている。この140731614838532というのが変換されたあとの整数らしい。 - というか10進数に変換しただけにみえる。
- しかし実際には
p
→addr
ではポインタから変数の型情報を消して変換している -
addr
→q
では変数に型情報を加えて変換している
ポインタとアロー演算子
- C/C++には構造体とメンバという概念がある
- 構造体の例
struct MemoryMap {
UINTN buffer_size; // offset: 0 Byte
VOID* buffer; // offset: 8 Byte
UINTN map_size;
UINTN map_key;
UINTN descriptor_size;
UINT32 descriptor_version;
};
- メンバは宣言順にメモリに配置される
- 構造体の先頭から各メンバの先頭までの距離をオフセットという
- この例では
sizeof(struct MemoryMap)
= 48 Byte
ここで
struct MemoryMap m;
struct MemoryMap* pm = &m;
とするとm
は48 Byte, pm
は8 Byteの領域を持つ。(ポインタ変数は相手の型によらず一定のバイト数を持つ。あくまでアドレスだから)
pm
からm
の各メンバにアクセスするときにアロー演算子が使われる。
pm->map_size = 0;
(*pm).map_size = 0;
m.map_size = 0;
どれも同じ処理を表す。
ポインタのポインタ
-
memmap_file
は「ファイル情報が書かれるメモリ領域」のアドレスを表すポインタ変数 - さらに
memmap_file
のポインタをptr_ptr
としている -
ptr_ptr
が使われているのは、memmap_file
が指しているアドレスを変更するため
写経
メモリマップを取得するコードが理解できなかったので、写経して読み解いてみることにした。
理解は後回しで、なんとなくわかればいいのかな…
感想
- 「メモリマップの取得」とところからいきなり難しくなったなぁ
- 第1版は誤字が多いので正誤表は要確認
- Raspberry Piで起動させてみようと思ったがだめらしい
Raspberry Pi でも動作する?
いいえ、できません。 本書で作る OS は x86-64 アーキテクチャ専用の OS です。 x86-64 を採用した Intel Core シリーズや AMD Ryzen などの CPU が搭載されたパソコンが必要です。Raspberry Pi は ARM という CPU が搭載されており、MikanOS は動作しません。
実験
自分のOSを作ってみるということで、$HOME/test/myOS/
配下に$HOME/workspace/mikanos/MikanLoaderPkg/*
をコピーし、Main.c
を次のように変更した。
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/PrintLib.h>
#include <Protocol/LoadedImage.h>
#include <Protocol/SimpleFileSystem.h>
#include <Protocol/DiskIo2.h>
#include <Protocol/BlockIo.h>
EFI_STATUS EFIAPI UefiMain(){
Print(L"Hello World!");
while (1);
return EFI_SUCCESS;
}
使わないライブラリがインクルードされているのは目をつぶっておいてください。
-
MikanLoader.dec
などの名前もmyOS.dec
などにした。 - またそれらの中に書いてある
MikanLoader
という言葉をmyOS
にした -
~/edk2/
にシンボリックリンクをはった -
source edk2setup.sh
をして、Conf/target.txt
を変更した
-- ACTIVE_PLATFORM = MikanLoaderPkg/MikanLoaderPkg.dsc
++ ACTIVE_PLATFORM = myOS/myOS.dsc
-
build
するとBuild/
にmyOSのものができていたので、run_qemu.sh
するとちゃんとハローワールドが出た
参考、使ったリンク
-
MikanOS ソースコード:https://github.com/uchan-nos/mikanos
-
MikanOS 開発環境:https://github.com/uchan-nos/mikanos-build
-
osdev-jp:https://osdev.jp/
-
著者(uchan)のTwitter:@uchan_nos
-
- SlackやDiscordでサポート、勉強会(2021年3月27日から)を行っているらしい! 途中からかもだけど参加してみよっと。