はじめに
この記事は自作OS Advent Calendar 2021並びにRust Advent Calendar 2021の14日目の記事です.
Rustを使用してOSを自作する場合,Nightlyの使用が前提とされている雰囲気を感じます.有名なチュートリアルであるWriting an OS in RustでもNightlyの機能を使用していますし,Rust OSDevが管理しているクレートにおいても,Nightlyでのみビルド可能なものが存在します.しかし実際には,Nightlyの機能を一切使用せず,安定版RustでOSを記述することも一応可能です.
この記事では,OSを書く際によく用いられ,かつNightly Rustでのみ利用可能な機能について説明し,安定版Rustでの代替方法を説明します.なお,安定版とNightlyの違いについては以下のWebページを確認してください.
また,実際に安定版のRustで書いたコード例はこちらとなります.
目次
- UEFIターゲット
core
クレートが要求するシンボルを用意する- カスタムターゲット
- インラインアセンブリ
alloc
クレート
UEFIターゲット
こちらの記事を参照してください.
core
クレートが要求するシンボルを用意する
概要
core
クレートは,いくつかのシンボルが既にあることを前提として構築されています.そのうちmemcpy
,memcmp
,そしてmemset
に関しては,Nightlyを使用する場合,以下の内容を.cargo/config.toml
に記述することで提供することができます.
[unstable] build-std = ["core", "compiler_builtins"] build-std-features = ["compiler-builtins-mem"]
安定版での代替方法
rlibc
クレートを使う方法が一番簡単です.このクレートは既にdeprecatedとなっているのですが,代替として述べられているcompiler-builtins
クレートはNightlyでしかビルドできないため,rlibc
を利用します.Cargo.toml
に依存クレートとして追加します.
[dependencies] rlibc = "1.0.0"
また,確実にクレートとリンクさせるために,以下のコードをmain.rs
ないしlib.rs
に追加します.
extern crate rlibc as _;
カスタムターゲット
概要
Rustでは,コンパイル時にターゲットを指定する必要があります.例として,x86_64-unknown-linux-gnu
や,x86_64-pc-windows-gnu
などが挙げられます.これらのターゲットは基本的に既存のプラットフォームを対象としているため,ベアメタルなプログラミングでは本来利用することができません.この場合,JSON形式のファイルにターゲットの情報を記述して,そのファイルを指定してビルドを行います.詳細は以下のWebページを確認してください.
しかしながら,そのようなターゲットに対しては当然ビルド済みの標準ライブラリは用意されていないため,Cargoの-Z build-std
オプションを利用して自分でビルドする必要があります.これはNightlyを必要とします.
最近x86_64-unknown-none
というターゲットが追加されました.このターゲットはまさにベアメタル用のターゲットなのですが,標準ライブラリが付属せず,RustのツールチェーンをRustupなどを使用せずに自身でソースコードからビルドするか,build-std
を使用して標準ライブラリをコンパイルする必要があります.前者は非常に大掛かりですぐに利用できるものではなく,後者に関してはNightlyが必要です.
安定版での代替
同じアーキテクチャのターゲット,コンパイルオプション,そしてリンカスクリプトを利用します.以下のコードは,最初に載せたリポジトリのものです.
kernel/.cargo/config.toml
[build] target = "x86_64-unknown-linux-gnu" [target.x86_64-unknown-linux-gnu] rustflags = [ "-C", "code-model=kernel", "-C", "link-args=-T kernel/kernel.ld", "-C", "relocation-model=static", "-C", "no-redzone=y", "-C", "default-linker-libraries=n", "-C", "soft-float=y", ]
リンカスクリプトのパスがkernel/kernel.ld
となっていますが,これはワークスペースの関係です.
仮にCargoの既定のビルドターゲットがx86_64-unknown-linux-gnu
であっても,target = "x86_64-unknown-linux-gnu"
と指定してください.これはbuild.rs
が誤った設定でコンパイルされないようにするためです.
kernel/kernel.ld
OUTPUT_FORMAT(elf64-x86-64); ENTRY(main); MEMORY { kernel (WX) : ORIGIN = 0xffffffff80000000, LENGTH = 0x20000000 } SECTIONS { . = 0xffffffff80000000; .text : { *(.text*) } > kernel .rodata : { *(.rodata*) } > kernel .data : { *(.data) } > kernel .bss : { *(.bss) } > kernel .eh_frame : { *(.eh_frame) } > kernel /DISCARD/ : { *(.init) *(.fini) } }
.init
や.fini
は必要ないため破棄しています.
kernel/src/main.rs
#![no_std] #![no_main] #![deny(unsafe_op_in_unsafe_fn)] extern crate kernel as _; use { aligned_ptr::ptr, boot_info::BootInfo, kernel::{idle, init}, }; /// # Safety /// /// `boot_info` must be dereferenceable. #[no_mangle] unsafe extern "sysv64" fn main(boot_info: *mut BootInfo) { // SAFETY: `boot_info` must be dereferenceable. init(unsafe { ptr::get(boot_info) }); idle(); }
リンカスクリプトに関数を認識させるため#[no_mangle]
は必須です.また,メイン関数は名前こそmain
となっていますが,コンパイラが求めるシグネチャではないため,#![no_main]
を用いてすり替えておきます.
Cargo.toml
[profile.dev] panic = "abort" [profile.release] panic = "abort"
パニック時の動作をabort
にしないと,eh_personality
のシンボルを要求されますが,このシンボルを与えるにはNightlyの機能である#![feature(lang_items)]
が必要となります.
制限
通常カーネルのコードでは,SSEやMMXを無効にするべきです.これは,割り込みが発生したときに保存するデータの大きさを減らすためです.詳細は以下のWebページを確認してください.
さて,安定版においてもSSEを無効にするようコンパイラに伝えることは可能です..cargo/config.toml
のrustflags
に以下の行を追加します.
"-C", "target-features=-sse"
しかしこれは未定義動作を引き起こします.既にコンパイルされているRustの標準ライブラリがこの機能を有効にしているため,整合がつかなくなるためです.詳細は以下のWebページを確認してください.
従って現在安定版のみを使用する場合,SSEを完全に無効にする術はありません.故に割り込みハンドラではxmm
レジスタなどの内容をfxsave
とfxrstor
を用いて保存,復帰させる必要があります.
.macro generic_handler vector fxsave_offset .extern interrupt_handler_\vector .global asm_interrupt_handler_\vector asm_interrupt_handler_\vector: push rbp mov rbp, rsp push rax push rcx push rdx push rsi push rdi push r8 push r9 push r10 push r11 // `fxsave` saves 512-byte data, and it requires a 16-byte aligned address. // After an interrupt or exception, if the exception pushes an error code, // `rsp mod 16` is 0. If the interrupt or exception does not push an error // code, `rsp mod 16` is 8, so we add `8` here. See: // https://forum.osdev.org/viewtopic.php?f=1&t=22014 sub rsp, 512+\fxsave_offset fxsave [rsp] call interrupt_handler_\vector fxrstor [rsp] add rsp, 512+\fxsave_offset pop r11 pop r10 pop r9 pop r8 pop rdi pop rsi pop rdx pop rcx pop rax mov rsp, rbp pop rbp iretq .endm .macro handler vector generic_handler \vector 8 .endm .macro handler_with_error_code vector generic_handler \vector 0 .endm handler_with_error_code 0x0e handler 0x20
fxsave
とfxrstor
を実行する際は,スタックのアラインメントに注意してください.これらの命令を実行する前はスタックポインタが16の倍数になっている必要があります.
インラインアセンブリ
概要
高級言語では発行できないような命令は,アセンブリ言語を用いて記述する必要があります.HLTやCLI,STI,IN,OUTはそのような命令の一例です.
インラインアセンブリはNightly Rustにおいて,#![feature(asm)]
や#![feature(asm_sym)]
,#![feature(asm_const)]
によって有効にできます.以下は,インラインアセンブリを使用したコードの例です.
#![features(asm)] pub fn hlt() { unsafe { asm!("hlt", options(nomem, nostack)); } }
なお,#![feature(asm)]
はもう少しで安定化されそうです.
安定版での代替
安定版では独立したアセンブリファイルにコードを記述します.例えば上記のコードの場合,次の内容でアセンブリファイルに記述します.ここではファイル名をasm.s
とします.
.text .code64 .intel_syntax noprefix .global asm_hlt asm_hlt: hlt ret
次に,このファイルをコンパイルするためにcc
クレートをCargo.toml
のbuild-dependency
として追加します.
[build-dependencies] cc = "1.0.70"
そしてbuild.rs
を編集して,cargo build
やcargo run
を実行したときにこのファイルもコンパイルするように指示します.
fn main() { cc::Build::new().file("src/asm.s").compile("asm"); }
最後にこの関数をRustのラッパで包みます.
fn hlt() { extern "sysv64" { fn asm_hlt(); } unsafe { asm_hlt(); } }
extern "C"
の罠
先程のhlt
関数の場合,関数は引数を取らないため関数の呼び出し規則について気にする必要はありませんでした.しかし例えばIN
やOUT
などの命令を使う場合,関数の呼び出し規則を気にかける必要があります.
extern "C"
では,ビルド時のターゲットによって引数がどのレジスタを用いて渡されるかが変化します.アセンブリファイル中で使用している呼び出し規則がビルド時のターゲットと一致するのならば問題ありませんが,例えばアセンブリファイル中ではSystem Vの呼び出し規則に則っている一方,ターゲットがマイクロソフトのx64呼び出し規則に従っている場合などは,呼び出し規則の不一致が発生してしまい,引数が正しく関数に渡されません.このような事態を防ぐため,特にライブラリを記述する際において,アセンブリファイルに記述されている関数を呼び出す際はextern "C"
を使用するるのではなく,extern "sysv64"
あるいはextern "win64"
などと,呼び出し規則を詳細に指定するべきです.
alloc
クレート
概要
alloc
クレートは,ヒープ領域を用いるデータ構造に関する公式のクレートです.
このクレートを使用すること自体は安定版のRustでも可能です.従って以下のコードは安定版でもビルド可能です.
extern crate alloc; use alloc::string::String; fn main() { let quote = String::from("Take the initiative and shoot flame. That's all."); println!("{}", quote); }
しかしながらno_std
では,alloc
クレートを使用したコードを安定版でビルドすることは不可能です.これは,メモリの確保に失敗した場合に呼び出される関数を指定する#[alloc_error_handler]
がNightly限定となっているためです.no_std
な環境においてはalloc
クレートを使用する場合,この属性が付いている関数は必ず用意する必要があり,従ってalloc
クレートを用いたコードをno_std
環境でそして安定版のツールチェーンを用いてビルドすることは不可能です.
代替方法
自分でデータ構造を記述する必要があります.参考文献としてRustonomiconのExample: Implementing Vecを挙げておきます.これはその名の通り,一からVec
を実装していくチュートリアルです.
なお,core
クレートにはGlobalAlloc
が用意されています.