『基礎から学ぶ 組込みRust』を書きました

はじめに

2021年4月20日、『基礎から学ぶ 組込みRust』(C&R研究所) を出版します。Rust の文法から組込み Rust でファームウェアを作成するところまでを、片手で持てる (多分!まだ持ったことないけど!) 1 冊の本にまとめた、喉から手が出るほど (私が) 求めていた書籍です。 先日、無事入稿を果たしたので、今の気持ちを徒然なるまま綴ったのが、このエントリです。

基礎から学ぶ 組込みRust

基礎から学ぶ 組込みRust

一番始めに言いたいこととしては、今現在、「組込み Rust がプロダクションレディか?」、と問われると、「ほとんどのプロジェクトに対してそうではない」というのが私個人の見解です。セーフティクリティカルな分野では、機能安全をはじめとする認証の問題があり、そんなにおいそれとプログラミング言語変更できないことも承知しています。

でも、その上で言いたい!
プログラミング言語は進化していて、Rust は本当に良いプログラミング言語です!私は組込み製品開発で Rust を使いたいと強く望みます。 C/C++ の他にもう1つくらい、選択肢があっても良いと思いませんか?思いますね!思いましょう!
Rust を有力な選択肢とするためにはエコシステムの発展が欠かせません。そのためには関わる人口を増やさねば!と考えたわけです。そんなおり、今回の書籍執筆のお話をいただいて、本当に価値ある書籍を書けるかどうか不安に思いながらも、本書を執筆するにいたりました。

普段から Rust を書いている方は、これを機に組込み Rust にも興味持って頂けると嬉しいです。 本業が組込みの方は、とりあえず仕事で使うかどうかは置いておいて、最近話題になってるけどどんなもんかな、という気持ちで触ってもらえると幸いです。 けっこう既存のドキュメントにはない、コードリーディングとトライ&エラー繰り返して習得したこと、いっぱい書きました。

あと、冒頭でお知らせしておきたいのは、非常に残念なことながら、本書のカバー内容ではプロユースできるレベルには到達できません。そういう期待を持って本書に興味を持ってくださった方は、ごめんなさい!今後にご期待下さい。

私と Rust

私が Rust を学び始めたのは、3年前からで、比較的新参者と言えるでしょう。当初から C/C++ 以外で組込みで使える言語ないかなぁ、というモチベーションでした。 Rust の入門を済ませて、組込み Rust のドキュメント3本 (The Embedded Rust Book, Discovery, Embedonomicon) を夢中で読み、「これはイケる!」という結論になりました。

当時は、今よりさらに組込み Rust やっている人が少なくて、まず仲間を増やすことが第一歩だな、と考えたことを覚えています。 先述の組込み Rust ドキュメントの和訳が存在していなかったため、まずリーチできる日本語の情報源を増やそう、ということで、2018年の年末からドキュメントの和訳を開始しました。おおよそ半年かけて3本のドキュメントを和訳しました。

この頃、ブログでも積極的に組込み Rust のエントリを書き、Interface のイベントやったり、第1回技書博で組込み/ベアメタルRustクックブック出したりしていました。

そして記念すべき、Interface 2020年5月号 に組込み Rust の特集が掲載され、少しずつ活動の芽が出てきている感じがしていました。

その後、私事ですが、次男が産まれたことで少し活動の時間が取りにくくなり、なんやかんやしている間に本書の執筆をすることになりました (雑。

これは内緒なのですが、実践Rustプログラミング入門に組込みの章があって、組込み Rust の認知度が上がるの良いぞぉ、と思う半面、先を越された!と本気で悔しがっておりました。

書籍執筆の経緯

まさかこんなに早く組込み Rust を題材にした書籍を出版する機会が訪れるとは思ってもみませんでした。 現在担当してくださっている方から、Rust の基礎を解説する書籍執筆の依頼を受けました。

この時点で内心ウキウキでしたが、1つものすごい不安がありました。それは、「Rust の基礎を解説する書籍」の執筆依頼だったことです。 正直、組込み Rust という特定分野ですらあやしいのに、Rust の基礎を解説するのに私という人選が良いとは思えなかったわけです。 なので、組込み Rust に関してだったら書けると思うけど、本当に Rust の基礎が必要なら不適任です、という返信をしました。 この時点で、「多分お断りだろうなぁ…」と8割方諦めていました。

先方から、「組込み Rust でええよ」(関西弁ではなかったですが) 、という予想外の返答が返ってきたので、心底驚きました。 この機会を逃すと次がいつになるかわかったものではないな、という打算もあり、Rust の文法から組込み Rust を扱う、という企画で執筆依頼を受けました。

Wio Terminal を選んだ理由

企画の当初から、Rust の文法解説から始めて、最終的にターゲットボードでアプリケーションを作る、という流れで話を進めていました。 そこで、ネックになったのが、ターゲットボード何にするか?です。
Discovery book だと STM32F3Discovery を採用しているのですが、現在だと少し入手性が悪くなっており、ドキュメントの内容終わった後も何して遊んで良いものか、という感想を持っていました。 (Discovery book は micro:bit 使うバージョンへの書き換えが進行中です)

そこで、当面は入手性が良さそうで、遊びがいがあって、安くて (3,000円くらい)、GDB デバッグできるもの、という条件で探していました。 遊びがい、という意味では、画面欲しいよね、と思ってました。やっぱわかりやすく遊べるので。embedded-graphics クレートの存在も知っていたので、対応するドライバがあるやつで探していると、意外と STM32 の評価ボードは高かったりで難航していました…。

当時、まだ共著じゃなかった井田さんに良いボードない?って質問したところ、Wio Terminal を紹介されました。 Wio Terminal は色々デバイスが載っているわりに、4,000円弱で手に入って、ちょっと改造すれば GDB デバッグもできます。 本の内容一通りやった後でも、色々遊べそうということもあり、Wio Terminal をターゲットにすることにしました。

Wio Terminal と Rust でいくなら、井田さんも書こうよ、という話をして、このあたりで引きずり込みました。

f:id:tomo-wait-for-it-yuki:20210329203923p:plain
かにさんを Wio Terminal に描写して遊びます

基礎から学ぶ組込みRust の良いところ

Rust の (最低限の) 文法から、組込み Rust のアプリケーション開発まで1冊の本にまとまっていることです。 この範囲を1冊でカバーしている本は、いまのところないはずです。 特に、embedded-hal トレイトの解説や BSP クレートを使ったアプリケーション作成方法は、あまりまとまった情報がないので、それが書かれているのは1つの特徴です。

ただ、Rust に入門する目的では、本書に掲載している文法の章を読むより、The Rust Programming Language 読んだり、他の入門書読む方が良いです。 またページ数の都合上、ドライバ書いたり、ブートストラップ書いたり、組込み Rust のリアルタイムフレームワーク RTIC を使ったりする内容は書けませんでした (無念じゃー!) 。

もう1つ重要な良いところがあります。 それは、私がしばらくは embedded-hal を取り扱った入門書を書かなくて済むことです! とにかく組込み Rust の認知度をあげるための活動を第一に行っていたわけですが、そのために自分が Rust を書く時間が少なくなっていました。 書籍発売で1つの区切りもでき、今後は自分が何か作りながら、その過程をまとめて公表できればなぁ、と思います。

書籍の想定読者と内容

組込みも Rust もあまり知らない状態からでも読めるようにしたつもりです。 しかし、どちらかの入門が済んでいる方が読みやすいと思います。どちらかというと、Rust の入門が済んでいる方が良いと思います。

C/C++/Rust 未経験者が本書を読んでどう感じるか、は未知数ですが、ちょっと色々話がつながらないところが出てくるのか、と思ってます。

回路図も参照しながら、こうデバイスがつながっているから、実装がこうなっているんだよ、という解説も書きました。 本書を読み終わった後でも、Wio Terminal 上の本書で取り扱っていないデバイス動かしたり、別のボード使うときに雰囲気だけでも読めると捗るはずです。 回路について、詳細な解説はしていません。なので回路図を見たことない方も気負わず読めると思います。

本書内で扱うデバイスは次の通りです。

  • LEDとボタン / GPIO
  • シリアル通信 / UART
  • タイマ / 割り込み
  • ブザー / PWM
  • 光センサ / ADC
  • 加速度センサ / I2C
  • 液晶ディスプレイ / SPI

無線はまだ動かないので、今後にご期待下さい!

サポートサイト

本書のサポートサイトです。本書について質問があれば、Issue にて受け付けています。また、本書で使うハードウェア一覧を掲載していますので、ぜひ発売日までにハードウェアを入手して、本の購入とともに Wio Terminal で遊んでいただければ幸いです。

目次を節レベルまで掲載していますので、ご購入の参考にして下さい。

GitHub - tomoyuki-nakabayashi/Embedded-Rust-from-Basics: 中林智之、井田健太が執筆した『基礎から学ぶ 組込みRust』 (C&R研究所) のサポートサイトです。

Rust serialport お試し①

はじめに

Rust でシリアル通信したくなったので、serialport クレートを使ってみます。

docs.rs

serialport クレート

serialport クレートはクロスプラットフォームで使えるシリアルポートライブラリです。 各プラットフォーム固有の API も提供しています。

using blocking I/O with serial ports

とあるので、async には対応していなさそうです。

環境

open

シリアルポートの open はビルダーで行います。デバイスパスとボーレートを指定して SerialPortBuilder オブジェクトを取得し、シリアルポートの設定を行います。

    let mut port = serialport::new("/dev/ttyUSB0", 115200)
        .stop_bits(serialport::StopBits::One)
        .data_bits(serialport::DataBits::Eight)
        .timeout(Duration::from_millis(10))
        .open()
        .unwrap_or_else(|e| {
            eprintln!("Failed to open \"{}\". Error: {}", "/dev/ttyUSB", e);
            ::std::process::exit(1);
    });

new() は下の通りなので、ストップビットとデータビットはデフォルト値で良いのですが、お試しのために設定しています。

serialport/src/lib.rs
pub fn new<'a>(path: impl Into<std::borrow::Cow<'a, str>>, baud_rate: u32) -> SerialPortBuilder {
    SerialPortBuilder {
        path: path.into().into_owned(),
        baud_rate,
        data_bits: DataBits::Eight,
        flow_control: FlowControl::None,
        parity: Parity::None,
        stop_bits: StopBits::One,
        timeout: Duration::from_millis(0),
    }
}

