LoginSignup
2
1

More than 5 years have passed since last update.

BitVisorのprocessをrustで作ってみようの巻

Posted at

BitVisorにはprocessと呼ばれる機能があります.ここでprocessとは,VMX root-modeのring3で動くプログラムのことです.BitVisorはVMX root-mode ring0, ゲストのOSはVMX non-root mode ring0, ゲストのアプリケーションはVMX non-root mode ring3で動いています.
BitVisor(やゲスト)とメモリ空間が別で特権レベルも低いので,保護ドメインと言ったりもします.

昨年のAdvent Calenderにいくつかprocessに関する記事があります.

以下の文章では主に著者の環境の影響でintelのx86_64を想定してます.ご了承ください.

BitVisorのprocessの基礎

本題に入る前に,processに関して昨年のACで触れられていない点に関して少しだけ書いてみようと思います.
processに関する主なコードはcore/process.cにあります.また,proces/以下に実際にring3で動作するprocessのコードがあります.

processのコンパイル

まず,processのコンパイル方法について.processはMakefile.buildを見てみると以下のような流れでコンパイルされることが分かります.

  • 1. process/Makefileに記載にしたがって,xxx.cからxxx.oをコンパイル
  • 2. xxx.bin.s を作る
    • xxx.bin.sは以下のような内容になっています.
.section .processes
.long prc_xxx_name, 0, prc_xxx_bin, 0
.quad prc_xxx_bin_end - prc_xxx_bin
.quad +0
.data
prc_xxx_name: .string "xxx"
prc_xxx_bin: .incbin "process/xxx.bin"
prc_xxx_bin_end:
  • 3. processes.o をコンパイル
    • xxx.bin.sからprocesses.oを以下みたいにコンパイルします.
gcc -m64 -g -nostdlib -Wl,--build-id=none -static -Wl,-r -o ./process/processes.o ./process/xxx1.bin.s ./process/xxx2.bin.s ...

ということで,processes.oは各processのelfバイナリを含んだelfバイナリになります.
どうしてこんなことになってるかというと,BitVisorはファイルシステムを持っていないのでこういう風にelfの中にオブジェクトを直接埋め込んで後でそこから各プロセスを取り出すようになってるからです.
bitvisor.ldsを見ると,processes.oは.dataセクションに格納されることが分かります.

processのロード

core/process.cnewprocess(name)という関数でprocessを作成します.
この際,まず_builtin_find()で指定した名前のprocessのELFバイナリが存在するか確認しています.
もしprocessが見つかればprocess_new()でprocessを作ります.

process_new()の主処理を抜粋すると,

  1. mm.c:mm_process_alloc(&phys)でprocess用のpage directory entry領域を取得して初期化します.
    • mm.c:create_initial_map()を見ると0x0000_0000-0x3fff_ffffがプロセス用のメモリ空間のようです
  2. mm.c:mm_process_switch()でprocess用のpage tableに切り替えます.
  3. process_load()でprocessをロードします
    • ELFのセクションを見ていき,ロードすべき領域があればload_bin()でロードします
    • load_bin()では必要ならページを割り当ててmemcpyします
    • プロセス自体はもともとrelocatableなオブジェクトとしてコンパイルされているので,アドレスは先頭0からコピーされていくことになります.
    • 戻り値としてエントリポイントのアドレス(_start()の開始位置)を返します.このアドレスはprocessのmsgdescの0番のコールバック関数として登録されます.
  4. mm.c:mm_process_switch()でpage tableを元に戻します.
  5. 戻り値としてprocessを表すdescriptor番号が返ります

作成されたprocessは後でsendint(d,0)システムコールを呼ぶことで,エントリポイントから処理を開始します.
sendint()に関しては昨年のACの記事を見てみてください.
少しだけ補足をすると,sendint()を呼ぶと,

  1. call_msgfunc1()の中でpage tableが切り替わり,
  2. call_msgfunc0()の中で実際に関数を呼びます
    • 実際にはスタック上に関数のアドレスを積んでprocess_sysenter.s:ret_to_use64()の中でsysretqを利用してユーザモードへ遷移します.

rustでprocessを書いてみる

ということで,上で書いたように適当にxxx.oというオブジェクトファイルを作ることができれば,あとはそれをprocesses.oに埋め込むだけです.
Cで書く方法は既にあるので今回はrustで書いてみました.いろんな言語で書ければその分いいことがあるかもしれません.

前準備

rustではno_stdと呼ばれる機能を使うことでCでいうnostdlibのように標準ライブラリに依存しないバイナリが作成できます.
no_stdに関して詳しくはこちらでも見てみてください.
no_stdといっても何もライブラリが無い訳ではなく,coreと呼ばれる環境に非依存なライブラリが自動でリンクされます.

今回の場合,以下のようなファイルを作成すればとりあえず何もしないprocessが作成できます.

cargo.toml

