1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Day 2【ゼロからのOS自作入門】

Posted at

とある事情で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
  • たしかにpqは同じになっている。この140731614838532というのが変換されたあとの整数らしい。
  • というか10進数に変換しただけにみえる。

Screenshot from 2021-06-04 21-38-18.png

  • しかし実際にはpaddrではポインタから変数の型情報を消して変換している
  • addrqでは変数に型情報を加えて変換している

ポインタとアロー演算子

  • 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するとちゃんとハローワールドが出た

参考、使ったリンク

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?