Rust製の組み込みOS『Tock』について調べてみた
これは 自作OS Advent Calendar 2019 の9日目の記事です.
はじめに
最近大人気ですよね,Rust. 全人類Rust書いとる...俺は... システムプログラミング言語ですし,LLVMを使っているので色々なアーキテクチャ向けにコンパイルできますし, 安全性を重視しているので,RustでOSが書けるようになると色々と面白そうです.
とはいえ,なんだかんだでちゃんと使ってみようという気になれず,まだあまり書いたことがありません. そこで,今回はRustで書かれたOSであるTockを使って,モダンな言語でのOSの作り方やRustそのものについて調べてみようと思います.
Tockとは
TockはRustで書かれたCortex-MやRISC-Vで動く組み込みOSです.
最近はSTM32でも動くらしいですね.
https://garasubo.github.io/hexo/2019/02/18/tockapp.html
とりあえず動かしてみる
何はともあれ,まずは動かしてみましょう. Getting Started Guideに従います.
まずはRustの環境構築と,tockloaderのインストールです. 実はここでrustupで指定されたバージョンのRustを入れるだけのはずなのにnightlyだとclippyが云々とか言われて激しく躓いたんですが,rustupってそういうもんなんですかね...? よく分からないけど,もうちょっとどうにかならないんだろうか(割と公式っぽいところの記述通りにやってもだめだったりするの初心者殺しでは?).
次に,カーネルをコンパイルします. カーネルのコンパイルはボード毎のディレクトリで行うようです.
$ cd boards/nucleo_f446re/ $ make -n RUSTFLAGS="-C link-arg=-Tlayout.ld -C linker=rust-lld -C linker-flavor=ld.lld -C relocation-model=dynamic-no-pic -C link-arg=-zmax-page-size=512" cargo build --target=thumbv7em-none-eabi --release "/home/sksat/.rustup/toolchains/nightly-2019-10-17-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm"-size target/thumbv7em-none-eabi/release/nucleo_f446re cp target/thumbv7em-none-eabi/release/nucleo_f446re target/thumbv7em-none-eabi/release/nucleo_f446re.elf "/home/sksat/.rustup/toolchains/nightly-2019-10-17-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm"-objcopy --output-target=binary target/thumbv7em-none-eabi/release/nucleo_f446re.elf target/thumbv7em-none-eabi/release/nucleo_f446re.bin $ make Compiling tock-registers v0.3.0 (/home/sksat/github/tock/libraries/tock-register-interface) Compiling tock-cells v0.1.0 (/home/sksat/github/tock/libraries/tock-cells) Compiling enum_primitive v0.1.0 (/home/sksat/github/tock/libraries/enum_primitive) Compiling nucleo_f446re v0.1.0 (/home/sksat/github/tock/boards/nucleo_f446re) Compiling tock_rt0 v0.1.0 (/home/sksat/github/tock/libraries/tock-rt0) Compiling kernel v0.1.0 (/home/sksat/github/tock/kernel) Compiling cortexm v0.1.0 (/home/sksat/github/tock/arch/cortex-m) Compiling capsules v0.1.0 (/home/sksat/github/tock/capsules) Compiling cortexm4 v0.1.0 (/home/sksat/github/tock/arch/cortex-m4) Compiling stm32f4xx v0.1.0 (/home/sksat/github/tock/chips/stm32f4xx) Finished release [optimized + debuginfo] target(s) in 6.91s text data bss dec hex filename 55809 2180 71548 129537 1fa01 target/thumbv7em-none-eabi/release/nucleo_f446re
最近はxargo?とかいうのを使わずともcargoで行けるんですね. ようするにcargoでターゲットを指定してビルドして,生成されたELFファイルをllvm-objcopyで素のバイナリにしているだけですね. また,cargoの出力からアーキテクチャ依存部分,ボードやSoC,ランタイム,Capsule,カーネル本体がRustのライブラリとして実装されていることも分かりました.
コンパイルができたので,まずはこのカーネルを手持ちのボードに書き込んでみます. 今回はSTM32F446を使いました(買ってくれた@tnishinagaさんありがとうございます). ...と思ったのですが,何故か書き込みができなくなってしまった(昨日はできたんだけどなあ...)ので, これ以降はnRF52840-DKを使います.申し訳...
(挙動からしてケーブルの接触不良感がするんですが諸事情により今使えるUSB mini Bなケーブルが1本しかないんですよね.マイコン弄りには致命的です.)
$ make flash Finished release [optimized + debuginfo] target(s) in 0.00s text data bss dec hex filename 94208 2464 259680 356352 57000 target/thumbv7em-none-eabi/release/nrf52840dk tockloader flash --address 0x00000 --jlink --board nrf52dk target/thumbv7em-none-eabi/release/nrf52840dk.bin Flashing binar(y|ies) to board... Using known arch and jtag-device for known board nrf52dk Finished in 0.560 seconds
書き込めたようです.
カーネルが書き込めたので,次はアプリケーションを動かしてみましょう(Tockではカーネルとアプリケーションは別に突っ込むみたいです). アプリケーションの書き込みには,専用ツールのtockloaderを使います. tockloaderはPython3で書かれています. 3で良かった...こういうのがPython2なのってたまによくあるけどめっちゃイラッとするんですよね.2019年も終わりますからね. Python2は速やかに滅ぼしましょう.
まずは対応しているボードを確認します.
$ tockloader list-known-boards Known boards: hail, imix, nrf51dk, nrf52dk, launchxl-cc26x2r1, ek-tm4c1294xl
あれ?もしかして結局STM32使えなかったオチ? まあ割と最近追加されたターゲットらしいですし,そのうち実装されるでしょう.
ということで,最初はblink
というアプリケーションを書き込んでみます.
$ tockloader install --port /dev/ttyACM0 --board nrf52dk --jlink blink Could not find TAB named "blink" locally. [0] No [1] Yes Would you like to check the online TAB repository for that app?[0] 1 Installing apps on the board... Using known arch and jtag-device for known board nrf52dk Finished in 2.497 seconds
どうやら,アプリケーションはTAB
という単位で管理されており,オンラインのTABリポジトリに登録されているexampleなどはダウンロードして使えるようです.
めっちゃ光るやん pic.twitter.com/n1jlE1m3Wy
— お前はやがて君になったか?俺は死んだ (@sksat_tty) 2019年12月9日
めっちゃ光ります.
アプリケーションを見てみる
アプリケーションの実装例はC/C++のものとRustのものがあるようです. 正確には,これらはC/C++とRust向けのユーザランドライブラリ群で,examplesにそれを使ったサンプルがあるかんじです. newlibとかluaとか使えるみたいですね.良さそう.
Rustはまだあんまり分かっていないので,ここではlibtock-cを見てみます. まずはblinkです.
#include <led.h> #include <timer.h> int main(void) { int num_leds = led_count(); for (int count = 0; ; count++) { for (int i = 0; i < num_leds; i++) { if (count & (1 << i)) { led_on(i); } else { led_off(i); } } delay_ms(250); } }
とりあえずこれもコンパイルして書き込んでみましょう.
$ git clone https://github.com/tock/libtock-c $ cd libtock-c $ cd examples/blink $ make ~~~libtockのビルド~~~ Application size report for architecture cortex-m0: text data bss dec hex filename 1240 188 352 1780 6f4 build/cortex-m0/cortex-m0.elf Application size report for architecture cortex-m3: text data bss dec hex filename 992 188 352 1532 5fc build/cortex-m3/cortex-m3.elf Application size report for architecture cortex-m4: text data bss dec hex filename 992 188 352 1532 5fc build/cortex-m4/cortex-m4.elf $ tockloader install --port /dev/ttyACM0 --board nrf52dk --jlink build/blink.tab Installing apps on the board... Using known arch and jtag-device for known board nrf52dk Finished in 2.916 seconds
最初と同じように光ったので,delayの時間を少し変えてコンパイルし直してみます.
— お前はやがて君になったか?俺は死んだ (@sksat_tty) 2019年12月9日
チカチカの間隔が長くなりましたね.ちゃんとビルド・書き込みができているようです.
さて,ここで気になるのがこのアプリケーションがどのようにビルドされているのかということです. ビルド時にアーキテクチャやボードは指定していないですし,make時のログを見る限りどうもCortex-M0,3,4向けにそれぞれビルドしているみたいです. ということでbuild/とtabファイルを見てみます.
$ ls build blink.tab cortex-m0/ cortex-m3/ cortex-m4/ $ file build/blink.tab build/blink.tab: POSIX tar archive (GNU) $ tar xvf build/blink.tab metadata.toml cortex-m0.tbf cortex-m0.bin cortex-m3.tbf cortex-m3.bin cortex-m4.tbf cortex-m4.bin $ cat metadata.toml tab-version = 1 name = "blink" only-for-boards = "" build-date = 2019-12-09T10:00:29Z
あっそういうことなの... とりあえず全部ビルドしてtarに突っ込んでるんですね. TABってtar archiveだったのか... つまり,書き込み時にtockloaderがtarを展開してmetadata.tomlを見ていいかんじに察して対応するアプリケーションのバイナリイメージだけを書き込んでいるということです. まあ,カーネルとアプリケーションが分離されているし基本的なアプリケーションを作る時にはアーキテクチャ毎にビルドしちゃって良いというのは分からないでもないんですが, 必要なやつだけビルドしたいなあ感が否めないですね.
さて,次に気になるのはled.h
です.
どのようにアプリケーションからカーネルを叩いているのかです.
libtock/led.c
を見てみると,led_on()
はこんなかんじになっていました.
~~~ int led_count(void) { return command(DRIVER_NUM_LEDS, 0, 0, 0); } int led_on(int led_num) { return command(DRIVER_NUM_LEDS, 1, led_num, 0); } ~~~
どうやらcommand()
がシステムコールラッパー的なやつで,LEDに関する操作は第一引数をDRIVER_NUM_LEDS
にするとよいっぽいですね.
command()
はlibtock/tock.c
で実装されていました.
int command(uint32_t driver, uint32_t command, int data, int arg2) { register uint32_t r0 asm ("r0") = driver; register uint32_t r1 asm ("r1") = command; register uint32_t r2 asm ("r2") = data; register uint32_t r3 asm ("r3") = arg2; register int ret asm ("r0"); asm volatile ( "svc 2" : "=r" (ret) : "r" (r0), "r" (r1), "r" (r2), "r" (r3) : "memory" ); return ret; }
SVC命令で割り込みを発生させてカーネルを呼んでいるみたいですね. r0のdriverがLEDとかButtunとかの大雑把なドライバ(Capusle毎とかなのかな?)の指定に使われていて, そのドライバに対してコマンドと2つの引数を指定できるようです.
ドライバ番号は以下のようになっていました. 結構色々ありますね.
値 | |
---|---|
DRIVER_NUM_ALARM | 0x0 |
DRIVER_NUM_CONSOLE | 0x1 |
DRIVER_NUM_LEDS | 0x00000002 |
DRIVER_NUM_BUTTON | 0x3 |
DRIVER_NUM_ADC | 0x5 |
DRIVER_NUM_DAC | 0x6 |
DRIVER_NUM_ANALOG_COMPARATOR | 0x7 |
DRIVER_NUM_SPI | 0x20001 |
DRIVER_NUM_USB | 0x20005 |
DRIVER_NUM_RNG | 0x40001 |
DRIVER_NUM_CRC | 0x40002 |
DRIVER_NUM_APP_FLASH | 0x50000 |
DRIVER_NUM_NONVOLATILE_STORAGE | 0x50001 |
DRIVER_NUM_SDCARD | 0x50002 |
DRIVER_NUM_TEMPERATURE | 0x60000 |
DRIVER_NUM_HUMIDITY | 0x60001 |
DRIVER_NUM_AMBIENT_LIGHT | 0x60002 |
DRIVER_NUM_NINEDOF | 0x60004 |
DRIVER_NUM_TSL2561 | 0x70000 |
DRIVER_NUM_TMP006 | 0x70001 |
DRIVER_NUM_LPS25HB | 0x70004 |
DRIVER_NUM_LTC294X | 0x80000 |
DRIVER_NUM_MAX17205 | 0x80001 |
DRIVER_NUM_PCA9544A | 0x80002 |
DRIVER_NUM_GPIO_ASYNC | 0x80003 |
DRIVER_NUM_NRF_SERIALIZATION | 0x80004 |
DRIVER_NUM_I2CMASTERSLAVE | 0x80020006 |
次に,せっかくなのでlibtock-rsの方も見てみました. 時間がありませんでした.
ブートシーケンスとアプリケーションの実行
さて,一通り使ってみたところで,Tockがどのように起動するのか調べてみましょう. とはいっても,大体はTock Startupに書いてある通りです.
まず,Tockには.vectors
と.irqs
の2つのテーブルがあります.
Cortex-Mはベクタテーブルのリセットハンドラに関数ポインタを突っ込んでおくだけで,起動(リセット)後即その関数に飛んでくれるお手軽アーキテクチャなので,
この中にあるtock_kernel_reset_handler
が最初に呼ばれる関数というわけです.
また,Rustでは#[link_section=".vectors"]
のように書いておけば変数が置かれるセクションを指定できるようです.素晴らしい.
#[used]
で最適化で消されないようにもできるんですね.
ここで指定されているリセットハンドラはboards/<board>/src/main.rs
にあるようです.ボード毎に実装されているわけですね.
リセットハンドラは以下のような実装になっていました.
#[no_mangle] // これでマングリングしないようにできるっぽい pub unsafe fn reset_handler() { stm32f4xx::init(); // ボード毎の初期化処理 // ペリフェラルとか諸々の初期化 let board_kernel = static_init!(kernel::Kernel, kernel::Kernel::new(&PROCESSES)); let chip = static_init!( stm32f4xx::chip::Stm32f4xx, stm32f4xx::chip::Stm32f4xx::new() ); // Console, LED, Buttunなどのcapsuleの生成 // ボードの構造体 let nucleo_f446re = NucleoF446RE { console: console, ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability), led: led, button: button, alarm: alarm, }; debug!("Initialization complete. Entering main loop"); extern "C" { static _sapps: u8; } kernel::procs::load_processes( board_kernel, chip, &_sapps as *const u8, &mut APP_MEMORY, &mut PROCESSES, FAULT_RESPONSE, &process_management_capability, ); board_kernel.kernel_loop( &nucleo_f446re, chip, Some(&nucleo_f446re.ipc), &main_loop_capability, ); }
最初にボード毎の初期化やペリフェラルの初期化をしていますね.
その後,board_kernel
という変数を定義しています.
これがこのボードにおけるカーネルを表現するための構造体か何かっぽいです.
static_init!()
が何をするマクロなのかは分かりませんが,多分kernel::Kernel
の型の変数なのでしょう.
その後に定義しているchip
はどうやらSoCに載っている諸々が実装されたものっぽいですね.
これはボードとは別にtock/chips/
以下に実装されています.GPIOのレジスタとか,そういうやつです.
その後は,console
, led
, buttun
などの抽象化されたデバイスが定義されています.
どうやらこれらの構造体はTockではCapsule
と呼ばれているようです.
TockのArchitectureを見てみると,カーネルがCore KernelとCapsuleに別れていることが分かります. Core Kernelはできるだけ小さくして,実際のハードウェアアクセスはCapsuleでやる,というかんじなんですかね. また,CapsuleはRustの構造体として実装され,明示的に指定されたリソースにのみアクセスできるので安全!とのことです.
Capsuleを作ったら,ボードの構造体にCapsuleを突っ込んで初期化は終わりです.
初期化が終わったら,kernel::procs::load_processes
でプロセスをロードします.
_sapps
はアプリケーションのバイナリイメージを含むROMの領域のアドレスです.boards/kernel_layout.ld
で定義されています.
kernel::procs::load_processes
の実装を読んでみると,PROCESSESの数だけProcess::create()
でプロセスを生成しているようでした.
あと,多値返却とかもできるんですねRust.
ちなみに,APP_MEMORY
はstatic mut APP_MEMORY: [u8; 65536] = [0; 65536];
となっていたので,アプリケーションは最大64KiBのメモリを使うことができるようです.
これでプロセスができたので,後はboard_kernel.kernel_loop()
でメインループに入っています.
このメインループはtock/kernel/src/sched.rs
で実装されており,
プロセスはスケジューラによってスケジューリングされつつ,process::State::Running
の時に実行されるようです.
おわりに
実はこの記事は2,3日で急いで書いたので今回はここまでとします. ちなみに書いてる途中に良さげな似たような記事を見つけて焦って内容を増やしました. でも,あんまり「Rustでどういうところがうれしいのか」みたいなところまで調べられませんでした. そもそも僕自身ほとんどRust書いたことがなくてなんもわかっとらんというのもあります.
とはいえ,最近はxv6とかLinuxとかのソースコードしか読んでなかったので,「おお〜モダンな言語っぽい書き方だなあ」という気持ちになりました. ボードとかSoCとかもちゃんと分けられていますしね. インターン先でZenで書いてるOSと似たものを感じました.
でも,チラッと見た限りですがポインタ操作とかちょっとめんどくさそうだな〜という気持ちにもなりました. もちろんユーザーアプリケーションを書く時にはあんまりポインタ使うべきではない/使うとしても安全に使えるようにすべき,というのは分かるんですが, ベアメタル環境で動くプログラムだとそうもいかないですよね.ペリフェラル弄ったりとか. 個人的にはそこらへんはZenの方が楽かな,と思いました(別にステマとかじゃないですよ).
あと,色々読んでみてRust結構良い言語だな〜となるやつ(N回目)をやったので, 少しずつRustでプログラム書いていきたいですね. やっていくぞ.