Rustでターゲットを自作してみかん本3.3(day03a)の内容をやる
はじめに
そもそも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ファイル
{
"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::specのTarget
やTargetOption
構造体に定義があり、その詳細に関しては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
[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
#![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
[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でも言語的な制約さえなければできるはずです。
付録
その他参考
ローダーのソースコード
オレオレUEFIアプリケーションSDKを使っていますが今回は特に紹介しません。一応、構造をstd
に寄せたり、println!
マクロを使って画面に文字表示したりできるようにはしてあったり、ジェネリックで取得でするプロトコルを切り替えたりできるようにするなどしてあります。
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_main
はuefi
側で呼ばれています。こちらもThe Enbedonomiconを参考に。
実行結果はこのようになります。
Discussion