大体普通のシリアルポート設定が SerialPortBuilder でできます。受信時のタイムアウトtimeout() で設定できます。

serialport/src/lib.rs
    /// Set the amount of time to wait to receive data before timing out
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

open() の代わりに open_native() すると、プラットフォーム固有のシリアルポートオブジェクトが取得できます。

serialport/src/lib.rs
    /// Open a platform-specific interface to the port with the specified settings
    #[cfg(unix)]
    pub fn open_native(self) -> Result<TTYPort> {
        posix::TTYPort::open(&self)
    }

    /// Open a platform-specific interface to the port with the specified settings
    #[cfg(windows)]
    pub fn open_native(self) -> Result<COMPort> {
        windows::COMPort::open(&self)
    }

read

手元のマイコンボードから送信された Hello World を受信してみます (とりあえずエラーは無視しています) 。

    let mut serial_buf: Vec<u8> = vec![0; 1000];
    loop {
        match port.read(serial_buf.as_mut_slice()) {
            Ok(t) => io::stdout().write_all(&serial_buf[..t]).unwrap(),
            Err(_e) => {},
        }
    }
$ cargo run
# マイコンボードをリセットします
hello world

お、ええやん。

knurling-rs のツールお試し Get a grip on bare-metal Rust!

はじめに

この記事は Rust Advent Calendar 2020 12日目の記事です。

github.com

Our mission is to improve the embedded Rust experience. To achieve this, we build and improve tools and create learning materials.

knurling-rs は Ferrous Systems による組込み Rust の開発経験を向上するためのツールや教材を作成するプロジェクトです。 本記事では、knurling-rs で提供されているツールを紹介します。

ターゲットボードにはアプリケーションテンプレートの手順で説明されている nRF52840-DK を使います。

プロジェクト成果物一覧 (2020/12時点)

次の3つのツールが公開されています。

  • probe-run : 組込み Rust のプログラムをネイティブと同じように実行します
  • defmt : 効率的な文字列フォーマットを提供します
  • flip-link : ゼロコストなスタックオーバーフロー保護機能を提供します

probe-run

github.com

Cargo のカスタムランナーで、RTT 経由でプログラムの出力を表示します。 ブレイクポイントでスタックバックトレースを表示してファームウェア実行を終了することができます。

UART を接続して他ターミナルを立ち上げなくても、ファームウェアからの出力を見ることができて、とっても便利です。 バックトレース表示してくれるのも Good です。

defmt

github.com

defmt ("de format", short for "deferred formatting") is a highly efficient logging framework that targets resource-constrained devices, like microcontrollers.

ということで、可能な文字列フォーマットはホスト側で行う仕組みになっており、マイコン側の負担が軽減されています。

flip-link

github.com

スタックオーバーフローを検出してくれます。組込み開発では気づきにくいバグなので、ありがたいですね。 現在は ARM Cortex-M でのテストが実施されています。

GitHub Sponsor でスポンサーになると、公開前のツールや教材をいち早く試すことができます。 現在で言うと、組込み Rust の教材 knurling-books はスポンサーのみが見ることができます。

サンプルアプリケーションを動かしてみる

セットアップ

Linux では libusb と libudev が必要です。

Knurling のツールをセットアップします。

$ cargo install flip-link
$ cargo install probe-run
$ cargo install cargo-generate

sudo なしでデバイスにアクセスできるように、udev ルールを設定しておきます。

/etc/udev/rules.d/99-nrf.rules
# udev rules to allow access to USB devices as a non-root user

# nRF52840 Development Kit
ATTRS{idVendor}=="1366", ATTRS{idProduct}=="1015", TAG+="uaccess"

アプリケーションテンプレートの作成

$ cargo generate \
    --git https://github.com/knurling-rs/app-template \
    --branch main \
    --name my-app

アプリケーションテンプレートの修正

nRF52840-DK 用に修正します。 これも手順通りです。

.cargo/config.toml
-runner = "probe-run --chip $CHIP --defmt"
+runner = "probe-run --chip nRF52840_xxAA --defmt"

デフォルトだと Cortex-M0 のターゲットトリプルが指定されているので、Cortex-M4F をターゲットにする。

.cargo/config.toml
-target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
+# target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
 # target = "thumbv7m-none-eabi"    # Cortex-M3
 # target = "thumbv7em-none-eabi"   # Cortex-M4 and Cortex-M7 (no FPU)
-# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
+target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
Cargo.toml
-# some-hal = "1.2.3"
+nrf52840-hal = "0.11.0"
src/lib.rs
-// use some_hal as _; // memory layout
+use nrf52840_hal as _;

実行

nRF52840-DK を USB ケーブルでホスト PC に接続して、実行します。

