🍊

Rustでターゲットを自作してみかん本3.3(day03a)の内容をやる

2022/01/02に公開

はじめに

https://book.mynavi.jp/ec/products/detail/id=121220
ゼロからの自作OS入門(みかん本)の「3.3 初めてのカーネル(osbook_day03a)」をRustで行う場合、書籍内ではコンパイラやリンカに渡しているオプションをどのようにRustで再現すれば良いかというところで少し詰まることがありました(もちろん、UEFIアプリケーションのビルドも困った)。
そもそもRustもClangも基盤はLLVMなのでClangでやっているようなことは基本的にRustでも可能なはずであると考えて調べたところ、The Embedonomiconで紹介されているようなターゲットを自作するしっくりきたので記載します。

環境

> wsl --list --running
Linux 用 Windows サブシステム ディストリビューション:
Ubuntu-20.04 (既定)
$ rustup --version
rustup 1.24.3 (ce5817a94 2021-05-31)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.59.0-nightly (c09a9529c 2021-12-23)`
$ cargo --version
cargo 1.59.0-nightly (fcef61230 2021-12-17)

rustcのunstable-optionsを使用することや、no_stdの環境なのでnightlyを使います。

ターゲット定義用のJSONファイル

./x86_64-unknown-elf.json
{
    "abi-return-struct-as-int": true,
    "allows-weak-linkage": false,
    "arch": "x86_64",
    "cpu": "x86-64",
    "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,
    "features": "-mmx,-sse,+soft-float",
    "is-builtin": false,
    "is-like-msvc": false,
    "is-like-windows": false,
    "linker": "ld.lld",
    "linker-flavor": "ld",
    "linker-is-gnu": true,
    "llvm-target": "x86_64-elf",
    "max-atomic-width": 64,
    "os": "none",
    "panic-strategy": "abort",
    "pre-link-args": {
        "ld": [
            "--entry=kernel_main",
            "--image-base=0x100000",
            "--static",
            "-z",
            "norelro"
        ]
    },
    "singlethread": true,
    "split-debuginfo": "packed",
    "stack-probes": {
        "kind": "call"
    },
    "target-pointer-width": "64"
}

"exe-suffix": ".elf"で出力ファイルの拡張子を.elfに指定できます。不要な定義も含まれているかもしれません。
それぞれの値はrustc_target::specTargetTargetOption構造体に定義があり、その詳細に関してはLLVMやClangのドキュメントを参照すると良いでしょう。

対応するコンパイラオプション

  • --targetx86_64-elf = "llvm-target": "x86_64-elf"
    Cross-compilation using Clang -- Clang 13 documentationのTarget Triplesにあるように、<arch><sub>-<vendor>-<sys>-<abi>のフォーマットで表されるものを指定する。RustのTargetは前述のJSONのような設定を参照しているだけなので注意。
    x86_64-unknown-uefiもLLVMのターゲットはx86_64-unknown-windowsで、みかん本のx86_64-pc-win32-coffではないです。結局PEフォーマットだからリンカのオプションさえ/subsystem:efi_applicationであれば大丈夫?

  • -fno-exception = "panic-strategy": "abort"
    パニック時、スタックの巻き戻しを行わない?

  • -mno-red-zone = "disable-redzone" : true
    みかん本コラム3.1を参照。

リンカオプション

"pre-link-args": {
        "ld": [
            "--entry=kernel_main",
            "--image-base=0x100000",
            "--static",
            "-z",
            "norelro"
        ]
    },

"-z norelro"とは書けないので注意。

config.toml

./.cargo/config.toml
[build]
target = "./x86_64-unknown-elf.json"

[unstable]
build-std = ["core"]

no_stdな環境ならRustのcoreクレートは必須。メモリ管理など追加し始めたらこちらにもallocなどは追加する必要はあると考えられます。

cargo build -Z build-std=core --target=./x86_64-unknown-elf.jsonと同義です。

main.rs

./src/main.rs
#![no_main]
#![no_std]

use core::panic::PanicInfo;

#[no_mangle]
extern "C" fn kernel_main() {
    loop {
        unsafe {
            core::arch::asm!("hlt");
        }
    }
}

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

Rustでインラインアセンブラを使う場合はasm!マクロを使用します。
現在(1.59.0-nightly)ではいくつかの例に登場する#![features(asm)]は不要になっています。

Cargo.toml

Cargo.toml
[package]
name = "kernel"
version = "0.1.0"
edition = "2021"

今回、Cargo.tomlに特別設定することはありません。

ビルド

$ cargo build
// 略

$ readelf -h ./target/x86_64-unknown-elf/debug/kernel.elf
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x101000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          9944 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         4
  Size of section headers:           64 (bytes)
  Number of section headers:         16
  Section header string table index: 14

無事、エントリポイントのアドレスを0x101000に設定できました。

QEMUで確認

(qemu) info registers
RAX=0000000000101000 RBX=0000000000000000 RCX=0000000000000000 RDX=0000000000000000
RSI=000000003fec93d0 RDI=000000003e7711c0 RBP=000000003e7711c8 RSP=000000003feaa5c0
R8 =000000003feaa4b4 R9 =000000003fb7b48f R10=000000003fbcd018 R11=fffffffffffffffc
R12=000000003e771418 R13=000000003effef18 R14=8000000000000002 R15=0000000000000131
RIP=0000000000101004 RFL=00000046 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=1
(qemu) x /2i 0x101004
0x00101004:  eb fd                    jmp      0x101003
0x00101006:  cc                       int3     
(qemu) x /2i 0x101003
0x00101003:  f4                       hlt      
0x00101004:  eb fd                    jmp      0x101003

HLT命令が実行されているらしきことが確認できます。int3はUEFIローダのunreachable!()でしょうか。

終わりに

自作OSや組込などのベアメタル環境であれば、対象に応じたターゲットを作った方が楽な場合も多いでしょう。Rustも基盤はLLVMなのでClangで出来る事はRustでも言語的な制約さえなければできるはずです。

付録

その他参考

https://docs.rust-embedded.org/embedonomicon/custom-target.html

https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/index.html

https://uefi.org/sites/default/files/resources/UEFI_Spec_2_9_2021_03_18.pdf

ローダーのソースコード

オレオレUEFIアプリケーションSDKを使っていますが今回は特に紹介しません。一応、構造をstdに寄せたり、println!マクロを使って画面に文字表示したりできるようにはしてあったり、ジェネリックで取得でするプロトコルを切り替えたりできるようにするなどしてあります。

https://github.com/rust-osdev/uefi-rs
特に理由がなければ自分で構造体定義から書いたりせずuefi-rsを使った方が良いでしょう。

#![no_std]
#![no_main]

use uefi::*;
use uefi::protocol::EfiLoadedImageProtocol;
use uefi::protocol::file::{EfiSimpleFileSystemProtocol, EfiFileProtocol, EfiFileInfo};
use uefi::types::EfiHandle;

fn open_root() -> &'static mut EfiFileProtocol {
    let image_handle = uefi::image_handle();
    let p = uefi::system_table().boot_services().open_protocol::<EfiLoadedImageProtocol>(
        image_handle, 
        image_handle, 
        EfiHandle::null(), 
        1).unwrap();
    let fs = uefi::system_table().boot_services().open_protocol::<EfiSimpleFileSystemProtocol>(
        p.device_handle,
        image_handle,
        EfiHandle::null(),
        1
    ).unwrap();
    fs.open_volume().unwrap()
}

#[no_mangle]
unsafe fn main() {
    let mut buf : [u8; 1024*8] =  [0; 1024*8];
    let memmap= uefi::system_table().boot_services().mem_service().get_memory_map(&mut buf).unwrap();
    
    let root = open_root();
    let mut filename = String16::from_str("\\kernel.elf");
    let kernel_file = root.open(
        &mut filename, 
        EfiFileProtocol::EFI_FILE_MODE_READ).unwrap();

    let kernel_info = kernel_file.get_info::<EfiFileInfo<12>>().unwrap();

    let mut kernel_file_size = kernel_info.filesize as usize;
    let mut kernel_base_addr = 0x100000;

    system_table().boot_services().mem_service().alloc_pages(
        services::mem::AllocateType::AllocateAddress, 
        services::mem::MemoryType::EfiLoaderData, 
        (kernel_file_size as usize + 0xfff) / 0x1000, &mut kernel_base_addr);

    kernel_file.read(&mut kernel_file_size, kernel_base_addr as usize as *mut core::ffi::c_void).unwrap();

    println!("Kernel: 0x{:x} ({} bytes)", kernel_base_addr, kernel_file_size);

    if let Err(_e) = system_table().boot_services().exit_boot_services(image_handle(), memmap.key()) {
        if let Ok(memmap) = system_table().boot_services().mem_service().get_memory_map(&mut buf) {
            
            if let Err(e) = system_table().boot_services().exit_boot_services(image_handle(), memmap.key()) {
                println!("Could not exit boot services. {:?}", e);
                loop {}
            }
        } else {
            println!("Could not get memory map.");
            loop {}
        }
    }
    
    let entry_addr = *((kernel_base_addr + 24) as *mut usize);
    let entry_point : extern "C" fn() = core::mem::transmute(entry_addr);
    (entry_point)();

    unreachable!()
}

elf_mainuefi側で呼ばれています。こちらもThe Enbedonomiconを参考に。


実行結果はこのようになります。

Discussion