備忘録やめた

備忘録として使用していたけどやめた.このブログに載せてあるコードのライセンスは別途記載がない限りWTFPL OR NYSLです.

安定版Rustでの自作OSの試み

はじめに

この記事は自作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ページを確認してください.

doc.rust-jp.rs

また,実際に安定版のRustで書いたコード例はこちらとなります.

github.com

目次

UEFIターゲット

こちらの記事を参照してください.

tokuchan3515.hatenablog.com

coreクレートが要求するシンボルを用意する

概要

coreクレートは,いくつかのシンボルが既にあることを前提として構築されています.そのうちmemcpymemcmp,そして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ページを確認してください.

os.phil-opp.com

しかしながら,そのようなターゲットに対しては当然ビルド済みの標準ライブラリは用意されていないため,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ページを確認してください.

os.phil-opp.com

さて,安定版においてもSSEを無効にするようコンパイラに伝えることは可能です..cargo/config.tomlrustflagsに以下の行を追加します.

    "-C", "target-features=-sse"

しかしこれは未定義動作を引き起こします.既にコンパイルされているRustの標準ライブラリがこの機能を有効にしているため,整合がつかなくなるためです.詳細は以下のWebページを確認してください.

doc.rust-lang.org

従って現在安定版のみを使用する場合,SSEを完全に無効にする術はありません.故に割り込みハンドラではxmmレジスタなどの内容をfxsavefxrstorを用いて保存,復帰させる必要があります.

.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

fxsavefxrstorを実行する際は,スタックのアラインメントに注意してください.これらの命令を実行する前はスタックポインタが16の倍数になっている必要があります.

インラインアセンブリ

概要

高級言語では発行できないような命令は,アセンブリ言語を用いて記述する必要があります.HLTやCLISTI,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)]はもう少しで安定化されそうです.

github.com

安定版での代替

安定版では独立したアセンブリファイルにコードを記述します.例えば上記のコードの場合,次の内容でアセンブリファイルに記述します.ここではファイル名をasm.sとします.

.text
.code64
.intel_syntax noprefix

.global asm_hlt

asm_hlt:
    hlt
    ret

次に,このファイルをコンパイルするためにccクレートCargo.tomlbuild-dependencyとして追加します.

[build-dependencies]
cc = "1.0.70"

そしてbuild.rsを編集して,cargo buildcargo 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関数の場合,関数は引数を取らないため関数の呼び出し規則について気にする必要はありませんでした.しかし例えばINOUTなどの命令を使う場合,関数の呼び出し規則を気にかける必要があります.

extern "C"では,ビルド時のターゲットによって引数がどのレジスタを用いて渡されるかが変化します.アセンブリファイル中で使用している呼び出し規則がビルド時のターゲットと一致するのならば問題ありませんが,例えばアセンブリファイル中ではSystem Vの呼び出し規則に則っている一方,ターゲットがマイクロソフトのx64呼び出し規則に従っている場合などは,呼び出し規則の不一致が発生してしまい,引数が正しく関数に渡されません.このような事態を防ぐため,特にライブラリを記述する際において,アセンブリファイルに記述されている関数を呼び出す際はextern "C"を使用するるのではなく,extern "sysv64"あるいはextern "win64"などと,呼び出し規則を詳細に指定するべきです.

github.com

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を実装していくチュートリアルです.

doc.rust-lang.org

なお,coreクレートにはGlobalAllocが用意されています.