$ cargo rb hello
    Finished dev [optimized + debuginfo] target(s) in 0.02s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/hello`
  (HOST) INFO  flashing program (8.14 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  Hello, world!
└─ hello::__cortex_m_rt_main @ src/bin/hello.rs:8
stack backtrace:
   0: __bkpt
   1: my_app::exit
        at src/lib.rs:29
   2: hello::__cortex_m_rt_main
        at src/bin/hello.rs:10
   3: main
        at src/bin/hello.rs:6
   4: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   5: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550

Hello, world! が出力されてスタックトレースが表示されました。

サンプルコードを拝見

サンプルコードの中身を少し見てみます。

まず、src/lib.rs です。 panic-probedefmt で使うグローバルオブジェクトや関数が実装されているようです。

src/lib.rs
#![no_std]

use core::sync::atomic::{AtomicUsize, Ordering};

use defmt_rtt as _; // global logger
use nrf52840_hal as _;

use panic_probe as _;

// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
fn panic() -> ! {
    cortex_m::asm::udf()
}

#[defmt::timestamp]
fn timestamp() -> u64 {
    static COUNT: AtomicUsize = AtomicUsize::new(0);
    // NOTE(no-CAS) `timestamps` runs with interrupts disabled
    let n = COUNT.load(Ordering::Relaxed);
    COUNT.store(n + 1, Ordering::Relaxed);
    n as u64
}

/// Terminates the application and makes `probe-run` exit with exit-code = 0
pub fn exit() -> ! {
    loop {
        cortex_m::asm::bkpt();
    }
}

hello.rs ではログレベル INFO で Hello, world を出力したあとに、my_app::exit() を呼んでいます。 cortex_m::asm::bkpt() の実行でバックトレースを吐き出して、プログラムが停止するようになっているみたいです。

src/bin/hello.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    defmt::info!("Hello, world!");

    my_app::exit()
}

src/bin 下には他にも5つのサンプルがあります。

$  ls
bitfield.rs  format.rs  hello.rs  levels.rs  overflow.rs  panic.rs

panic.rs は、panic 発生時にバックトレースが出力されることが確認できます。

src/bin/panic.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    defmt::info!("main");

    defmt::panic!()
}

実行すると次のようになります。

$ cargo rb panic
   Compiling my-app v0.1.0 (/home/tomoyuki/rust/my-app)
    Finished dev [optimized + debuginfo] target(s) in 0.25s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/panic`
  (HOST) INFO  flashing program (8.22 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  main
└─ panic::__cortex_m_rt_main @ src/bin/panic.rs:8
0.000001 ERROR panicked at 'explicit panic'
└─ panic::__cortex_m_rt_main @ src/bin/panic.rs:10
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/asm.rs:104
   3: _defmt_panic
        at src/lib.rs:15
   4: defmt::export::panic
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/defmt-0.1.3/src/export.rs:204
   5: panic::__cortex_m_rt_main
        at src/bin/panic.rs:10
   6: main
        at src/bin/panic.rs:6
   7: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   8: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550

overflow.rsは、スタックオーバーフローを検出する flip-link crate の動作を確認するものです。 わざと大きなデータを ack 関数の入り口で確保して、ひたすら再帰呼び出しします。

src/bin/overflow.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    ack(10, 10);
    my_app::exit()
}

fn ack(m: u32, n: u32) -> u32 {
    defmt::info!("ack(m={:u32}, n={:u32})", m, n);
    let mut big = [2; 512];
    if m == 0 {
        n + 1
    } else {
        big[100] += 1;
        if n == 0 {
            ack(m - 1, 1)
        } else {
            ack(m - 1, ack(m, n - 1))
        }
    }
}

実行すると次のようになります。