[dependencies]
rlibc = "1.0"
compiler_builtins = { git = "https://github.com/rust-lang-nursery/compiler-builtins" }

src/lib.rs

#![crate_type = "staticlib"]
#![feature(lang_items)]
#![no_std]
#![feature(compiler_builtins_lib)]
#![feature(asm)]

extern crate rlibc;
extern crate compiler_builtins;

pub mod syscalls;
#[macro_use]
pub mod io;

#[lang = "eh_personality"]
#[no_mangle]
pub extern "C" fn eh_personality() {}

#[lang = "panic_fmt"]
#[no_mangle]
pub extern "C" fn panic_fmt(fmt: core::fmt::Arguments, file: &'static str, line: u32) -> ! {
    println!("program panic at '{}', {}:{}", fmt, file, line);
    println!("process terminated.");
    syscalls::exitprocess(1);
    unreachable!();
}

#[no_mangle]
pub extern "C" fn _start(_a: u32, _b: u32) -> u32 {
    syscalls::exitprocess(0);
    0
}

no_stdを使用する際にはいくつか自前で用意しなければならない関数があり,上ではそれを定義しています.(syscallなどはこのあと説明します)

ビルドは以下のようにします.

xargo build --verbose --target=x86_64-bitvisor --release
ld --gc-sections -r -e _start ./target/x86_64-bitvisor/release/libxxx.a -o xxx.o

xargoターゲットにx86_64-bitvisorを指定してビルドしたあと,ldでオブジェクトファイルを作成します.この際--gc-sectionsを使って不要なものは除去します.あとはMakefileをごにょごにょしてxxx.oをうまくprocesses.oに埋め込めばokです.

システムコールの追加

process/lib以下にprocessが使うことのできるライブラリ関数がいくつか用意されています.
rustからCの関数を呼ぶことは簡単なのでこのライブラリをそのまま使ってもいいんですが,今回は自前で用意してみます.

何か有用なことをするためにはまずBitVisor本体とやりとるするためのシステムコールが必要です.syscalls.rsでそのシステムコールの定義をおこなっています.
以下に例を示します.rustのインラインアセンブラはLLVM IRのものに似ていますが,なぜか微妙に違うので注意が必要です.

macro_rules! syscall2 {
    ($rb:expr, $rs:expr, $rd:expr, $ra:expr) =>{
        unsafe {
            asm!("syscall"
                    : "={rax}" ($ra)
                    : "{rbx}" ($rb as u64),
                      "{rsi}" ($rs as u64),
                      "{rdi}" ($rd as u64)
                    : "memory", "cc", "rcx", "rdx"
                      ,"r8", "r9", "r10", "r11", "r12", "r13"
                      ,"r14", "r15"
                    : "volatile");
        }
    }
}

#[allow(unused_mut)]
pub fn msgsendint(desc: i32, data: i32) -> i32 {
    let mut tmp: u64;
    syscall2!(SyscallNumber::MSGSENDINT, desc, data, tmp);
    return tmp as i32;
}

ちなみに,BitVisorのシステムコールの引数の順序はLinuxとかとは異なります.
また,syscall.rsではBitVisorのシステムコール全て定義していますが,msgsendintexitprocessぐらいしか使ったことがないので他のものは何か問題あるかもしれません.

printf

msgsendintシステムコールで他のprocessとのmessageのやりとりをします.
dbgshからprocessを起動した場合,0がキーボード入力,1が画面出力に対応します.これを利用して入出力関数を作成します.

rustのlibcoreにはprintf相当の機能が含まれており,core::fmt::Writeトレイトを実装することでその機能が利用できます.

extern crate core;
use core::fmt;

use syscalls;

fn putchar(c: u8) {
    syscalls::msgsendint(1, c as i32);
}

pub struct Writer;

impl Writer {
    pub fn write_byte(b: u8) {
        putchar(b);
    }
}

impl fmt::Write for Writer {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        for b in s.bytes() {
            Writer::write_byte(b);
        }

        Ok(())
    }
}

#[macro_export]
macro_rules! println {
    () => {
        print!("\n")
    };
    ($fmt:expr) => {
        print!(concat!($fmt, "\n"))
    };
    ($fmt:expr, $($arg:tt)*) => {
        print!(concat!($fmt, "\n"), $($arg)*)
    };
}

#[macro_export]
macro_rules! print {
    () => ();
    ($fmt:expr) => {
        {
            use core::fmt::Write;
            write!(::io::Writer, $fmt).unwrap();
        }
    };
    ($fmt:expr, $($arg:tt)*) => {
        {
            use core::fmt::Write;
            write!(::io::Writer, $fmt, $($arg)*).unwrap();
        }
    };
}

これだけでprintln!{}が使用できるようになります.まぁ,自分でprintfを実装したい人にはちょっと物足りないかもしれないですね()

