Introduction Rust for Creating Your Operating System
これは、Rustを使って自作OSをするための手引兼メモです.
間違っていることもあると思うので、何かあればコメントをおねがいします.
Introduction
Rustは、Mozillaによって開発されたプログラミング言語であり、目標の一つにCPUやOSなどのLow-levelなものを抽象化することがある.
特徴としては、メモリ安全なプログラミングを行うための言語的機能などなどといろいろあるが、触れると長くなるので簡単に、OSを作ることを考えた時に気になるであろう事項を書いておく.
また、特に上から順に読む必要はなく、必要な箇所だけ必要なときに参照するしてほしい.
stableなrustだといくつかの機能が使えないようなので、先にEnvironmentの項を見て、ビルドしておくことをおすすめする.
Features of Rust
これらについて、簡単に書こうと思い調べたところ、非常によくまとまっている投稿を発見したので、そちらを参照してほしい.
また、Referencesの段落にも書いてあるが、以下の2つがrust公式のチュートリアルとreferenceである.
- The Rust Programming Language
-
The Rust Reference
ここに書いてある言語的なことは、もちろん全て、これらの中に書いてあるので、より厳密で詳細な事柄が知りたい場合は、そちらを読んでいただきたい.
unsafe block
OSを自作する際、Video memoryやデバイスIOなど、メモリのアドレスを直に指定し、読み書きを行うことがよくある.
C言語でこれを行う場合は、以下のコードが定石だと思われる.
// vram.c
// Fill in text mode vram with blue.
for (size_t i = 0xB8000; i < 0xC0000; i += 2) {
*(uint16_t*)(i) = 0x1220;
}
このような操作をrustで行う場合にはunsafeブロックで該当箇所を囲み、rustコンパイラに、ブロック内の動作はプログラマで責任を持つということを明示する必要がある.
上記Cプログラムと同じことを行うrustプログラム以下になる.
// vram.rs
let mut i = 0xB8000;
while i < 0xC0000 {
unsafe {
*(i as *mut u16) = 0x1220;
}
i += 2;
}
ただし、rustコンパイラの恩恵を最大限に受けるためにunsafeブロックないのコードは最小限に留めることが望ましい.
他にも具体的にunsafeブロック内では以下のことが可能となる.
- 生ポインタの参照外し (上記の例)
- FFI(Foreign Function Interface)経由での関数呼び出し
- ビット単位でのキャスト
- インラインアセンブリ
no_std
通常のプログラムは、何らかのOSの上で実行されるため、システムコールを必要とする(プロセスの生成と終了、入出力など、標準ライブラリの多くはそれを必要とする).
しかし、それらのシステムコールはOSが提供するものであるから、OSを作成するときに使うわけには行かない(というよりないものは使えない、printfは偉大なのだ).
だいぶ大雑把な説明だが、それらを無効にした環境をfreestandingという.
Cでは、リンカやコンパイラにオプションを渡してfreestandingなバイナリ(bare-metalなバイナリ)を生成する.
rustも同様にして、コード中に#[no_std]属性を指定して行う.
これによって、以下の機能などが無効化される.
- メモリ確保
- スレッド
- 出力
no_stdの欠点として、stdに含まれているOS非依存の有用な機能まで無効化されてしまうことがある(例えば、イテレータ).
しかし、libcoreを使うことで、いくつかの便利な機能を使用することができるようになる.
ただ、libcoreはunstableであるので、可能な限りstdを使用することが推奨される.
とはいっても、OSを作るのにstdを使うことは不可能なのでガンガンlibcoreを使っていこうと考えている.
Lang Items
Rustコンパイラに指示を出すために使う.
例えば、言語内には存在しないが、あるライブラリで実装されており、存在しているということをコンパイラに伝えるために使用される.
フォーマットは下記のようになる
#[lang = "..."]
現在(2015/09/11), lang itemはunstableである.
Error output from rustc
rustcコマンドでrustをコンパイルするが、驚くほど丁寧なエラーログだったので、書いておく.
例として、上部でリンクを記載しておいた、rustにおける所有権がどうなっているかを見るコードを挙げる.
struct Sushi {
neta:&'static str,
}
pub fn main() {
let sushi1 = Sushi {neta:"炙りえんがわ"};
let sushi2 = sushi1;
println!("{}", sushi1.neta);
println!("{}", sushi2.neta);
}
このコードではsushi1
がsushi2
にmoveされているため、sushi1.neta
を出力することは出来ない.
試しにコンパイルしてみると、以下のような出力を得られる.
% rustc ./sushi.rs
./sushi.rs:9:20: 9:31 error: use of moved value: `sushi1.neta` [E0382]
./sushi.rs:9 println!("{}", sushi1.neta);
^~~~~~~~~~~
<std macros>:2:25: 2:56 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
./sushi.rs:9:5: 9:33 note: in this expansion of println! (defined in <std macros>)
./sushi.rs:9:20: 9:31 help: run `rustc --explain E0382` to see a detailed explanation
./sushi.rs:7:9: 7:15 note: `sushi1` moved here because it has type `Sushi`, which is moved by default
./sushi.rs:7 let sushi2 = sushi1;
^~~~~~
./sushi.rs:7:9: 7:15 help: if you would like to borrow the value instead, use a `ref` binding as shown:
./sushi.rs: let ref sushi2 = sushi1;
error: aborting due to previous error
まず、真っ先にmoveされてるからエラーであるとのメッセージがある.
その後に、大括弧で書いてあるのがエラー種類になる.
もう少し下を見てみると
./sushi.rs:9:20: 9:31 help: run `rustc --explain E0382` to see a detailed explanation
とのことなので、rustc --explain E0382
を実行してみる.
:% rustc --explain E0382
This error occurs when an attempt is made to use a variable after its contents
have been moved elsewhere. For example:
struct MyStruct { s: u32 }
fn main() {
let mut x = MyStruct{ s: 5u32 };
let y = x;
x.s = 6;
println!("{}", x.s);
}
Since `MyStruct` is a type that is not marked `Copy`, the data gets moved out
of `x` when we set `y`. This is fundamental to Rust's ownership system: outside
of workarounds like `Rc`, a value cannot be owned by more than one variable.
If we own the type, the easiest way to address this problem is to implement
`Copy` and `Clone` on it, as shown below. This allows `y` to copy the
information in `x`, while leaving the original version owned by `x`. Subsequent
changes to `x` will not be reflected when accessing `y`.
#[derive(Copy, Clone)]
struct MyStruct { s: u32 }
fn main() {
let mut x = MyStruct{ s: 5u32 };
let y = x;
x.s = 6;
println!("{}", x.s);
}
Alternatively, if we don't control the struct's definition, or mutable shared
ownership is truly required, we can use `Rc` and `RefCell`:
use std::cell::RefCell;
use std::rc::Rc;
struct MyStruct { s: u32 }
fn main() {
let mut x = Rc::new(RefCell::new(MyStruct{ s: 5u32 }));
let y = x.clone();
x.borrow_mut().s = 6;
println!("{}", x.borrow.s);
}
With this approach, x and y share ownership of the data via the `Rc` (reference
count type). `RefCell` essentially performs runtime borrow checking: ensuring
that at most one writer or multiple readers can access the data at any one time.
If you wish to learn more about ownership in Rust, start with the chapter in the
Book:
https://doc.rust-lang.org/book/ownership.html
懇切丁寧な例題付き(ドキュメントへのリンクまである)の説明が出力された.
いちいちググらなくても大体のことはこれで理解できるはずだ.
大変ありがたい.
しかし、すべてのエラーについて、こういった説明があるわけではないらしい.
この文書がMarkdownで書かれているので、削除してしまったが、説明中のプログラムはバッククォートで囲まれている.
Testing
Rustでは、テストコードをソースファイル内に書くことが出来る.
今まで、C言語で書いていたことを考えると非常に便利だ.
テストコードでは、当然、標準出力に何かを表示してデバッグを行いたくなる.
C言語では、マクロやMakefileなどを駆使する必要があり、これは非常に面倒くさかった.
もしや、Rustでも、そうなのでは無いかと危惧したが結果として、簡単だった.
Rustでは、条件付きコンパイルを行うことが出来るため、それを使ってソースにいくつか書き加えるだけで、テスト時のみ標準ライブラリを使用するといったことが可能だった.
つまり、自作OSではlibcoreを、テストではlibstdを使うという切り替えを行う事ができた. (ただ、libcoreはunstableなので、それが気がかりである...)
書きかけのもので申し訳ないが、textモードで描画するためのOS内のgraphicモジュールである.
#![feature(core)]
#![feature(no_std)]
#![no_std]
use core::cell::Cell;
pub static COLOR_TEXT_BLACK: u8 = 0x0;
pub static COLOR_TEXT_BLUE: u8 = 0x1;
pub static COLOR_TEXT_GREEN: u8 = 0x2;
pub static COLOR_TEXT_CYAN: u8 = 0x3;
pub static COLOR_TEXT_RED: u8 = 0x4;
pub static COLOR_TEXT_MAGENTA: u8 = 0x5;
pub static COLOR_TEXT_BROWN: u8 = 0x6;
pub static COLOR_TEXT_LIGHT_GRAY: u8 = 0x7;
pub static COLOR_TEXT_DARK_GRAY: u8 = 0x8;
pub static COLOR_TEXT_LIGHT_BLUE: u8 = 0x9;
pub static COLOR_TEXT_LIGHT_GREEN: u8 = 0xA;
pub static COLOR_TEXT_LIGHT_CYAN: u8 = 0xB;
pub static COLOR_TEXT_LIGHT_RED: u8 = 0xC;
pub static COLOR_TEXT_LIGHT_MAGENTA: u8 = 0xD;
pub static COLOR_TEXT_YELLOW: u8 = 0xE;
pub static COLOR_TEXT_WHITE: u8 = 0xF;
pub struct Graphic {
vram_addr: usize,
is_text_mode: bool,
pub color_background: Cell<u8>,
pub color_foreground: Cell<u8>,
}
impl Graphic {
pub fn new(is_text: bool, vaddr: usize) -> Graphic
{
let default_bg = if is_text == true { COLOR_TEXT_BLACK } else { 0 };
let default_fg = if is_text == true { COLOR_TEXT_GREEN } else { 0 };
Graphic {
vram_addr:vaddr,
is_text_mode:is_text,
color_background:Cell::new(default_bg),
color_foreground:Cell::new(default_fg),
}
}
pub fn change_color(&self, bg: u8, fg: u8)
{
self.color_background.set(bg);
self.color_foreground.set(fg);
}
fn get_one_pixel(&self, c: &char) -> u16
{
let bg = self.color_background.get() as u16;
let fg = self.color_foreground.get() as u16;
(bg << 12) | (fg << 8) | (*c as u16)
}
}
// Test codes.
#[cfg(test)]
#[macro_use]
extern crate std;
#[cfg(test)]
mod test {
use super::*;
#[test]
fn get_one_pixel() {
let graphic = Graphic::new(true, 0xB800);
graphic.change_color(COLOR_TEXT_BLUE, COLOR_TEXT_GREEN);
assert_eq!(graphic.get_one_pixel(&'A'), 0x1241);
println!("\t0x{:x}", graphic.get_one_pixel(&'A'));
}
}
この中の#[cfg(test)]
というのが条件付きコンパイルの設定値で、テスト時のみ有効にするという意味だ.
ファイル先頭で#![no_std]
しているので、テストコードのために、テスト時のみextern crate std;
するようにしている.
% rustc --test ./graphic.rs
% ./graphic
running 1 test
test test::get_one_pixel ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
テストを実行するのも特に難しくなく、--test
をつけるだけという、手軽さがよい.
また、実行すると、テスト結果も自動でまとめてくれるので、これもまたありがたい.
References
The Rust Programming Language
The Rust Reference
Rustのポインタ(所有権・参照)・可変性についての簡単なまとめ
Borrow and AsRef
Rust1.0学習用私的メモ
Writing Unsafe and Low-Level Code in Rust
Writing an OS in Rust in tiny steps (Steps 1-5)
Environment
Rustのstable(1.2)では, OSを自作するために必要な機能が"Experimentalなので使用できないよ"とのエラーがでるので,nightly版をビルドする.
もしくは、ここからnightlyのバイナリをダウンロードしても良い.
ここでの必要な機能とは、前項で説明したfreestandingなバイナリを出力するために必要なものである.
以降では、githubにあるrustのソースから、以下の2つのアーキテクチャに対応したバイナリを生成できるrustコンパイラ(rustc)のビルドについて説明をする.
似た手順でARM対応することも可能なので、詳しくはReferencesを参考にしてほしい.
また、rustが対応しているアーキテクチャはここを見るとわかる.
- i686-unknown-linux-gnu
- x86_64-unknown-linux-gnu
rustでは、上記のようにx86_64ではなく、i686となっていることに注意 (i386をサポートしないという意味なのかどうかはわからない).
また、targetはtarget tripleという形式で指定することにも注意が必要.
ビルドを実行した環境はArch Linuxなので、Arch Linux基準で説明する.
他のLinuxディストリビューションでも、大体は同じはずである.
また、rustに限った話ではないが、クロスコンパイル時にbuild, target, hostを混乱しがちなので、はっきりさせておく.
詳しくはこの記事がわかりやすいので、参照してほしい.
項目 | 設定 |
---|---|
Build | x86_64 |
Host | x86_64 |
Target | x86_64, i686 |
まず、i686形式を出力するため、multilib対応のC/C++コンパイラが必要となる.
clangでもビルド可能なはずだが、utilityヘッダがないというコンパイルエラーがどうしても取れなかったためgccを使った.
以下の手順でコマンドを実行すれば、小一時間でコンパイルが終わるはずである.
yay -S gcc-multilib lib32-gcc-libs
git clone https://github.com/rust-lang/rust.git
cd rust
./configure --target=i686-unknown-linux-gnu,x86_64-unknown-linux-gnu
make -j 4
sudo make install
この時、システムディレクトリ(/usr/local/)ではなく、$HOME/.local/などにインストールしたい場合は、configureで設定を帰るとよい.
ただし、rustcを実行するときの動的ライブラリが見つからないとのエラーが出るので、LD_LIBRARY_PATHを設定するか、ldconfigするかの設定が必要となる.
ここを参考に好きな設定をするとよい.
ついでに、cargoも入れておくと便利かもしれない.
Cargoはrust専用のパッケージマネージャであり、ビルドツールで、依存関係があるビルドなども綺麗に設定できることが売りらしい.
ただ、OSをビルドするのは難しそうだったのでmakeを使っている.
余談だが、rustのライブラリのことをcrateといい、木箱の意味.
そして、cargoは貨物という意味.
つまり、木箱を貨物で運ぶということになる.
References
Rust binary archives
How to build rust for cross-compiling?
Embedded Rust Right Now!
Cannot link against core library when cross compiling
Rust bare metal on ARM microcontroller
ruststrap/1-how-to-cross-compile.md
Rustインストール後にlibrustc_driverが無いといわれる現象について
japaric/rust-on-openwrt
rust/src/librustc_back/target/
Examples
ここでは、具体的な例を紹介する.
Rustの関数をCから呼び出す
ructc
% rustc -vV
rustc 1.5.0-dev (e362679bb 2015-10-08)
binary: rustc
commit-hash: e362679bb6a76064442492fdd5e07f06854f5605
commit-date: 2015-10-08
host: x86_64-unknown-linux-gnu
release: 1.5.0-dev
以下のRustプログラムがCから呼び出される側.
// hello.rs
#![feature(no_std, lang_items, core_str_ext)]
#![no_main] // main関数が無いということをrustcに伝えている.
#![no_std] // 同様に、stdを使用しないということをrustcに伝えている.
// Cから呼び出せるようにmanglingを無効にする.
#[no_mangle]
pub extern fn hello_rust() -> *const u8
{
"Hello, world!\0".as_ptr()
}
// この辺はいまいちよくわかっていない
// おまじない状態である.
#[lang="stack_exhausted"] extern fn stack_exhausted() {}
#[lang="eh_personality"] extern fn eh_personality() {}
#[lang="panic_fmt"]
pub fn panic_fmt(_: &core::fmt::Arguments, _: &(&'static str, usize)) -> !
{
loop { }
}
呼び出す側のCプログラム.
// main.c
#include <stdio.h>
extern char const* hello_rust(void);
int main(int argc, char const* argv[])
{
puts(hello_rust());
return 0;
}
コンパイルと実効結果は以下のようになる.
% rustc --emit=obj hello.rs
% clang hello.o main.c
% ./a.out
Hello, world!
ソースコード内にコンパイラへの指示がいくつもかけるのは非常に便利だ.
おそらく、OSを作り始めていく時も、こんなふうなrustコードを書いていくことになると思われる.
manglingについては、ここを参照してほしい.
OS Bootloader
この文書はRustを使って自作OSを作るための手引であるので、少しは自作OSらしい例も載せなければならないと思う.
そこで、nasmで書いたブートローダーからrustのカーネルエントリーを呼び出すということをやってみる.
なお、multiboot-specificationを使用するので、厳密にはブートローダーというよりは、nasmコード自体がカーネルエントリーになり、rustコードはカーネル内の別関数という方が厳密かもしれない.
例のごとく、multiboot-specificationについてはここを参照してほしい.
また、multiboot-specificationに対応するローダーの一つがgrub2であるので、grub2を使う.
512バイト(MBRのエントリーを除けば446バイト)で縛りプレイのブートローダーからガリガリ書いていくのは最高にエキサイティングだし、自作OSの最初の醍醐味ではあるが、コードが長くなるのでどうか許してほしい.
というわけで、以下がnasmの一番初めに呼び出される部分になる.
; boot.asm
; vim:ft=nasm:foldmethod=marker
bits 32
; Multiboot header section.
; This header is read by multiboot bootstraps loader (E.g., grub2).
; And based on "The Multiboot Specification version 0.6.96"
; {{{
MULTIBOOT_HEADER_MAGIC equ 0x1BADB002 ; The magic number identifying the header
MULTIBOOT_PAGE_ALIGN_BIT equ 0x00000001 ; Align all boot modules on i386 page (4KB) boundaries.
MULTIBOOT_MEMORY_INFO_BIT equ 0x00000002 ; Must pass memory information to OS.
MULTIBOOT_VIDEO_MODE_BIT equ 0x00000004 ; Must pass video information to OS.
MULTIBOOT_BOOTLOADER_MAGIC equ 0x2BADB002 ; This must be set in %eax by bootloader.
section .multiboot_header
align 4
dd MULTIBOOT_HEADER_MAGIC
dd (MULTIBOOT_PAGE_ALIGN_BIT | MULTIBOOT_MEMORY_INFO_BIT | MULTIBOOT_VIDEO_MODE_BIT)
dd -(MULTIBOOT_HEADER_MAGIC + (MULTIBOOT_PAGE_ALIGN_BIT | MULTIBOOT_MEMORY_INFO_BIT | MULTIBOOT_VIDEO_MODE_BIT))
; Address field (this fields are required in a.out format)
dd $0
dd $0
dd $0
dd $0
dd $0
; Graphic field
dd $1
dd $0
dd $0
dd $0
; }}}
; Boot kernel section
; This section invoked by bootloader.
; Then, This calls kernel main routine.
; {{{
section .boot_kernel
align 4
global boot_kernel
extern main
boot_kernel:
; NOTE that We cannot use eax and ebx.
; Because eax and ebx has multiboot magic number and multiboot info struct addr.
cli
; Check multiboot magic number.
; If mismatched, goto sleep loop.
cmp eax, MULTIBOOT_BOOTLOADER_MAGIC
jne sleep
; Set kernel stack.
mov esp, kernel_init_stack_top
; ebx is pointer to multiboot info struct.
push ebx
; Jump to main routine
call main
sleep:
hlt
jmp sleep
; }}}
; BSS (Block Started by Symbol) section
; This allocate initial kernel stack witch is 4KB.
; {{{
section .bss
align 4
KERNEL_INIT_STACK_SIZE equ 0x1000
global kernel_init_stack_top
kernel_init_stack_bottom:
resb KERNEL_INIT_STACK_SIZE
kernel_init_stack_top:
; }}}
次に、main関数を持つrustコードが以下である.
// main.rs
#![feature(lang_items)]
#![feature(start)]
#![no_main]
#![feature(no_std)]
#![no_std]
#![feature(asm)]
#[no_mangle]
#[start]
pub extern fn main()
{
let mut i = 0xB8000;
while i < 0xC0000 {
unsafe {
*(i as *mut u16) = 0x1220;
}
i += 2;
}
loop {
unsafe {
asm!("hlt");
}
}
}
#[lang = "stack_exhausted"]
extern fn stack_exhausted() {}
#[lang = "eh_personality"]
extern fn eh_personality() {}
#[lang = "panic_fmt"]
pub fn panic_fmt(_: &core::fmt::Arguments, _: &(&'static str, usize)) -> !
{
loop { }
}
こちらは先ほどと対して変わらない.
main関数の中では画面を一色で塗りつぶすという処理を行っている(これは、別の段落の例として上げた処理である).
なお、このmain関数は通常のプログラムのmain関数とは少し違うので注意してほしい.
その証拠に#[start]
というattributeが書いてある.
これは、ここをプログラム全体のエントリーポイントにするという意味である.
次に、リンカスクリプトがいかになる.
OUTPUT_FORMAT("elf32-i386")
SECTIONS {
/* OS booting entry point. */
ENTRY(boot_kernel)
LD_KERNEL_START = .;
. = 0x00100000;
/*
* Entry point.
* This is called by bootstraps loader.
* NOTE that AT command indicates load memory address.
* Kernel load address is NOT equal virtual address.
* So, We subtract virtual base address.
*/
.boot_kernel BLOCK(4K) : ALIGN(4K)
{
*(.multiboot_header)
*(.boot_kernel)
}
.text BLOCK(4K) : ALIGN(4K)
{
*(.text*)
}
.data BLOCK(4K) : ALIGN(4K)
{
*(.data*)
*(.rodata*)
}
.bss BLOCK(4K) : ALIGN(4K)
{
LD_KERNEL_BSS_START = .;
*(.bss*)
*(COMMON)
LD_KERNEL_BSS_END = .;
}
LD_KERNEL_BSS_SIZE = LD_KERNEL_BSS_END - LD_KERNEL_BSS_START;
LD_KERNEL_END = .;
LD_KERNEL_SIZE = LD_KERNEL_END - LD_KERNEL_START;
}
multiboot headerがバイナリの先頭8KB内に存在しなければならないので、nasmで書いたセクションを先頭の方に持ってきている.
また、OSバイナリはelf形式を使用する.
% rustc -Z no-landing-pads --target=i686-unknown-linux-gnu --emit=obj ./main.rs -o main.o -C lto -C opt-level=2
% nasm -f elf32 boot.asm -o boot.o
% ld -Map kernel.map -m elf_i386 --format elf32-i386 -nostartfiles -nodefaultlibs -nostdlib -static -T link.ld -o axel.bin boot.o main.o
% cp axel.bin ./img/boot/
% grub-mkimage -O i386-pc -o ./img/efi.img multiboot biosdisk iso9660
% grub-mkrescue -o axel.iso ./img
% qemu-system-i386 -monitor stdio -vga std -m 32 -boot order=dc -no-reboot -d int -cdrom ./axel.iso
上記のコマンドでOSバイナリのコンパイルと、qemu上で実行できるようになるはずである.