Mac で始める「ゼロからのOS自作入門」

Mac で「ゼロからのOS自作入門」の「8.7 メモリ管理に挑戦」まで進められたので、ひとまずそこまでの手順をメモとして残しておきます。この環境構築方法で本の最後まで進められるかどうかはまだわかりません。

環境は macOS Sonoma(14.5), CPU は Intel でも Apple Silicon でも OK です。Homebrew はすでにインストールされているものとします。

`PATH` にいろいろ追加していきますが、適宜 `.zshrc` 等の `PATH` の設定にも追加してください。

# EDK II の準備

後でインストールする binutils がすでにインストールされ `PATH` に追加されていると失敗するので注意してください。

```sh
$ cd $HOME
$ git clone https://github.com/tianocore/edk2.git
$ cd edk2
$ git checkout edk2-stable202208
$ git submodule update --init
$ cd BaseTools/Source/C
$ make
```

# mikanos-build リポジトリの準備

```sh
$ cd $HOME
$ git clone https://github.com/uchan-nos/mikanos-build.git osbook
$ cd osbook/devenv
$ curl -L https://github.com/uchan-nos/mikanos-build/releases/download/v2.0/x86_64-elf.tar.gz | tar xz
$ cd ..
```

次の内容の mac.patch というファイルを作って適用します。

```diff_shell
--- a/devenv/make_image.sh
+++ b/devenv/make_image.sh
@@ -23,11 +23,24 @@ qemu-img create -f raw $DISK_IMG 200M
 mkfs.fat -n 'MIKAN OS' -s 2 -f 2 -R 32 -F 32 $DISK_IMG
 
 $DEVENV_DIR/mount_image.sh $DISK_IMG $MOUNT_POINT
-sudo mkdir -p $MOUNT_POINT/EFI/BOOT
-sudo cp $EFI_FILE $MOUNT_POINT/EFI/BOOT/BOOTX64.EFI
+if [ `uname` = 'Darwin' ]; then
+    mkdir -p $MOUNT_POINT/EFI/BOOT
+    cp $EFI_FILE $MOUNT_POINT/EFI/BOOT/BOOTX64.EFI
+else
+    sudo mkdir -p $MOUNT_POINT/EFI/BOOT
+    sudo cp $EFI_FILE $MOUNT_POINT/EFI/BOOT/BOOTX64.EFI
+fi
 if [ "$ANOTHER_FILE" != "" ]
 then
-    sudo cp $ANOTHER_FILE $MOUNT_POINT/
+    if [ `uname` = 'Darwin' ]; then
+        cp $ANOTHER_FILE $MOUNT_POINT/
+    else
+        sudo cp $ANOTHER_FILE $MOUNT_POINT/
+    fi
 fi
 sleep 0.5
-sudo umount $MOUNT_POINT
+if [ `uname` = 'Darwin' ]; then
+    hdiutil detach $MOUNT_POINT
+else
+    sudo umount $MOUNT_POINT
+fi
diff --git a/devenv/mount_image.sh b/devenv/mount_image.sh
index ba8233e..aea4d7d 100755
--- a/devenv/mount_image.sh
+++ b/devenv/mount_image.sh
@@ -16,5 +16,9 @@ then
     exit 1
 fi
 
-mkdir -p $MOUNT_POINT
-sudo mount -o loop $DISK_IMG $MOUNT_POINT
+if [ `uname` = 'Darwin' ]; then
+    hdiutil attach -mountpoint $MOUNT_POINT $DISK_IMG
+else
+    mkdir -p $MOUNT_POINT
+    sudo mount -o loop $DISK_IMG $MOUNT_POINT
+fi
```


```sh
$ patch -p1 < mac.patch
```

# QEMU のインストール

Homebrew でインストールします。

```sh
$ brew install qemu
```

# LLVM のインストール

本では LLVM7 を使っていますが、現在（２０２４　年　７　月時点）　mikanos リポジトリでは　LLVM14 を使うようになっていることに合わせて　LLVM14 を使います。

```sh
$ brew install llvm@14

# Intel Mac の場合
$ export PATH=/usr/local/opt/llvm@14/bin:$PATH

# Apple Silicon Mac の場合
$ export PATH=/opt/homebrew/opt/llvm@14/bin:$PATH
```

# その他のビルドに必要なパッケージのインストール

```sh
$ brew install nasm dosfstools binutils

# Intel Mac の場合
$ export PATH=/usr/local/opt/binutils/bin:$PATH

# Apple Silicon Mac の場合
$ export PATH=/opt/homebrew/sbin:/opt/homebrew/opt/binutils/bin:$PATH
```

# 「2.2 EDK II でハローワールド」をビルドする

```sh
$ cd $HOME
$ mkdir workspace
$ cd workspace
$ git clone https://github.com/uchan-nos/mikanos.git
$ cd mikanos
$ git checkout osbook_day02a
$ cd $HOME/edk2
$ ln -s $HOME/workspace/mikanos/MikanLoaderPkg .
$ source edksetup.sh
```