同様に,lineinputで文字列読み込みの関数を定義しています (process/lib/lib_lineinput.cで定義されている関数は履歴機能があるなどこれよりも高機能です).

メモリの確保

processの中でmallocみたいなことをしたい場合は,適当なサイズのglobal変数を確保してそれを利用するのが楽な方法だと思います.
実際process/lib/lib_mm.cではそんなことをしているように見えます.

さて,rustではメモリアロケータ用のトレイトが用意されています.このトレイトを実装してあげると,allocというライブラリが使えるようになり,つまりBoxVecが使えるようになります.
mm.rsにrustらしからぬunsafeにまみれた突っ込みどころの多い怪しいコードがありますが,これを利用するとglobal変数の領域をヒープとして利用できるようになります (実際にはXargo.tomlにallocを使う旨の記載が必要です).

ここに入力文字列をエコーして入力が数字なら入力数字以下の素数を出力するという究極に意味が分からないプログラムがあります. (Makefileの修正がいい加減なのは目を瞑ってください..)

#![crate_type = "staticlib"]
#![no_std]
#![feature(alloc)]
#![feature(global_allocator)]
#![feature(iterator_step_by)]
#![feature(inclusive_range_syntax)]

#[macro_use]
extern crate alloc;
#[macro_use]
extern crate bitvisor_process_lib;

use bitvisor_process_lib::*;
use bitvisor_process_lib::mm::{Allocator,heap_init};

#[global_allocator]
static ALLOC: Allocator = Allocator;

static mut HEAP: [u8; 1024 * 1024] = [0; 1024 * 1024];

#[no_mangle]
pub extern "C" fn _start(_a1: i32, _a2: i32) -> i32 {
    let mut buf: [u8; 10] = [0; 10];

    unsafe{
        heap_init(&HEAP as *const _ as usize, HEAP.len());
    }
    println!("heap initialized");

    loop {
        print!("echo> ");
        let input = io::lineinput(&mut buf).unwrap();
        if input == "exit" {
            break;
        }
        println!("{}", input);

        if let Ok(n) = input.parse::<u32>() {
            let mut a = vec![2];
            for i in (3..=n).step_by(2) {
                if a.iter().filter(|&n| i % n == 0).take(1).count() == 0 {
                    print!("{} ", i);
                    a.push(i);
                }
            }
            println!{};
        }
    }
    syscalls::exitprocess(0);

    0
}
$ ./dbgsh
> echo
heap initialized
echo> hello
hello
echo> 100
100
3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
echo> exit
>

ちなみにメモリが足らなくなると静かに死にます.

備考

応用

実際に何かprocessの中で有用なことをする場合は,

  • BitVisor本体から(msgsendint()ではなく)msgsendbuf()を使ってprocessに対して処理するデータを送信
  • processは受け取ったバッファのデータに対して処理をして返す

という流れになると思います.ストレージの暗号化なんかはドライバが入出力の前にmsgsendbufを使って暗号化していたような気がします.どうしてもprocess切り替えのコストがかかるので,あまり多量にprocessを呼ぶような構成にすると通常時の性能が落ちるかもしれません.

注意点

no_stdやメモリアロケータに関連する機能の多くはunstableで今後仕様が変わる可能性が十分あります.というか実際よく変わります.

ファイルサイズ

各processのファイルサイズは以下のようになっています.

% ls -1sh *.o | sort -k1 -nr
 76K echo.o
 36K debug.o
 20K idman.o
 20K echoctl.o
 16K vpn.o
 16K monitor.o
 12K shell.o
 12K sendexample.o
 12K log.o
8.0K storage.o
8.0K serialtest.o
8.0K sendint.o
8.0K recvexample.o
8.0K panic.o
8.0K init.o
8.0K help.o

echo.oが今回作成したprocessです (--gc-sections後).76KBなので他と比べると結構大きくなってしまいました.ただし,liballocを含めなかったら20KB以下だったと思います.

浮動小数点演算

soft-floatが有効になっているので効率は悪いでしょうが浮動小数点演算が使えます.
どうしてもFPUが使いたいという人はmsgsendintで呼ばれる関数の先頭でFPUレジスタを退避してprocessを終了するときに戻せば使えると思います.多分.

スケジューリング

BitVisorにprocessをプリエンプティブにスケジューリングする機能はありません (processがsystem callを呼んだ際にスケジューリングされる可能性はあります).うっかりprocessの中で無限ループしてしまうと永遠にコアを占有してしまうかもしれません.そうした場合にprocessをkillする機能はないと思いますが,IPIでNMIを送ってNMIハンドラでなんとかすればできるかもしれません.

まとめ

BitVisorのprocessのコンパイル/ロードの流れと,rustによるprocessの作成方法について書きました.
本来なら何か有用なものを作ろうと思ってたんですが... 時間切れなのでまたの機会にでも (ないかもしれない)
(BitVisorの記事というよりrustの記事に(ry)

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1