ASUS X205TA 実機で UEFI アプリによる "Hello, World!" in Rust

# 動機

みかん本（通称：ゼロからのOS自作入門）を参考にしながら Rust でぼちぼちと自作 OS に取り組んでいます。QEMU でしか動かしてなかったので、実機で動かしたいなと思い立ち、大昔に買った ASUS X205TA というノート PC を引っ張り出してきました。X205TA は CPU が Intel ATOM Z3735F、メモリ 2G、eMMC 32GB という鬼スペックの PC です。しかもこいつは CPU アーキテクチャは x64 なのに、UEFI は 32bit という変わりもの。まあ、この PC なら文鎮になっても構わないので、実験台になってもらうことにしました。

# 開発機

Linux penguin on Chromebook x86_64（これも ASUS 製 PC。ASUS 好きですね。）

# Rust の設定

### toolchain

nightly を使います。rustc 1.67.0-nightly (e1d819583 2022-12-05)

### rust-analyzer

VSCode + rust-analyzer で開発しています。下記設定を追加して`#![no_std]`でエラーが出ないようにします。

```json
"rust-analyzer.checkOnSave.allTargets": false
```

# 実装

kernel をロードするところはまだ未実装ですが、そのうち実装すると思うので`loader`という名前でクレートを作ります。

```bash
cargo new loader
```

### loader/.cargo/config.toml

core クレートのためのおまじないと、target の指定です。

```toml
[unstable]
build-std = ["core"]
build-std-features = ["compiler-builtins-mem"]

[build]
target = "i686-unknown-uefi"
```

### loader/src/uefi.rs

[uefi-rs](https://crates.io/crates/uefi) というクレートがありますが、[UEFI Spec](https://uefi.org/specs/UEFI/2.10/index.html) 見ながら自分でゴリゴリと書いていきます。やっぱ自分で書きたいですよね。何回かパディングサイズ間違いました（あるある）。短いので全部載せます。

```rust
use core::fmt;

pub struct EfiStatus(pub usize);

impl EfiStatus {
    pub fn is_success(&self) -> bool {
        self.0 == 0
    }
}

#[repr(C)]
pub struct EfiSystemTable<'a> {
    header: [u8; 24],
    pub firmware_vendor: *const u16,
    _padding0: [u8; 16],
    pub con_out: &'a EfiSimpleTextOutputProtocol,
}

#[repr(C)]
pub struct EfiSimpleTextOutputProtocol {
    _padding0: [u8; 4],
    output_string: fn(&EfiSimpleTextOutputProtocol, *const u16) -> EfiStatus,
    _padding1: [u8; 16],
    clear_screen: fn(&EfiSimpleTextOutputProtocol) -> EfiStatus,
}

impl EfiSimpleTextOutputProtocol {
    fn output_string(&self, string: *const u16) -> EfiStatus {
        (self.output_string)(self, string)
    }

    pub fn clear_screen(&self) -> EfiStatus {
        (self.clear_screen)(self)
    }

    fn write_char(&self, c: u8) {
        let buf = [c as u16, 0];
        self.output_string(buf.as_ptr());
    }
}

pub struct TextWriter<'a>(&'a EfiSimpleTextOutputProtocol);

impl<'a> TextWriter<'a> {
    pub fn new(stop: &'a EfiSimpleTextOutputProtocol) -> Self {
        Self(stop)
    }
}

impl<'a> fmt::Write for TextWriter<'a> {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        s.bytes().for_each(|c| {
            if c.eq(&b'\n') {
                self.0.write_char(b'\r');
            }
            self.0.write_char(c);
        });
        Ok(())
    }
}
```

### loader/src/main.rs

`core::fmt::Write`を実装するだけで`writeln!`マクロが使えるのが便利すぎますね。

```rust
#![no_std]
#![no_main]
#![feature(abi_efiapi)]

use core::arch::asm;
use core::fmt::Write;
use core::panic::PanicInfo;

mod uefi;
use uefi::*;

#[no_mangle]
pub extern "efiapi" fn efi_main(_image: *const u8, system_table: &EfiSystemTable) -> ! {
    system_table.con_out.clear_screen();
    let mut writer = TextWriter::new(system_table.con_out);

    writeln!(&mut writer, "Hello, World!").unwrap();

    loop {
        unsafe { asm!("hlt") };
    }
}

#[panic_handler]
fn panic_impl(_info: &PanicInfo) -> ! {
    loop {
        unsafe { asm!("hlt") };
    }
}
```

# 結果

`cargo build --release`して出来上がった`target/i686-unknown-uefi/release/loader.efi`を USB メモリの`/efi/boot/bootia32.efi`にコピーします。その USB を X205TA に差し込んで起動！（BIOS で USB から起動するように設定変更しておくこと）

...

![IMG_1121_.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/80d55711-fad5-6c05-c428-ea854859fc0a.jpeg)

やったぜ:relaxed:

# QEMU

さすがに X205TA 実機でデバッグするのは大変なので、開発機の QEMU でも動くように環境を整えます。私の環境の Chromebook Linux だと`mount /mnt`でエラーが出るので`mtools`を導入しました。

### loader/run-qemu.sh

```sh
#!/bin/bash -ex

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

mmd -i disk.img ::/EFI
mmd -i disk.img ::/EFI/BOOT
mcopy -i disk.img ./target/i686-unknown-uefi/release/loader.efi ::/EFI/BOOT/BOOTIA32.EFI

qemu-system-i386 \
  -m 1G \
  -drive if=pflash,format=raw,readonly=on,file=./ovmf/OVMF_CODE.fd \
  -drive if=pflash,format=raw,file=./ovmf/OVMF_VARS.fd \
  -drive if=ide,index=0,media=disk,format=raw,file=disk.img \
  -monitor stdio
```

### OVMF

OVMF を 32bit 用に自分でビルドします。[mikanos-build](https://github.com/uchan-nos/mikanos-build#mikanos-build) の手順に従っておれば、ビルド環境は整っているはずです。下記でビルドが始まります。

```sh
cd ~/edk2/OvmfPkg
./build.sh -a IA32 -b RELEASE -n 4
```

`~/edk2/Build/OvmfIa32/RELEASE_GCC5/FV/`に`OVMF_CODE.fd`と`OVMF_VARS.fd`が出来ておれば成功です。これを QEMU に読み込ませれば OK です。

今年中に USB マウス動かすところぐらいまでは進めたいな。
おわり。