M1 Mac 上で aarch64 向けの Rust 製 MikanOS をつくりたい
ちなみに作業リポジトリはここ.
まずはディスクイメージの作成.
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
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
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
次は Rust ツールチェーンの設定.
とりあえず nightly にしないといろいろできないので仕方なく.
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy"]
ビルド設定は uefi-rs にあるものをパクればよい.
[build]
target = "aarch64-unknown-uefi"
[unstable]
build-std = ["core", "compiler_builtins", "alloc"]
build-std-features = ["compiler-builtins-mem"]
問題はカーネルをビルドするときの ELF ターゲットなのだが,これは現状の rustc では aarch64 に対応していないようなので以下のような Custom Target を書いた.全然詰められていないのでいろいろ検討したほうがよいかもしれない.
{
"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
[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"]
ビルド・実行環境はこれで一通り整った.
あとは実際に Rust で MikanOS を実装していくなかで困った・時間のかかったポイントを書いていく.
とりあえず 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(())
};
}
uefi-rs のいくつかの関数はバッファの作成を要求してくるので,こんな感じに作成する.
Vec を使わなくても良い気もするがあとあとバッファにアクセスしたときには便利そうなので.
サイズは最初は決め打ちでいいと思うが丁寧にやるなら EFI のエラーコードベースでリサイズしてリトライするみたいな機構を作りたい.
let mut buf = Vec::<u8>::with_capacity(size);
#[allow(clippy::uninit_vec)]
unsafe {
buf.set_len(buf.capacity());
}
またいくつかの関数は &mut [MaybeUninit<Handle>]
みたいな型で要求してくるので,こんな感じにつくる.
core::iter::repeat_with(MaybeUninit::<Handle>::uninit)
.take(size)
.collect::<Vec<_>>()
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)
}
}
ELF のエントリポイントのアドレスがどこになるかは割と不定なので, elf_rs
クレートをつかって読んでおく.わざわざ全部読まなくても ELF ファイル内の 0x18
から usize
で読んであげれば問題ない (ただし環境によってエンディアンが変わることがあるっぽい)
Elf::from_bytes(buf)
.unwrap()
.entry_point() as usize
小ネタ.
x86_64 における hlt 命令は aarch64 では wfi なので注意.
ARM と X86 の命令セット違い過ぎてビビる.
私は面倒なので aarch64
クレートを使った.
loop {
aarch64::instructions::halt();
}