https://github.com/uchan-nos/mikanos/commit/b5f7740c04002e67a95af16a5c6e073b664bf3f5 で行われいるのと同じ変更を `MikanLoaderPkg/MikanLoaderPkg.dsc` に行います。（この変更は　mikanos リポジトリの master ブランチには入っているが先ほどチェックアウトした　osbook_day02a タグには入っていないので手動で変更する必要があります。）

`Conf/target.txt` を開き次のように変更します。`TOOL_CHAIN_TAG` が本とは異なるので注意してください。CLANG38 ツールチェインは Linux でしか使えないため代わりに CLANGPDB ツールチェインを使用します。

| 設定項目 | 設定値 |
|---|---|
| ACTIVE_PLATFORM | MikanLoaderPkg/MikanLoaderPkg.dsc |
| TARGET | DEBUG |
| TARGET_ARCH | X64 |
| TOOL_CHAIN_TAG | CLANGPDB |

変更を保存したらビルドします。

```sh
$ build
```

`$HOME/edk2/Build/MikanLoaderX64/DEBUG_CLANGPDB/X64/Loader.efi` というファイルが作られれば成功です。

# QEMU で実行

次のようにして QEMU を実行します。

```sh
$ $HOME/osbook/devenv/run_qemu.sh $HOME/edk2/Build/MikanLoaderX64/DEBUG_CLANGPDB/X64/Loader.efi
```

うまくいくと QEMU が起動し、さっきビルドしたプログラムが実行されるはずです。

![スクリーンショット 2021-03-23 23.45.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110491/a9a8f5c7-e59d-255d-de72-19878932cde8.png)

# 「3.3 初めてのカーネル」の補足

本とは異なり「LLVMのインストール」で LLVM14 を使うようにしました。LLVM 10 以降では `-z separate-code` というオプションをつけてリンクしないと本書のブートローダでロードすることができません。([「ゼロからのOS自作入門」 03日目](https://www.czu.jp/blog/20210405/) を参考にさせていただきました。ありがとうございます。)

本の

```sh
$ ld.lld $LDFLAGS --entry KernelMain -z norelro --image-base 0x100000 --static -o kernel.elf main.o
```

の代わりに

```sh
$ ld.lld $LDFLAGS --entry=KernelMain -z norelro --image-base 0x100000 --static -o kernel.elf -z separate-code main.o
```

とします。

# 「3.4 ブートローダからピクセルを描く」の補足

Linux では標準のシェルとして bash が使われているのに対し、Mac では zsh が使われています。変数に含まれるスペースの扱いが zsh と bash では異なるため、bash を前提としている本の

```bash
$ clang++ $CPPFLAGS -O2 --target=x86_64-elf -fno-exceptions -ffreestanding -c main.cpp
```

ではコンパイルできません。代わりに

```zsh
$ clang++ ${=CPPFLAGS} -O2 --target=x86_64-elf -fno-exceptions -ffreestanding -c main.cpp
```

としてください。


# 「3.5 カーネルからピクセルを描く」の補足

『「2.2 EDK II でハローワールド」をビルドする』で `TOOL_CHAIN_TAG` に本で指定している CLANG38 ではなく、CLANGPDB を指定しました。CLANG38 ツールチェインは System V AMD64 ABI を使用しますが、CLANGPDB ツールチェインは Microsoft x64 ABI を使用します。(ABI については本の「コラム 4.1 ABI」を参照してださい。）このため System V AMD64 ABI で引数が渡ってくることを想定しているカーネルの `KernelMain` に引数を渡せません。そこで本のリスト 3.8 の

```c
typedef void EntryPointType(UINT64, UINT64);
```

を

```c
typedef void __attribute__((sysv_abi)) EntryPointType(UINT64, UINT64);
```

にすると、強制的に System V AMD64 ABI でカーネルを呼び出すことができ、引数を渡せます。

# 「4.1 make 入門」の補足

Makefile の LDFLAGS の行を変更し、`-z separate-code` をつけてリンクするようにします。

```
LDFLAGS += --entry KernelMain -z norelro --image-base 0x100000 --static -z separate-code
```

# 「5.2 分割コンパイル」の補足

Makefile で sed を使うようになりますが、Linux の sed と Mac の sed はコマンドラインオプションが異なります。sed を使う行は

```
	sed -I '' -e 's|$(notdir $(OBJ))|$(OBJ)|' $@
```

にする必要があります。

また本とは使用するツールチェインが異なるためだと思いますが、 make を実行すると

```
ld.lld: error: undefined symbol: __cxa_pure_virtual
>>> referenced by graphics.cpp
>>>               graphics.o:(vtable for PixelWriter)
make: *** [kernel.elf] Error 1
```

というエラーが出るようになります。完全仮想関数を呼び出そうとしてしまった場合に呼ばれるエラーハンドラの実装を提供する必要があるそうです。([OS 自作に便利な C++ の機能 10 選（前編）](https://uchan.hateblo.jp/entry/2017/11/30/211206))

main.cpp に次のような実装を書いておくとビルドできるようになります。

```cpp
extern "C" void __cxa_pure_virtual() {
  while (1) __asm__("hlt");
}
```