重力に縋るな

千種夜羽です

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です.

https://github.com/tock/tock

最近は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などはダウンロードして使えるようです.

めっちゃ光ります.

アプリケーションを見てみる

アプリケーションの実装例は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の時間を少し変えてコンパイルし直してみます.

チカチカの間隔が長くなりましたね.ちゃんとビルド・書き込みができているようです.

さて,ここで気になるのがこのアプリケーションがどのようにビルドされているのかということです. ビルド時にアーキテクチャやボードは指定していないですし,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と呼ばれているようです.

https://raw.githubusercontent.com/tock/tock/master/doc/architecture.png

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_MEMORYstatic 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でプログラム書いていきたいですね. やっていくぞ.