$ cargo rb overflow
    Finished dev [optimized + debuginfo] target(s) in 0.76s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/overflow`
  (HOST) INFO  flashing program (8.67 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  ack(m=10, n=10)
└─ overflow::ack @ src/bin/overflow.rs:13
0.000001 INFO  ack(m=10, n=9)
└─ overflow::ack @ src/bin/overflow.rs:13
// ...
0.004313 INFO  ack(m=1, n=1)
└─ overflow::ack @ src/bin/overflow.rs:13
0.004314 INFO  ack(m=1, n=0)
└─ overflow::ack @ src/bin/overflow.rs:13
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: {"package":"panic-probe","tag":"defmt_error","data":"{:str}{:str}","disambiguator":"18392481814486563737"}
error: the stack appears to be corrupted beyond this point
  (HOST) ERROR the program has overflowed its stack

スタックオーバーフローしたことを最後に教えてくれていますね。

テスト

testsuite 下にテストコードがあります。defmt の assert と assert_eq を使っていることと、#[defmt_test::tests]アトリビュートを tests モジュールに付与していること以外、普通のテストを同じような感じですね。

testsuite/tests/test.rs
#![no_std]
#![no_main]

use my_app as _; // memory layout + panic handler
use defmt::{assert, assert_eq};

// See https://crates.io/crates/defmt-test/0.1.0 for more documentation (e.g. about the 'state'
// feature)
#[defmt_test::tests]
mod tests {
    #[test]
    fn assert_true() {
        assert!(true)
    }

    #[test]
    fn assert_eq() {
        assert_eq!(24, 42, "TODO: write actual tests")
    }
}

次のコマンドで実行できます。ちゃんとアサーションが失敗したときにはバックトレースを出力されています。

$ cargo test -p testsuite
0.000000 INFO  running assert_true ..
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000001 INFO  .. assert_true ok
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000002 INFO  running assert_eq ..
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000003 ERROR panicked at 'assertion failed: `(left == right)`
  left: `24`,
 right: `42`: TODO: write actual tests', testsuite/tests/test.rs:…
└─ panic_probe::print_defmt::print @ /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:140
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/asm.rs:104
   3: rust_begin_unwind
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:75
   4: core::panicking::panic_fmt
        at /rustc/04488afe34512aa4c33566eb16d8c912a3ae04f9/src/libcore/panicking.rs:85
   5: test::tests::assert_eq
        at tests/test.rs:18
   6: main
        at tests/test.rs:9
   7: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   8: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550
error: test failed, to rerun pass '-p testsuite --test test'

素晴らしい。お手軽にベアメタルでもテストが書けますね!

knurling って何?

www.weblio.jp

ハンドルの握り棒や検査具の丸棒など使用中に手や指が滑らないようにするため操作部分に付ける横、または斜めの凹凸をローレットといい、旋盤などで凹凸を付けることをローレット加工、その加工を行うときに使用する工具をローレット工具(またはローレット)という。また、転造法の一種でダイヤモンド形、七子目などの凹凸のあるロール(こま型エ具)を、回転しながら棒材の表面に押し付けてギザギザを付ける加工をいう。

へー!

Cargoでgitレポジトリ内にある一部crateを依存に追加する

はじめに

Cargo で crates.io に登録されていない crate を指定するには、git や path で指定できます。 git のレポジトリに複数の crate が存在する場合には、左辺に依存する crate 名を指定すると、レポジトリ内から該当 crate を依存関係に追加できます。

# atsamd レポジトリ内の wio-terminal crate を依存に追加
wio-terminal = { git = "https://github.com/jessebraham/atsamd.git", branch = "wio-terminal" }

経緯

最近、Wio Terminal x Rust で遊んでいたところ、 Wio Terminal で使用している SoC の HAL crate を開発している atsamd レポジトリに Wio Terminal の BSP を追加するプルリクが出されました。

github.com

fork 先を clone して examples あたりにコードを追加すれば試せるのですが、Cargo.toml で fork 先から追加された wio-terminal crate だけ引っ張ってきてアプリケーション書けないかな、と考えました。

ちなみに、atsamd レポジトリには、各 SoC の PAC (Peripheral Access Crate) や、atsamd-hal crate、各ボードの BSP crate が集められています。

$ tree -L 2
.
├── boards
│   ├── arduino_mkrvidor4000
│   ├── arduino_mkrzero
│   ├── arduino_nano33iot
# ...
│   ├── trellis_m4
│   ├── trinket_m0
│   ├── wio_terminal  # fork 先のこれだけ持ってきたい
│   └── xiao_m0
├── hal
│   ├── Cargo.toml
│   ├── README.md
│   └── src
# ...
├── pac
│   ├── atsamd11c
│   ├── atsamd21e
# ...
│   ├── atsamd51p
│   ├── atsame54p
│   └── README.md
# ...
└── update.sh

Cargo.toml

atsamd レポジトリ内の atsamd51p crate と wio-terminal crate を引っ張ってきたい場合、次のように書くと意図通り動きました。

[dependencies]
atsamd51p = { git = "https://github.com/jessebraham/atsamd.git", branch = "wio-terminal" }
wio-terminal = { git = "https://github.com/jessebraham/atsamd.git", branch = "wio-terminal" }
panic-halt = "0.2"

解説

解説、というか、The Cargo Book を見ると書いてありました。

doc.rust-lang.org

Cargo will fetch the git repository at this location then look for a Cargo.toml for the requested crate anywhere inside the git repository (not necessarily at the root - for example, specifying a member crate name of a workspace and setting git to the repository containing the workspace).

ということで、fetch してきたレポジトリ内から、指定した Cargo.toml を一生懸命探してくれるようです (root ディレクトリに置いてある必要がなく、メンバー crate も指定できる) 。

Rust no-stdのasync完全理解を目指そう!

はじめに

この記事はRust Advent Calendar 2019の17日目として書きました。

組込みRust界の神japaricさんがno-std環境でasyncを使うPoCレポジトリを公開しています。

github.com

理解できるかどうか非常に自信がありませんが、これは見てみるしかありません!

後日正式な記事が書かれるそうなので、それを待ったほうが得策かもしれません!

引用の領域超えている気がしますので、一応ライセンス表記します。 今回解説するレポジトリは、MIT license、もしくは、Apache License, Version 2.0、でライセンスされています。

目次

自分なりのまとめ

  • 組込みのno-std環境で使えるasync-awaitのproof of conceptを紹介するよ (nightlyは必要だけどね!)
  • cooperativeスケジューラ (executor) は割り込みハンドラから完全に隔離するよ
  • なので割り込みハンドラで実行されるリアルタイム性が要求されるコードの予測性を損なわずに、cooperativeなコードを実行できるよ
  • そのために、executor#[global_allocator]とは違う専用のメモリアロケータを使うよ
  • 現在Rustのcollectionはglobal_allocatorを使うようにハードコーディングされているので、collectionを書き直す必要があるよ

つまるところ、async-awaitを使うと、リアルタイムタスクの最悪実行時間が計算しずらくなるため、割り込みコンテキストと完全に分離できるように、async-awaitno-std環境で使えるようにした、という話のようです。 現在のところ、no-std環境はCortex-Mに限定されています。

実装も覗いてみましたが、手続きマクロ以外は割と読めそうな感じです。

ツール

nightlyツールチェインと、Cortex-M3向けのクロスビルド環境が必要です。

$ rustup override set nightly
$ rustup update
$ rustup target add thumbv7-none-eabi

後、実行環境として、qemu-system-armを使います。

$ cargo run --example async-await --features="nightly"

README

まずなにはともあれ、プロジェクトのREADMEを見てみましょう。 と思ったらREADMEだけで1100行あるではありませんか! これは前途多難な予感です…。

Goal

Real Time For the Masses (RTFM) と一緒に使えるリアルタイムアプリケーション向けのcooperativeスケジューラを作ることが目標のようです。 cooperativeスケジューラは最悪実行時間の解析が難しくなります。

Background

Asynchronous code

皆さんご存知のようにRust 1.39 からasync / await の機能が安定化しました。

非同期コードはexecutorによって実行されることを意味します。 executorは標準ライブラリでは提供されていませんが、async-stdtokioといったマルチスレッドexecutor crateがあります。

async fnインスタンスtaskになり、executorがスケジューリング、実行します。

// toolchain: 1.39.0
// async-std = "1.2.0"

use async_std::task;

fn main() {
    // schedule one instance of task `foo` -- nothing is printed at this point
    task::spawn(foo());

    println!("start");
    // start task `bar` and drive it to completion
    // this puts the executor to work
    // it's implementation defined whether `foo` or `bar` runs first or
    // whether `foo` gets to run at all
    task::block_on(bar());
}

async fn foo() {
    println!("foo");
}

async fn bar() {
    println!("bar");
}
$ cargo run
start
foo
bar

サンプルコード内のコメントによると、タスクの実行順序はexecutorの実装依存なのですね。 勉強になります!

executorはタスクを協調的に実行します。 最も単純な場合、.awaitに到達するまでタスクを実行し、実行をブロックする必要がある場合はそのタスクをサスペンドし、別のタスクをresumeします。

Some implementation details

シンタックス的には、ジェネレータは、サスペンションポイント (yield) を含む、クロージャのような (|| { .. }) ものです。

// toolchain: nightly-2019-12-02

use core::{pin::Pin, ops::Generator};

fn main() {
    let mut g = || {
        println!("A");
        yield;
        println!("B");
        yield;
        println!("C");
    };

    let mut state = Pin::new(&mut g).resume();
    println!("{:?}", state);
    state = Pin::new(&mut g).resume();
    println!("{:?}", state);
    state = Pin::new(&mut g).resume();
    println!("{:?}", state);
}

セマンティクス的には、ジェネレータはyield間の状態マシンで、外部からresumeされることで状態遷移します。 ふむふむ。

executorは状態マシンのリストを持っていて、全ての状態マシンが完了になるまで、resumeし続けます。 各状態マシンは異なるサイズで違うコードを実行するので、トレイトオブジェクト (Box<dyn Generator>) としてリストされます。

これはサンプルコードを見ると理解しやすいです。

fn executor(mut tasks: Vec<Pin<Box<dyn Generator<Yield = (), Return = ()>>>>) {
    let mut n = tasks.len();
    while n != 0 {
        for i in (0..n).rev() {
            let state = tasks[i].as_mut().resume();
            if let GeneratorState::Complete(()) = state {
                tasks.swap_remove(i); // done; remove
            }
        }

        n = tasks.len();
    }
}

なるほど。タスク (状態マシン) をVec<Box<dyn Generator>>として受け取り、各タスクのresumeを呼ぶ。 状態が完了になると、Vecからswap_removeする、と。

Idea

アプローチとしては、非同期コードを#[idle]もしくはfn mainに隔離します。 その理由は

  • 協調的タスクには終了しないものと、短期間で終了するもがあるため、終了しない#[idle]タスクでexecutorを動かすのが賢明である
  • executorで必要となる動的メモリ確保は、#[idle]に制限されます。#[idle]内ではリアルタイムでないアロケータを使用し、通常のタスクは動的メモリ確保をしないようにします。アロケータを#[idle]内で排他的に使用することで、mutexなどの排他制御が不要になります。

ふむ、よくわからないので、もう少し先を見てみましょう。

Implementation

実装には2つのコンポーネントがあります。「スレッドモード」アロケータと「スレッドモード」executorです。 「スレッドモード」はARMの「スレッドモード」を意味しています。

アロケータとexecutorは、「スレッドモード」でのみ利用できます。 ARMの「ハンドラモード」ではアロケータとexecutorにアクセスできません。

「ハンドラモード」は主に割り込みや例外を処理するためのモードです。

Cortex-Mで、リセットハンドラは「スレッドモード」で実行されます。

RTFMアプリでは、#[init]#[idle]は「スレッドモード」で実行します。

TM (Thread-Mode) allocator

TMアロケータはseparate allocatorです。ん?どういうことでしょう? #[global_allocator]で定義されているものとは、独立アロケータです。ああ、そういうこと。

理想的には、RFC #1398で提案されているallocator-genericコレクションをAllocトレイトを通じて使えると良いのですが、Allocトレイトは安定化していませんし、allocator-genericコレクションは存在していません。

あー、なるほど。C++のようにカスタムアロケータが設定できるコレクションが提案されているのですね?(要確認) 今のコレクションは、#[global_allocator]を使うようにハードコーディングされています。

TMアロケータはstableで実装できますが、コレクションはそうではないようです。 Rcで使用するcore::intrinsics::abortがunstableであるなどの理由で。 stableではコレクションの型強制もできないようです。Box<impl Generator>は使えないため、Box<dyn Generator>を使います。

// toolchain: 1.39.0

use cortex_m_tm_alloc::allocator;
use tlsf::Tlsf;

#[allocator(lazy)]
static mut A: Tlsf = {
    // `MEMORY` is transformed into `&'static mut [u8; 64]`
    static mut MEMORY: [u8; 64] = [0; 64];

    let mut tlsf = Tlsf::new();
    tlsf.extend(MEMORY);
    tlsf
};

