Open14

M1 Mac 上で aarch64 向けの Rust 製 MikanOS をつくりたい

Naoki IkeguchiNaoki Ikeguchi

まずはディスクイメージの作成.
newfs_msdos ではイメージファイルの操作ができなさそうだったので諦めて dosfsutils を入れた.

brew install dosfsutils

mkfs までは本の通り.

qemu-img create -f raw ./disk.img 200M
mkfs.fat -n 'MIKAN' -s 2 -f 2 -R 32 -F 32 ./disk.img

マウント・アンマウントは以下.

hdiutil mount -mountpoint ./mnt ./disk.img
umount ./mnt
Naoki IkeguchiNaoki Ikeguchi

QEMU で EFI を使うために OVMF というファームウェアがよく使われるが,この aarch64 バージョンが必要.セルフビルドを試したが macOS ツールチェーン + aarch64 はターゲットリストになかったので諦めた.Debian のパッケージから拝借することにした.

curl -sSL http://ftp.jp.debian.org/debian/pool/main/e/edk2/qemu-efi-aarch64_2022.05-2_all.deb > source.deb
ar -x ./source.deb
tar -xf ./source.deb
cp ./usr/share/AAVMF/AAVMF_CODE.fd ./AAVMF_CODE.fd
cp ./usr/share/AAVMF/AAVMF_VARS.fd ./AAVMF_VARS.fd
cp ./usr/share/qemu-efi-aarch64/QEMU_EFI.fd ./QEMU_EFI.fd
Naoki IkeguchiNaoki Ikeguchi

QEMU の起動オプションはいろいろ試した結果以下に落ち着いた.特に -device ramfb がミソで,よく情報が出てくる -device vga やら -vga std やらは aarch64 向けに QEMU が対応していないし, -device virtio-gpu だと FrameBuffer が使えなかった.

qemu-system-aarch64 \
    -machine virt \
    -cpu cortex-a57 \
    -m 512 \
    -bios ./aavmf/QEMU_EFI.fd \
    -drive 'if=virtio,file=./disk.img,format=raw' \
    -device ramfb \
    -monitor stdio
Naoki IkeguchiNaoki Ikeguchi

次は Rust ツールチェーンの設定.
とりあえず nightly にしないといろいろできないので仕方なく.

rust-toolchain.toml
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy"]

ビルド設定は uefi-rs にあるものをパクればよい.

efi/.cargo/config.toml
[build]
target = "aarch64-unknown-uefi"

[unstable]
build-std = ["core", "compiler_builtins", "alloc"]
build-std-features = ["compiler-builtins-mem"]
Naoki IkeguchiNaoki Ikeguchi

問題はカーネルをビルドするときの ELF ターゲットなのだが,これは現状の rustc では aarch64 に対応していないようなので以下のような Custom Target を書いた.全然詰められていないのでいろいろ検討したほうがよいかもしれない.

aarch64-unknown-elf.json
{
  "abi-return-struct-as-int": true,
  "allows-weak-linkage": false,
  "arch": "aarch64",
  "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128",
  "disable-redzone": true,
  "emit-debug-gdb-scripts": false,
  "exe-suffix": ".elf",
  "executables": true,
  "is-builtin": false,
  "is-like-msvc": false,
  "is-like-windows": false,
  "linker": "ld.lld",
  "linker-flavor": "ld",
  "linker-is-gnu": true,
  "llvm-target": "aarch64-elf",
  "max-atomic-width": 64,
  "os": "none",
  "panic-strategy": "abort",
  "pre-link-args": {
    "ld": [
      "--entry=kernel_main",
      "--image-base=0x40000000",
      "--static",
      "-z",
      "norelro"
    ]
  },
  "singlethread": true,
  "split-debuginfo": "packed",
  "stack-probes": {
    "kind": "call"
  },
  "target-pointer-width": "64"
}

あとはこれを Cargo に食わせればよいのだが, macOS 標準の ld (llvm-ld) は必要なコンパイラオプションに対応していなかった. GCC を入れてもよいのだがなんか嫌なので mold を入れた.

brew install mold
kernel/.cargo/config.toml
[build]
target = "../aarch64-unknown-elf.json"

[target.aarch64-unknown-elf]
linker = "mold"

[unstable]
build-std = ["core", "compiler_builtins", "alloc"]
build-std-features = ["compiler-builtins-mem"]
Naoki IkeguchiNaoki Ikeguchi

ビルド・実行環境はこれで一通り整った.
あとは実際に Rust で MikanOS を実装していくなかで困った・時間のかかったポイントを書いていく.

Naoki IkeguchiNaoki Ikeguchi

とりあえず println くらい使いたいので適当にマクロにしておいた.

macro_rules! println {
    ($($t: tt)*) => {
        writeln!(unsafe { uefi_services::system_table().as_mut() }.stdout(), $($t)*)
            .map_err(err!())
    };
}

macro_rules! eprintln {
    ($($t: tt)*) => {
        writeln!(unsafe { uefi_services::system_table().as_mut() }.stderr(), $($t)*)
            .unwrap_or(())
    };
}
Naoki IkeguchiNaoki Ikeguchi

uefi-rs のいくつかの関数はバッファの作成を要求してくるので,こんな感じに作成する.
Vec を使わなくても良い気もするがあとあとバッファにアクセスしたときには便利そうなので.
サイズは最初は決め打ちでいいと思うが丁寧にやるなら EFI のエラーコードベースでリサイズしてリトライするみたいな機構を作りたい.

let mut buf = Vec::<u8>::with_capacity(size);

#[allow(clippy::uninit_vec)]
unsafe {
    buf.set_len(buf.capacity());
}
Naoki IkeguchiNaoki Ikeguchi

またいくつかの関数は &mut [MaybeUninit<Handle>] みたいな型で要求してくるので,こんな感じにつくる.

core::iter::repeat_with(MaybeUninit::<Handle>::uninit)
        .take(size)
        .collect::<Vec<_>>()
Naoki IkeguchiNaoki Ikeguchi

uefi-rs には RegularFile という型があってこれ経由で SimpleFileSystem プロトコル上のファイルにアクセスできるが,これが Write すら実装していなくて writeln! が使えなかったのでラップした.これは Pull Request してもいい気がする

struct WrappedFile {
    file: RegularFile,
}

impl WrappedFile {
    fn close(self) {
        self.file.close()
    }
}

impl From<RegularFile> for WrappedFile {
    fn from(file: RegularFile) -> Self {
        Self { file }
    }
}

impl Write for WrappedFile {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        self.file.write(s.as_bytes()).map_err(|_| core::fmt::Error)
    }
}
Naoki IkeguchiNaoki Ikeguchi

ELF のエントリポイントのアドレスがどこになるかは割と不定なので, elf_rs クレートをつかって読んでおく.わざわざ全部読まなくても ELF ファイル内の 0x18 から usize で読んであげれば問題ない (ただし環境によってエンディアンが変わることがあるっぽい)

Elf::from_bytes(buf)
    .unwrap()
    .entry_point() as usize
Naoki IkeguchiNaoki Ikeguchi

小ネタ.
x86_64 における hlt 命令は aarch64 では wfi なので注意.
ARM と X86 の命令セット違い過ぎてビビる.
私は面倒なので aarch64 クレートを使った.

loop {
    aarch64::instructions::halt();
}