TMアロケータをAという名前で定義します。Aは実行時に[TLSF]アロケータを初期化します。

Aアロケータのハンドラはgetコンストラクタで取得します。 getコンストラクタは、Option<A>を返します。「スレッドモード」で呼び出すとSomeヴァリアントが、「ハンドラモード」で呼び出すとNoneが帰ります。 Aは、CopyAllocトレイトを実装したサイズ0の型です。SendSyncトレイトは実装していないため、インスタンスを割り込み / 例外ハンドラに渡すことができません。

な、なるほど…。

#[entry]
fn main() -> ! {
    hprintln!("before A::get()").ok();
    SCB::set_pendsv();

    if let Some(a) = A::get() {
        hprintln!("after A::get()").ok();
        SCB::set_pendsv();

        // ..
    } else {
        // UNREACHABLE
    }

    // ..
}

#[exception]
fn PendSV() {
    hprintln!("PendSV({:?})", A::get()).ok();
}
$ cargo run
before A::get()
PendSV(None)
after A::get()
PendSV(None)

PendSVハンドラ内では、A::get()してもNoneになっています。 #[entry]内では、Someヴァリアントが得られていますね(2回めのPendSVに突入していることから)。

if let Some(a) = A::get() {
    // ..

    let mut xs: Vec<i32, A> = Vec::new(a);

    for i in 0.. {
        xs.push(i);
        hprintln!("{:?}", xs).ok();
    }
}

一度アロケータインスタンスを取得すれば、コレクションの初期化時にアロケータのコピーを渡すことで、アロケータを使用できます。 アロケータはサイズ0の型なので、スタックサイズは増えません。

if let Some(a) = A::get() {
    // ..

    let mut xs: Vec<i32, A> = Vec::new(a);

    for i in 0.. {
        xs.push(i);
        hprintln!("{:?}", xs).ok();
    }
}

グローバルアロケータと同様に、TMアロケータもOut Of Memoryになる可能性があります。 その場合、#[oom]アトリビュートを使って定義されたOut Of Memoryハンドラが呼ばれます。

#[alloc_oom::oom]
fn oom(layout: Layout) -> ! {
    hprintln!("oom({:?})", layout).ok();
    debug::exit(debug::EXIT_FAILURE);
    loop {}
}
$ cargo run
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
oom(Layout { size_: 32, align_: 4 })

$ echo $?
1

TM (Thread-Mode) executor

TM executorはTMアロケータに依存しています。

// toolchain: nightly-2019-12-02

use cortex_m_tm_alloc::allocator;
use cortex_m_tm_executor::executor;

#[allocator(lazy)]
static mut A: Tlsf = { /* .. */ };

executor!(name = X, allocator = A);

TMアロケータ同様に、TM executorも「スレッドモード」のみでハンドラを取得できます。 getコンストラクタはTM executorとTMアロケータを返します。 TM executorもCopyトレイトを実装しますが、SendSyncは実装しません。

executorはタスクをspawnするのに使います。 spawnは、具体的なジェネレータを受け取り、Box化し、内部キューに格納します。 spawn自体は、ジェネレータ / タスクコードを実行しません! タスクを実行するにはblock_on APIを使います。

ふむ?とりあえずサンプルコードを見てみますか。

|> examples/tasks.rs

#[entry]
fn main() -> ! {
    if let Some((x, _a)) = X::get() {
        x.spawn(move || {
            hprintln!(" A0").ok();
            yield;

            hprintln!(" A1").ok();
            // but of course you can `spawn` a task from a spawned task
            x.spawn(|| {
                hprintln!("  C0").ok();
                yield;

                hprintln!("  C1").ok();
            });
            yield;

            hprintln!(" A2").ok();
            // NOTE return value will be discarded
            42
        });

        let ans = x.block_on(|| {
            hprintln!("B0").ok();
            yield;

            hprintln!("B1").ok();
            yield;

            hprintln!("B2").ok();
            yield;

            hprintln!("B3").ok();

            42
        });

        hprintln!("the answer is {}", ans).ok();
    }

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

上記コードを実行した結果は、次のようになります。

$ cargo run --example tasks --features="nightly"
B0
 A0
B1
 A1
B2
  C0
 A2
B3
the answer is 42

もう一度サンプルコードに戻ってみると、下のジェネレータをexecutor Xspawnした時点では、まだ非同期コードは実行されません。

#[entry]
fn main() -> ! {
    if let Some((x, _a)) = X::get() {
        x.spawn(move || {
            hprintln!(" A0").ok();
            yield;

            hprintln!(" A1").ok();
            // but of course you can `spawn` a task from a spawned task
            x.spawn(|| {
                hprintln!("  C0").ok();
                yield;

                hprintln!("  C1").ok();
            });
            yield;

            hprintln!(" A2").ok();
            // NOTE return value will be discarded
            42
        });

続く、block_onでジェネレータが与えられると、実行を開始します。 B0出力後、yieldすると、先ほどspawnしたタスクに制御が移り、A0が出力されます。

        let ans = x.block_on(|| {
            hprintln!("B0").ok();
            yield;

            hprintln!("B1").ok();
            yield;

            hprintln!("B2").ok();
            yield;

            hprintln!("B3").ok();

            42
        });

        hprintln!("the answer is {}", ans).ok();
    }

A1ではさらにタスクをspawnしていますが、その時点では実行されておらず、B2出力後のyieldで制御が移ってきます。 C0後にyieldすると、A2を出力するコードに制御が移っていますね。

block_onで実行したジェネレータからは、戻り値 (42) を受け取っています。

でもC1が実行されていませんね?

そう、block_onはspawnしたタスク全てが完了になることを保証しません。 単に、引数で渡されたジェネレータが完了になるまで、実行を進めるだけです。

block_onをネストするとデッドロックする可能性があるため、TM executorではネストしたblock_on呼び出しはパニックになるよう、実装されています。

#[r#async] / r#await!

block_onをネストできないとすると、ジェネレータをどうやったら完了状態にできるのでしょうか? r#await!マクロを使います。ジェネレータを返す関数を簡単に書くために#[r#async]アトリビュートもあります。

例として、割り込みハンドラから非同期にデータを受け取りたいとします。#[r#async]を使って次のように書くことができます。 まずは、ジェネレータを簡単に書くためのアトリビュートです。

use core::ops::Generator;

use heapless::{
    spsc::Consumer, // consumer endpoint of a single-producer single-consumer queue
    ArrayLength,
};
use gen_async_await::r#async;

#[r#async]
fn dequeue<T, N>(mut c: Consumer<'static, T, N>) -> (T, Consumer<'static, T, N>)
where
    N: ArrayLength<T>,
{
    loop {
        if let Some(x) = c.dequeue() {
            break (x, c);
        }
        yield
    }
}

// OR you could have written this; both are equivalent
fn dequeue2<T, N>(
    mut c: Consumer<'static, T, N>,
) -> impl Generator<Yield = (), Return = (T, Consumer<'static, T, N>)>
where
    N: ArrayLength<T>,
{
    || loop {
        if let Some(x) = c.dequeue() {
            break (x, c);
        }
        yield
    }
}

dequeueはジェネレータを返す関数で、ジェネレータでは割り込みハンドラのProducerからデータが送られてくるとSomeになるから、そこで値を返して、yieldしています。 んー?ジェネレータが複雑な型になったりすると有り難いのかな…?

アプリケーションは次のように書けます。

#[entry]
fn main() -> ! {
    static mut Q: Queue<i32, consts::U4> = Queue(i::Queue::new());

    let (p, mut c) = Q.split();

    // send the producer to an interrupt handler
    send(p);

    if let Some((x, _a)) = X::get() {
        // task that asynchronously processes items produced by
        // the interrupt handler
        x.spawn(move || loop {
            let ret = r#await!(dequeue(c)); // <- ★
            let item = ret.0;
            c = ret.1;
            // do stuff with `item`
        });

        x.block_on(|| {
            // .. do something else ..
        });
    }

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

うーん、難しい。dequeue(c)はジェネレータを返しているので、x.spawnの引数もジェネレータになると。

std_async::sync::Mutex?

std_asyncでは、2つのタスク間でメモリを共有したい場合、MutexRwLockを使います。 task::spawn APIの引数はSendトレイトを実装したジェネレータです。 std_asyncのジェネレータはマルチスレッドで動作し、並行実行される可能性があるからです。

use async_std::{sync::Mutex, task};

fn main() {
    let shared: &'static Mutex<u128> = Box::leak(Box::new(Mutex::new(0u128)));

    task::spawn(async move {
        let x = shared.lock().await;
        println!("{}", x);
    });

    task::block_on(async move {
        *shared.lock().await += 1;
    });
}

TM executorは必ず同じコンテキストで動作し、タスクは1つずつ順番に実行されます。 そのためspawnの引数はSendを実装する必要がありません。 そのため、タスク間でデータを共有する際、Mutexの代わりに、単にRefCellCellを使うことができます。

あ、そうですね。

use core::cell::RefCell;

#[entry]
fn main() -> ! {
    static mut SHARED: RefCell<u64> = RefCell::new(0);

    if let Some((x, _a)) = X::get() {
        let shared: &'static _ = SHARED;

        x.spawn(move || loop {
            hprintln!("{}", shared.borrow()).ok();
            yield;
        });

        x.block_on(move || {
            *shared.borrow_mut() += 1;
            yield;
            *shared.borrow_mut() += 1;
            yield;
        });
    }

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

うん、これはよくわかる。

実装を覗いてみよう

さて、ここまでがREADMEです (長い…) 。

r#await! / #[r#async]

r#await!マクロは、わりと読めます。

|> gen-async-await/src/lib.rs

/// `e.await` -> `r#await(e)`
// expansion is equivalent to the desugaring of `($g).await` -- see
// rust-lang/rust/src/librustc/hir/lowering/expr.rs (Rust 1.39)
// XXX Does `$g` need to satisfy the `: Unpin` bound? -- I think not because `$g` is drived to
// completion so any self-referential borrow will be over by the time this macro returns control
// back to the caller. This is unlike `futures::select!` which partially polls its input futures.
// Those input futures may be moved around and then passed to a different `select!` call; the move
// can invalidate self-referential borrows so the input future must satisfy `Unpin`
#[macro_export]
macro_rules! r#await {
    ($g:expr) => {
        match $g {
            mut pinned => {
                use core::ops::Generator;
                loop {
                    match unsafe { core::pin::Pin::new_unchecked(&mut pinned).resume() } {
                        core::ops::GeneratorState::Yielded(()) => {}
                        core::ops::GeneratorState::Complete(x) => break x,
                    }
                    yield ()
                }
            }
        }
    };
}

ジェネレータのステートマシンに沿った動作をしているようです。

#[r#async]アトリビュートですが、修行をサボっていたせいで未だに手続きマクロがいまいち読めません! が、関数の戻り値型をimpl Generator型にし、関数内の処理をジェネレータにしているっぽいです。

/// `async fn foo() { .. }` -> `#[r#async] fn foo() { .. }`
// NOTE the built-in `async fn` desugars to a generator and wraps it in a newtype that makes it
// `!Unpin`; this is required because `async fn`s allow self-referential borrows (i.e. `let x = ..;
// let y = &x; f().await; use(y)`). AFAICT, self referential borrows are not possible in generators
// (as of 1.39) so I think we don't need the newtype
#[proc_macro_attribute]
pub fn r#async(args: TokenStream, item: TokenStream) -> TokenStream {
    if !args.is_empty() {
        return parse::Error::new(Span::call_site(), "`#[async]` attribute takes no arguments")
            .to_compile_error()
            .into();
    }

    // snip

    let block = &item.block;
    quote!(
        #(#attrs)*
        #vis fn #ident #generics (
            #inputs
        ) -> impl core::ops::Generator<Yield = (), Return = #output> #(+ #lts)*
        #where_clause
        {
            move || #block
        }
    )
    .into()

collections

初期化時に任意のアロケータが指定できるコレクションです。 例えばVecなら次のような感じです。

|> collections/src/vec.rs

pub struct Vec<T, A>
where
    A: Alloc,
{
    allocator: A,
    cap: usize,
    len: usize,
    ptr: Unique<T>,
}

impl<A, T> Vec<T, A>
where
    A: Alloc,
{
    // `new`で`Alloc`トレイトを実装するAを引数として渡す
    pub fn new(allocator: A) -> Self {
        let cap = if mem::size_of::<T>() == 0 {
            usize::max_value()
        } else {
            0
        };

        Self {
            allocator,
            cap,
            len: 0,
            ptr: Unique::empty(),
        }
    }
    // snip

cortex-m-tm-alloc

Cortex-Mのスレッドモードでのみ使用できる「スレッドモードアロケータ」です。 get()を呼んだ際、スレッドモードならアロケータインスタンスを、そうでなければNoneが得られるのでした。

Cortex-Mを知っていれば、実装は素直です。SCBのICSRの値を読み込んで、スレッドモードならSomeを、そうでなければNoneを返しています。

|> cortex-m-tm-alloc/src/lib.rs

    pub unsafe fn get() -> Option<Self> {
        if cfg!(not(cortex_m)) {
            return None;
        }

        const SCB_ICSR: *const u32 = 0xE000_ED04 as *const u32;

        if SCB_ICSR.read_volatile() as u8 == 0 {
            // Thread mode (i.e. not within an interrupt or exception handler)
            Some(Private {
                _not_send_or_sync: PhantomData,
            })
        } else {
            None
        }
    }

allocatorアトリビュートの実装は歯が立たなかったです。出直します。

cortex-m-tm-executor

Executorの実装を見てみます。

アロケータとタスク配列、実行中かどうかを示すフラグをフィールドに持ちます。

|> cortex-m-tm-executor

pub struct Executor<A>
where
    A: Alloc + Copy,
{
    allocator: A,
    /// Spawned tasks
    tasks: UnsafeCell<Vec<Pin<Task<A>>, A>>,
    running: Cell<bool>,
}

まず、spawnです。 タスクをヒープ領域に作って、タスク配列に追加するだけです。 なので、spawn()を呼んだだけではタスクが実行されなかったわけですね。

impl<A> Executor<A>
where
    A: Alloc + Copy,
{
    // snip
    pub fn spawn<T>(&self, g: impl Generator<Yield = (), Return = T> + 'static) {
        // this alternative to `GenDrop` produces larger heap allocations
        // let g = || drop(r#await!(g));
        let task: Task<A> = Box::new(GenDrop { g }, self.allocator);
        unsafe {
            (*self.tasks.get()).push(task.into());
        }
    }
}

一方、block_onでは、ジェネレータが完了状態になるまで、タスク配列内のタスクを単純に順番に実行していることがわかります。

    pub fn block_on<T>(&self, g: impl Generator<Yield = (), Return = T>) -> T {
        self.running.set(true);

        pin_mut!(g);

        loop {
            // move forward the main task `g`
            if let GeneratorState::Complete(x) = g.as_mut().resume() {
                self.running.set(false);
                break x;
            }

            let n = unsafe { (*self.tasks.get()).len() };
            for i in (0..n).rev() {
                let s = {
                    let task: TaskMut =
                        unsafe { (*self.tasks.get()).get_unchecked_mut(i).as_mut() };
                    task.resume()
                };

                if let GeneratorState::Complete(()) = s {
                    // task completed -- release memory
                    let task = unsafe { (*self.tasks.get()).swap_remove(i) };
                    drop(task);
                }
            }
        }
    }

ジェネレータの実装について少し。 結果を破棄するGenDropジェネレータが実装されています。 resumeするとGeneratorStateが返ります。

GeneratorState::Completeになると、値をdrop()していることがわかります。

impl<G> Generator for GenDrop<G>
where
    G: Generator<Yield = ()>,
{
    type Yield = ();
    type Return = ();

    fn resume(self: Pin<&mut Self>) -> GeneratorState<(), ()> {
        match G::resume(self.g()) {
            GeneratorState::Yielded(()) => GeneratorState::Yielded(()),
            GeneratorState::Complete(x) => {
                drop(x);
                GeneratorState::Complete(())
            }
        }
    }
}

終わりに

完全に理解できなかったァ…。 とは言え、かなり理解は深まりました。

ただ、アプリケーションはともかく、実行エンジン作るのは、かなり大変そうですね…。

技術書典7でRustが関連する本/サークル一覧メモ

はじめに

随時、更新します。抜けや、間違いがあればご連絡下さい。

techbookfest.org

既刊ですが、私も「組込み/ベアメタルRustクックブック」を頒布しますので、よろしくお願いします(宣伝)。

新刊

い35C: esproject(エスプロジェクト)

techbookfest.org

く54D: OtakuAssembly

techbookfest.org

RustでOSやコンテナをネタにした内容が含まれるそうです。

け04D: ヤバイテックトーキョー

techbookfest.org

Writing a (micro)kernel in Rust in 12 days - 2.5th day - by nullpo-head 「Rustでマイクロカーネル書くやつのつづきやります」

こ31D: 井山梃子歴史館

techbookfest.org

Rustを用いてゲームエンジンを作ります.近年話題のEntity Component Systemがどのように実装されるのかを見ていき,またRustでどのようにグラフィックを扱えばよいのかを解説します.

こ32D: Team Jackalope

techbookfest.org

RustではじめるOpenGLSDL + OpenGL + Dear ImGuiによるGUIアプリ。

し41D: ふがふが(フガフガ)

techbookfest.org

  • M5Stackではじめる組み込みRust
  • 【委託】SePIA timers本

Rustの低レイヤ部分に興味がある人

せ34D: 肉と鍋(ニクトナベ)

techbookfest.org

既刊

VxWorksの脆弱性「URGENT11」のテクニカルホワイトペーパーを読む①

はじめに

7月末にVxWorks脆弱性発見が公開されました。

armis.com

日本語関連記事

VxWorksは組込み機器では非常に有名なRTOSで、20億以上のデバイスに搭載されています。 今回の脆弱性URGENT11で影響を受けるデバイスは2億個以上であると報告されています。

脆弱性を報告したARMISが公開している上のページでは、医療機器やルータで任意コードを実行するエクスプロイトデモ動画が掲載されています。

URGENT11では11個の脆弱性が報告されています。Wind Riverセキュリティアドバイザリによると、その中の3つは、CVEのレーティングが9.8と非常に脅威度が高いものとなっています。

URGENT11 テクニカルホワイトペーパー

本記事では、ARMISが公開している脆弱性のテクニカルホワイトペーパーを読み、自分なりにまとめてみます。 今回は、レーティングが9.8のもののうち、リモート (LAN内) から任意コード実行が可能なCVE-2019-12256Stack overflow in the parsing of IPv4 packets’ IP optionsに関する部分を読んでみます。

ホワイトペーパーに掲載されているコードスニペットでは一部解析できない部分があり、コードスニペットの解析では、一部推測が混じっています。

最終的には、「C言語で安全なコード書くの大変!」というお話になります。 ZenやRustのような配列 (またはスライス) が配列要素数情報を持っている言語であれば、(少なくとも)DoSまでは脅威度が下げられたように見えます。

URGETN11のホワイトペーパーではスタックオーバーフローと記載されていますが、スタックバッファオーバーフローなのではないかと疑っています。記事内では元のホワイトペーパーのままスタックオーバーフローとしています。

URGETN11

過去に発見されたTCP/IPスタックの脆弱性と同様のものが、ソースコードがクローズドなRTOSにもないかどうか、を研究しています。 研究にあたっては、ダウンロード可能なファームウェア (デバッグ情報付きのELF) を逆コンパイルしている、とあります。

概要

複数のSRR (Source Record Route) オプションを含む不正なIPパケットを送信すると、攻撃者が意図的にスタックオーバーフローを起こすことができ、任意コードが実行できます。これは適切な長さチェックを行わずに、SRRオプションをスタック上に確保したバッファにコピーすることに起因しています。

バックグラウンド情報

SRR (Source Record Route)

ja.wikipedia.org

ソース・ルーティングとは、ネットワーク通信において、データの送信者が送信先のみでなく中継地点をも指定する経路制御方式のことである。

通ってきたルート情報をオプション内に記録していきます。オプションヘッダが3バイトあり、その後ろに、通ってきたルートのIPv4アドレスが記録されます。

記録できるルート情報は最大9件です。IPv4のオプションフィールドが (固定部分を除き) 最大で40バイトなので、オプションヘッダ (3バイト) + ルート情報 (4バイト×9 = 36バイト) まで、ということなのでしょう。

ICMPエラーパケット

IPパケットを処理している間にエラー状態になった場合 (不正なIPパケットを受け取った場合) 、ICMPエラーパケットを返信します。この場合、不正なIPパケットとは、送信先に到達できないパケットである不正なオプションフィールドを含んでいる、などが挙げられます。

ICMPエラーパケットには、不正なIPパケットのコピーが含まれる場合が多いです。

はい、嫌な予感がしますね!

脆弱性

1つのIPパケットに複数のSRRオプションが含まれている状態は、エラーとして検出されます。しかし、脆弱性のあるVxWorksの不正IPパケット検出ロジックでは、そのエラーを検出する前に別のエラーを検出した場合、複数のSRRオプションが含まれていることを認識しないまま、不正IPパケットをコピーしてICMPエラーパケットを作成します。

ICMPエラーパケットとして確保されているオプションフィールドは、スタックに確保されている40バイトです。この領域に対して、最大で40バイトの大きさになるSRRオプションを複数回コピーしてしまい、スタックオーバーフローが発生します。

コード

ファームウェアのバイナリから逆コンパイルしたコードスニペットがホワイトペーパー内に掲載されています。

不正なIPパケットを受け取った場合、下のipnet_icmp4_send関数からICMPエラーパケットを送信します。

int ipnet_icmp4_send(Ipnet_icmp_param *icmp_param, Ip_bool is_igmp)
{
    Ipnet_icmp_param *icmp_param;
    Ipcom_pkt *failing_pkt;
    struct Ipnet_copyopts_param options_to_copy;
    // スタックに確保された40バイトの配列
    struct Ipnet_ip4_sock_opts opts;
    // ...
    // 問題のオプションをコピーする関数を呼び出す
    ipnet_icmp4_copyopts(icmp_param, &options_to_copy, &opts, &ip4_info);
    // ...
}

Ipnet_ip4_sock_opts構造体の定義が掲載されていないので詳細は不明ですが、ホワイトペーパー内では、これは40バイトの配列である、と解説されています。

int ipnet_icmp4_copyopts(Ipnet_icmp_param *icmp_param,
                         struct Ipnet_copyopts_param *copyopts_param,
                         struct Ipnet_ip4_sock_opts *opts, void *ip4_info)
{
// ...
    while ( 1 ) {
        current_opt = ipnet_ip4_get_ip_opt_next( /* ... */ );
        // ...
            if ( opt_type == 0x83 || opt_type == 0x89 ) {
                // IPオプションがSRR (LSRRもしくはSSRR)
                srr_ptr_offset = 39;
                srr_opt = (srr_opt_t *)&opts->opts[opts->len];
                // ポインタオフセットは最大で39バイト目までしか指さないようにしているが、
                // このオフセットが現在のオプション内で有効であるかどうか、は検証されていない
                if ( (int)current_opt[2] <= 39 )
                    srr_ptr_offset = current_opt[2];
                offset_to_current_route_entry = srr_ptr_offset - 5;
                // ...
                // オプション内に記録されているIPアドレスを逆順に1つずつコピーする。
                while ( offset_to_current_route_entry > 0 ) {
                    memcpy((char *)srr_opt + srr_opt->length, current_route_entry, 4);
                    current_route_entry -= 4;
                    offset_to_current_route_entry -= 4;
                    srr_opt->length += 4;
                }
                // 最新 (自身) のルート情報を追加する
                memcpy((char *)srr_opt + srr_opt->length, &icmp_param->to, 4);
                srr_opt->length += 4;
                total_opts_len = opts->len + srr_opt->length;
            }
        }
    }
...
}

SRRオプションは次のデータ構造になっており、lengthはオプションのバイト数で、pointerroute dataのオフセットです (オプション開始位置から数えるので、4から始まります) 。pointer (コード中のsrr_ptr_offset) はlength以下でなければならないはずですが、上記コードではそれがチェックされていません。

Loose Source and Record Route
+--------+--------+--------+---------//--------+
|    0x83| length | pointer| route data |
+--------+--------+--------+---------//--------+

脆弱性をつくには、次のようなIPオプションフィールドを送信します。

type length pointer type length pointer
0x83 3 0x27 (39) 0x83 3 0x27 (39)

これでルート情報が欠落しているLSRRオプションが2連続で続くことになります。実際にはルート情報はありませんが、pointer0x27 (39)を指しているので、9番目のルート情報が格納されている位置から逆順に、ルート情報をコピーしようとします

このとき、コピー先はmemcpy((char *)srr_opt + srr_opt->length, current_route_entry, 4);から、srr_optです。srr_optは、40バイトの配列であるoptsどこかを指すポインタです。opts->lenを更新するコードが掲載されていないのですが、一番外側のwhileループが回るごとにオプションの長さ分加算されていると推測できます。

srr_opt = (srr_opt_t *)&opts->opts[opts->len];

上述の脆弱性をつくオプションフィールドをwhile文で1つずつ処理し、2つ目のオプションを処理する際には、opts->len340になっていると考えられます (オプションフィールドのlengthで更新していれば3、コピーしたオプションフィールドの長さで更新していれば40) 。そこを起点に、40バイトしか確保していない領域に、さらに最大で40バイトのコピーが発生します。

ここでコピーされる内容は、オプションフィールドに続く任意のデータです

ということで、上位に戻って、ipnet_icmp4_sendoptsを突き抜けてデータを書き込まれ、スタック上のリターンアドレスが、書き込まれた任意コードの先頭アドレスに書き変われば攻撃成功、なはずです。

int ipnet_icmp4_send(Ipnet_icmp_param *icmp_param, Ip_bool is_igmp)
{
    Ipnet_icmp_param *icmp_param;
    Ipcom_pkt *failing_pkt;
    struct Ipnet_copyopts_param options_to_copy;
    // スタックに確保された40バイトの配列
    struct Ipnet_ip4_sock_opts opts;

新しいプログラミング言語なら?

一番の直接的な問題点は、pointerオフセットが有効なlengthの範囲内であるかどうか検証していないこと、でしょう。ここはロジックレベルの実装ミスであり、プログラミング言語レベルでは防ぎようがありません。

int ipnet_icmp4_copyopts( /* ... */ )
{
// ...
                srr_ptr_offset = 39;
                srr_opt = (srr_opt_t *)&opts->opts[opts->len];
                // ポインタオフセットは最大で39バイト目までしか指さないようにしているが、
                // このオフセットが現在のオプション内であるかどうか、は検証されていない
                if ( (int)current_opt[2] <= 39 )
                    srr_ptr_offset = current_opt[2];

最終防衛線は、optsが確保しているメモリの範囲外アクセスを検出することです。

// ...
                srr_opt = (srr_opt_t *)&opts->opts[opts->len];
// ...
                    memcpy((char *)srr_opt + srr_opt->length, current_route_entry, 4);
// ...

ZenやRustのような言語であれば、opts->optssrr_opt配列要素数を型の一部として持つ配列 (へのポインタ) 、もしくは、スライスになります。このような言語であればopts->opts[opts->len]で範囲外アクセスを行った場合や、スライスのコピー操作により範囲外アクセスを防ぐことが可能です。

どちらの言語でもunsafeC言語のmemcpyと同等のコピー関数がありますが、そのような関数を使うと、もちろんC言語と同じ結果になります。

次のコードは、かなり問題を単純化した例をZenで書いたものです。

const std = @import("std");

fn copy_opts(opts: *[]u8) void {
    const length: usize = 38;
    // `opts`の38番目から42番目の要素を指すスライスを取得して、書き換えようとする
    var entry = opts.*[length..length + 4];
    std.mem.copy(u8, entry, [_]u8{ 1, 2, 3, 4 });
}

pub fn main() void {
    var opts: [40]u8 = [_]u8{0} ** 40;
    copy_opts(&opts[0..]);
}

このコードは、パニックで停止します。copy_opts関数の引数optsが40個の要素しか持たないことがわかっているため、それを超える範囲のスライスを作ろうとすると、実行時にパニックが発生します。

index out of bounds
main.zen:5:23: 0x2250b6 in copy_opts (main)
    var entry = opts.*[length..length + 4];
                      ^

お次はRustで書いたものです。

fn copy_opts(opts: &mut [u8]) {
    let length: usize = 38;
    // `opts`の38番目から42番目の要素を指すスライスを取得して、書き換えようとする
    let entry = &mut opts[length..length+4];
    entry.copy_from_slice(&[1, 2, 3, 4]);
}

fn main() {
    let mut opts: [u8; 40] = [0; 40];
    copy_opts(&mut opts);
}

このコードも、パニックで停止します。

thread 'main' panicked at 'index 42 out of range for slice of length 40', src/libcore/slice/mod.rs:2555:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

パニック発生時にパニックを捕捉してハンドリングすることも可能です。

マルウェアを送り込まれた上で動作し続けることに比べると、システムが定義されたパニック状態に陥る方が、かなり被害が軽減できるでしょう。 このような最終防衛線がプログラミング言語レベルで用意されていることが、いかに大切かわかりますね!