The embedonomicon

embedonomiconは、#![no_std]アプリケーションをスクラッチから作成するプロセスと、 Cortex-Mマイクロコントローラ向けにアーキテクチャ固有の機能を作るイテレーティブなプロセスを案内します。

目的

本書を読むことで、次のことを学べます。

  • #[no_std]アプリケーションのビルド方法。これは、#![no_std]ライブラリをビルドするより、はるかに複雑です。 なぜなら、ターゲットシステムではOSが動いていないからです(もしくは、OSを作ろうとしているかもしれません!)。 そして、プログラムは、ターゲット上で動作する唯一(もしくは、最初の1つ)のプロセスになります。 この場合、プログラムは、ターゲットシステム向けにカスタマイズが必要です。
  • Rustプログラムのメモリレイアウトを細かく制御するためのコツ。 リンカ、リンカスクリプト、および、RustプログラムのABIの一部を制御できるようにするRustの機能について学びます。
  • (実行時にコストがかからない)静的オーバーライド可能なデフォルト機能を実装する秘訣。

対象読者

本書は主に、2つの読者を対象としています。

  • エコシステムがまだサポートしていないアーキテクチャ(例えば、Rust 1.28におけるCortex-R)や、 Rustがサポートを始めたばかりのアーキテクチャ(例えば、Extensaは将来サポートされるかもしれません)に対して、 ベアメタルでのブートを提供したい人々

要求事項

本書は、自己完結しています。読者は、Cortex-Mアーキテクチャについて詳しかったり、Cortex-Mマイクロコントローラを持っている必要はありません。 本書内の例は、全てQEMU上でテストできます。しかしながら、本書内の例を実行したり調査するため、次のツールをインストールする必要があります。

  • 本書内の全コードは、2018エディションを使います。2018エディションの機能やイディオムを知らない場合は、 エディションガイドを確認して下さい。
  • Rust 1.31以上のツールチェインとARM Cortex-Mコンパイルのサポート
  • ARMエミュレーションをサポートしているQEMU。qemu-system-armがインストールされていなければなりません。
  • ARMサポートのGDB。

設定例

全てのOSに共通する手順です。

$ # Rustツールチェイン
$ # 1からやる場合、https://rustup.rs/からrustupを入手して下さい
$ rustup default stable

$ # ツールチェインは、これより新しくなければなりません
$ rustc -V
rustc 1.31.0 (abe02cefd 2018-12-04)

$ rustup target add thumbv7m-none-eabi

$ # cargo-binutils
$ cargo install cargo-binutils

$ rustup component add llvm-tools-preview

macOS

$ # arm-none-eabi-gdb
$ # 最初に`brew tap Caskroom/tap`を実行しなければならないかもしれません
$ brew cask install gcc-arm-embedded

$ # QEMU
$ brew install qemu

Ubuntu 16.04

$ # arm-none-eabi-gdb
$ sudo apt install gdb-arm-none-eabi

$ # QEMU
$ sudo apt install qemu-system-arm

Ubuntu 18.04 or Debian

$ # gdb-multiarch。gdbを起動する時は、`gdb-multiarch`を使って下さい
$ sudo apt install gdb-multiarch

$ # QEMU
$ sudo apt install qemu-system-arm

Windows

(オプションのステップ)(Ubuntu 18.04でテスト済み)ARMからツールチェイン一式をインストール

  • 最近の2018では、Cortex-Mマイクロコントローラ向けのリンカが、GCCのリンカからLLDに切り替わりました。 gcc-arm-none-eabiはもはや必要ありません。しかし、このツールチェインを使いたい人は、ここから下記の手順でインストールできます。
$ tar xvjf gcc-arm-none-eabi-8-2018-q4-major-linux.tar.bz2
$ mv gcc-arm-none-eabi-<version_downloaded> <your_desired_path> # オプション
$ export PATH=${PATH}:<path_to_arm_none_eabi_folder>/bin # 設定を永続的にするため、この行を.bashrcに追加します。

最小限の#![no_std]プログラム

このセクションでは、コンパイルできる最小限の#![no_std]プログラムを書きます。

#![no_std]はどういう意味でしょうか?

#![no_std]は、クレートレベルのアトリビュートです。これは、このクレートにstdクレートではなくcoreクレートをリンクすることを示します。 しかし、アプリケーションにとって、これは何を意味するのでしょうか?

stdクレートはRustの標準ライブラリです。 標準ライブラリは、プログラムがベアメタルではなく、オペレーティングシステム上で動作することを仮定した機能を、含んでいます。 stdは、オペレーティングシステムが、サーバやデスクトップで使うような汎用オペレーティングシステムであることも仮定します。 この理由から、stdは、スレッド、ファイル、ソケット、ファイルシステム、プロセス、など汎用オペレーティングシステムにある機能に対して標準APIを提供します。

その一方、coreクレートは、stdクレートのサブセットで、プログラムが動作するシステムについて、一切の仮定を置きません。 そのため、coreクレートは、浮動小数点や文字列、スライスのような言語のプリミティブと、 アトミック操作やSIMD命令のようなプロセッサ機能を利用するためのAPIを提供します。 しかし、coreクレートは、ヒープメモリアロケーションやI/Oといったものに対するAPIがありません。

アプリケーションに対しては、stdは単に抽象化されたOS機能へのアクセス方法を提供するだけに留まりません。 stdは、とりわけ、スタックオーバーフロープロテクションの設定、コマンドライン引数の処理、 プログラムのmain関数が呼び出される前のメインスレッド生成、の面倒をみます。 #![no_std]アプリケーションは、これらの標準的なランタイムを持ちません。そのため、必要に応じて、自身のランタイムを初期化しなければなりません。

これらの性質から、#![no_std]アプリケーションは、システム上で動作する最初の / 唯一のコードになれます。 標準のRustアプリケーションでは決して作ることができない、次のようなプログラムを書くことができます。

  • OSのカーネル
  • ファームウェア
  • ブートローダ

コード

この普通でない方法で、コンパイル可能な最小限の#![no_std]プログラムに取り掛かることができます。

$ cargo new --edition 2018 --bin app

$ cd app
$ # main.rsを下記の内容に修正して下さい
$ cat src/main.rs

#![allow(unused)]
#![no_main]
#![no_std]

fn main() {
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}
}

このプログラムは、標準的なRustプログラムでは目にすることがない内容を含んでいます。

#![no_std]アトリビュートについては、既に十分に説明しました。

#![no_main]アトリビュートは、エントリポイントとして標準のmain関数を使わないプログラムであることを意味します。 本書を書いている時点では、Rustのmainインタフェースは、プログラムを実行する環境について、いくつかの仮定を置いています。 例えば、コマンドライン引数が存在していることですが、これは一般的に#![no_std]プログラムにはふさわしくありません。

#[panic_handler]アトリビュートでマーキングされた関数は、パニック発生時の動作を定義します。 ライブラリレベルのパニック(core::panic!)と言語レベルのパニック(範囲外のインデックスアクセス)両方が対象です。

このプログラムは、役に立つものではありません。実際、空のバイナリを生成します。

$ $ `size target/thumbv7m-none-eabi/debug/app`と同じです
$ cargo size --target thumbv7m-none-eabi --bin app
   text	   data	    bss	    dec	    hex	filename
      0	      0	      0	      0	      0	app

リンク前、このクレートはパニックのシンボルを含んでいます。

$ cargo rustc --target thumbv7m-none-eabi -- --emit=obj

$ cargo nm -- target/thumbv7m-none-eabi/debug/deps/app-*.o | grep '[0-9]* [^n] '
00000000 T rust_begin_unwind

しかしながら、これがスタート地点です。次のセクションでは、役に立つものをビルドします。 しかしその前に、Cargo呼び出しごとに--targetフラグを付けなくて良いように、デフォルトビルドターゲットを設定しましょう。

$ mkdir .cargo

$ # .cargo/configが下記内容になるように修正します
$ cat .cargo/config
[build]
target = "thumbv7m-none-eabi"

メモリレイアウト

次のステップは、ターゲットシステムがプログラムを実行できるように、プログラムに正しいメモリレイアウトを持たせることです。 例では、LM3S6965という仮想のCortex-M3マイクロコントローラを取り扱います。 私達のプログラムは、デバイス上で動作する唯一のプロセスになります。そのため、デバイスの初期化も面倒を見る必要があります。

背景となる情報

Cortex-Mデバイスは、コードメモリ領域の開始地点にベクタテーブルがあること、を要求します。 ベクタテーブルはポインタの配列です。最初の2つのポインタは、デバイスが起動するときに必要です。 残りのポインタは例外に関係するもので、今は無視します。

リンカは、プログラムの最終的なメモリレイアウトを決定します。しかし、リンカスクリプトを使うことで、メモリレイアウトを制御できます。 リンカスクリプトによる制御の粒度は、セクションレベルです。セクションは、連続したメモリに置かれるシンボルの集まりです。 ここで、シンボルはデータ(静的変数)か命令(Rustの関数)になります。

全てのシンボルは、コンパイラによって割り当てられた名前を持ちます。Rust 1.28以降では、Rustコンパイラは、 _ZN5krate6module8function17he1dfc17c86fe16daE、のような形式でシンボル名を割り当てます。 このシンボルは、krate::module::function::he1dfc17c86fe16daにデマングルできます。 ここで、krate::module::functionは、関数か変数のパスです。そして、he1dfc17c86fe16daは何らかのハッシュです。 Rustコンパイラは、各シンボルをシンボル固有のセクションに配置します。例えば、上述したシンボルは、 .text._ZN5krate6module8function17he1dfc17c86fe16daEというセクションの配置されます。

コンパイラが生成したシンボル名とセクション名は、Rustコンパイラのリリースごとに変わる可能性があります。 しかし、次のアトリビュートを使って、シンボル名やセクション配置を制御することができます。

  • #[export_name = "foo"]は、シンボル名をfooに設定します。
  • #[no_mangle]は、関数名や変数名を(フルパスではなく)シンボル名として使うことを意味します。#[no_mangle] fn bar()は、barというシンボル名を生成します。
  • #[link_section = ".bar"]は、シンボルを.barという名前のセクションに配置します。

これらのアトリビュートにより、プログラムの安定的なABIを公開することができ、リンカスクリプトで利用することができます。

Rust側

上述の通り、Cortex-Mデバイスに対して、ベクタテーブルの最初の2つのエントリを配置する必要があります。 1つ目は、スタックポインタの初期値で、リンカスクリプトだけを使って配置することができます。 2つ目のリセットベクタは、Rustのコードを作成する必要があり、リンカスクリプトを使って正しく配置しなければなりません。

リセットベクタは、リセットハンドラのポインタです。リセットハンドラは、デバイスがシステムリセットの後、もしくは、 最初に電源が入った後に実行する関数です。リセットハンドラは、常にハードウェアコールスタックの最初のスタックフレームになります。 戻るためのスタックフレームがないため、リセットハンドラから戻ることは、未定義動作です。 発散関数のマーキングを行うことで、リセットハンドラが決して戻らないように強制できます。 発散関数は、fn(/* .. */) -> !というシグネチャがついた関数です。


#![allow(unused)]
fn main() {
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // 戻れないため、ここで無限ループに入ります
    loop {}
}

// リセットベクタは、リセットハンドラへのポインタです
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

}

ここで、ハードウェアは、特定の形式を期待しています。これに従うため、extern "C"を使うことで、コンパイラがこの関数をC ABIを使うように指示します。 そうしなければ、安定していないRust ABIが使われます。

リンカスクリプトからリセットハンドラとリセットベクタを参照するために、#[no_mangle]を使って安定したシンボル名を与えます。 RESET_VECTORの位置を細かく制御しなければなりません。そこで、.vector_table.reset_vectorと呼ばれるセクションに配置します。 リセットハンドラであるReset自身の正確な位置は重要ではありません。これに対しては、デフォルトでコンパイラが生成するセクションを使用します。

また、入力のオブジェクトファイルを解析する間、リンカは、内部シンボルと呼ばれる内部リンケージのシンボルを無視します。 そこで、2つのシンボルが外部リンケージを持つようにする必要があります。Rustでシンボルを外部向けにする唯一の方法は、 関連するアイテムをpublic (pub) にして、到達可能(アイテムとクレートのトップレベルとの間にプライベートなモジュールがない)なものにすることです。

リンカスクリプト側

下記に、正しい位置にベクタテーブルを配置する最小限のリンカスクリプトを示します。 全体に目を通してみましょう。

$ cat link.x
/* LM3S6965マイクロコントローラのメモリレイアウト */
/* 1K = 1 KiBi = 1024バイト */
MEMORY
{
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

/* エントリポイントはリセットハンドラです */
ENTRY(Reset);

EXTERN(RESET_VECTOR);

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* 1つ目のエントリ。スタックポインタの初期値 */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* 2つ目のエントリ。リセットベクタ */
    KEEP(*(.vector_table.reset_vector));
  } > FLASH

  .text :
  {
    *(.text .text.*);
  } > FLASH

  /DISCARD/ :
  {
    *(.ARM.exidx.*);
  }
}

MEMORY

リンカスクリプトのこのセクションは、ターゲット内のメモリブロックの位置とサイズを記述します。 2つのメモリブロックが定義されています。FLASHRAMです。これらは、ターゲットで利用可能な物理メモリと関連しています。 ここで使用されている値は、LM3S6965マイクロコントローラのものです。

ENTRY

ここでは、リンカにResetというシンボル名を持つリセットハンドラが、プログラムのエントリポイントであることを教えています。 リンカは、不要なセクションを積極的に破棄します。リンカは、エントリポイントと、エントリポイント関数から呼ばれる関数を使用されると考え、 破棄しなくなります。この行がないと、リンカは、Reset関数と、そこから呼ばれる全ての関数を破棄するでしょう。

EXTERN

リンカは怠け者です。エントリポイントから再帰的に参照されるシンボルが全て見つかった時点で、入力オブジェクトファイルの解析を停止します。 EXTERNにより、他の参照されるシンボルが全て見つかった後でも、リンカはEXTERNの引数が見つかるまで探し続けます。 基本、エントリポイントから呼ばれないシンボルが出力バイナリで必要な場合、KEEPと関連付けてEXTERNを使う必要があります。

SECTIONS

ここでは、入力オブジェクトファイル内のセクション(入力セクション)がどのように出力オブジェクトファイルのセクション(出力セクション)に配置されるのか、 もしくは破棄されるのか、を説明します。 2つの出力セクションを定義します。

  .vector_table ORIGIN(FLASH) : { /* .. */ } > FLASH

.vector_tableは、ベクタテーブルを含んでおり、FLASHメモリの開始地点に配置されます。

  .text : { /* .. */ } > FLASH

そして、.textは、プログラムのサブルーチンを含んでおり、FLASHのどこかに配置されます。開始アドレスは指定されていませんが、 リンカは直前の出力セクションである.vector_tableの後ろに、このセクションを配置するでしょう。

出力セクションの.vecotr_tableは、次のものを含んでいます。

    /* 1つ目のエントリ。スタックポインタの初期値 */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

(コール)スタックをRAMの最後に配置します。スタックは、完全な降順です。すなわち、小さいアドレスに向かって伸びます。 そのため、RAMの最後のアドレスをスタックポインタ(SPモード)の初期値として使用します。 このアドレスは、リンカスクリプト内でRAMメモリブロックに入力した情報を使って、計算されます。

    /* 2つ目のエントリ。リセットベクタ */
    KEEP(*(.vector_table.reset_vector));

次に、SPの初期値の直後に.vector_table.reset_vectorと名付けられた全ての入力セクションがリンカによって挿入されるように、KEEPを使います。 RESET_VECTORがこのセクションに配置される唯一のシンボルです。これは、ベクタテーブルの2つ目にRESET_VECTORを配置するのに効率的な方法です。

出力セクションの.textは、次の内容を含んでいます。

    *(.text .text.*);

これは、.text.text.*という名前の入力セクションを全て含んでいます。 リンカが不必要なセクションを破棄しないようにさせるための、KEEPを、ここでは使わないことに留意して下さい。

最後に、破棄用の特別な/DISCARD/セクションを使います。

    *(.ARM.exidx.*);

.ARM.exidx.*という入力セクションを破棄します。これらのセクションは、例外処理に関連したものですが、 パニック時のスタック巻き戻しを行わないのと、これらのセクションはFlashメモリの容量を使うため、単に破棄します。

1つにまとめる

これで、アプリケーションをリンクできます。参考用に、完全なRustプログラムを示します。


#![allow(unused)]
#![no_main]
#![no_std]

fn main() {
use core::panic::PanicInfo;

// リセットハンドラ
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // 戻れないため、ここで無限ループに入ります
    loop {}
}

// リセットベクタは、リセットハンドラへのポインタです
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}
}

私達のリンカスクリプトを使うために、リンカプロセスに手を加えなければなりません。これは、rustc-C link-argフラグを渡すことで達成できます。 しかし、2つのやり方があります。下記のようにcargo-rustcサブコマンドをcargo-buildの代わりに使用することができます。

重要:このコマンドを実行する前に、前回のセクションの最後に追加した.cargo/configファイルがあることを確認して下さい。

$ cargo rustc -- -C link-arg=-Tlink.x

もしくは、.cargo/configにrustflagsを設定し、cargo-buildサブコマンドを使い続けることもできます。 cargo-binutilsとの統合がやりやすいため、2つ目の方法を使います。

# .cargo/configを次の内容で修正します
$ cat .cargo/config
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x"]

[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi]の部分は、このフラグがターゲット向けのクロスコンパイル時のみ有効であることを意味しています。

調査

それでは、望み通りのメモリレイアウトになっているか確認するため、出力バイナリを調査してみましょう。

$ cargo objdump --bin app -- -d -no-show-raw-insn

app:	file format ELF32-arm-little

Disassembly of section .text:
Reset:
	sub	sp, #4
	movs	r0, #42
	str	r0, [sp]
	b	#-2 <Reset+0x8>
	b	#-4 <Reset+0x8>

これは.textセクションの逆アセンブリです。Resetというリセットハンドラが0x8番地に位置していることがわかります。

$ cargo objdump --bin app -- -s -section .vector_table

app:	file format ELF32-arm-little

Contents of section .vector_table:
 0000 00000120 09000000                    ... ....

これは、.vector_tableセクションの内容を示しています。セクションは0x0番地から開始しており、セクションの1つ目のワードは、0x2001_0000であることがわかります (objdumpはリトリエンディアン形式で出力します)。これはSPの初期値で、RAMの最後のアドレスと一致します。 2つ目のワードは0x9です。これは、リセットハンドラのthumbモードアドレスです。 関数がthumbモードで実行される場合、そのアドレスの1ビット目は1に設定されます。

テスト

このプログラムは、有効なLM3S6965プログラムです。このプログラムをテストするため、仮想のマイクロコントローラ(QEMU)で実行できます。

$ # this program will block
$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -gdb tcp::3333 \
      -S \
      -nographic \
      -kernel target/thumbv7m-none-eabi/debug/app
$ # 別ターミナル
$ arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/app
Reading symbols from target/thumbv7m-none-eabi/debug/app...done.

(gdb) target remote :3333
Remote debugging using :3333
Reset () at src/main.rs:8
8       pub unsafe extern "C" fn Reset() -> ! {

(gdb) # SPがベクタテーブルにプログラムした初期値を持っています
(gdb) print/x $sp
$1 = 0x20010000

(gdb) step
9           let _x = 42;

(gdb) step
12          loop {}

(gdb) # 次にスタック変数の`_x`を調査します
(gdb) print _x
$2 = 42

(gdb) print &_x
$3 = (i32 *) 0x2000fffc

(gdb) quit

mainインタフェース

現在、最小限の動くプログラムがあります。しかし、エンドユーザーが安全なプログラムをビルドできるようにパッケージを作る必要があります。 このセクションでは、標準のRustプログラムが使うようなmainインタフェースを実装します。

まず最初に、バイナリクレートをライブラリクレートに変換します。

$ mv src/main.rs src/lib.rs

そして、クレートを「runtime」を意味するrtという名前に変えます。

$ sed -i s/app/rt/ Cargo.toml

$ head -n4 Cargo.toml
[package]
edition = "2018"
name = "rt" # <-
version = "0.1.0"

最初の変更は、リセットハンドから外部のmain関数を呼び出すようにすることです。

$ head -n13 src/lib.rs
#![no_std]

use core::panic::PanicInfo;

// 変更しました!
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    extern "Rust" {
        fn main() -> !;
    }

    main()
}

#![no_main]アトリビュートも取り除いています。このアトリビュートはライブラリクレートには効果がありません。

ここで、直交する疑問が湧きます。rtライブラリは標準のパニック動作を提供すべきでしょうか?それとも、 #[panic_handler]関数を提供せずに、ユーザーがパニック動作を選べるように残しておくべきでしょうか? 本ドキュメントでは、この疑問を深堀りせず、単純化のためにrtクレートにダミーの#[panic_handler]関数を残しておきます。 しかしながら、他の選択肢があることを読者に伝えておきます。

2つ目の変更は、これまでに書いたリンカスクリプトを、アプリケーションクレートに提供することです。 リンカがライブラリサーチパス(-L)とリンカを呼び出したディレクトリから、リンカスクリプトを探すことはご存知でしょう。 アプリケーションクレートがlink.xのコピーを持たなくて済むように、ビルドスクリプトを使ってrtクレートが、 ライブラリサーチパスにリンカスクリプトを置くようにします。

$ # `rt`のルートディレクトリに、以下の内容でbuild.rsを作ります
$ cat build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

fn main() -> Result<(), Box<Error>> {
    // このクレート用のビルドディレクトリです
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // ライブラリサーチパスを追加します
    println!("cargo:rustc-link-search={}", out_dir.display());

    // `link.x`をビルドディレクトリに置きます
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    Ok(())
}

これで、ユーザーはmainシンボルを公開するアプリケーションを書くことができ、rtクレートとリンクすることができます。 rtは、アプリケーションプログラムに正しいメモリレイアウトを提供します。

$ cd ..

$ cargo new --edition 2018 --bin app

$ cd app

$ # Cargo.tomlを`rt`クレートとの依存関係を持つように修正します
$ tail -n2 Cargo.toml
[dependencies]
rt = { path = "../rt" }
$ # デフォルトターゲットとリンカ呼び出しを微調整した設定ファイルをコピーします
$ cp -r ../rt/.cargo .

$ # `main.rs`の内容を下記の通り変更します
$ cat src/main.rs
#![no_std]
#![no_main]

extern crate rt;

#[no_mangle]
pub fn main() -> ! {
    let _x = 42;

    loop {}
}

逆アセンブリの結果は似ていますが、ここではユーザーのmain関数を含んでいます。

$ cargo objdump --bin app -- -d -no-show-raw-insn

app:	file format ELF32-arm-little

Disassembly of section .text:
main:
	sub	sp, #4
	movs	r0, #42
	str	r0, [sp]
	b	#-2 <main+0x8>
	b	#-4 <main+0x8>

Reset:
	bl	#-14
	trap

型安全にする

mainインタフェースは機能しますが、簡単に誤った使い方ができてしまいます。 例えば、ユーザーは発散しない関数としてmain関数を書くかもしれません。その結果、コンパイルエラーは発生しませんが、 未定義動作になるでしょう(コンパイラはプログラムに誤った最適化を行います)。

シンボルインタフェースではなくマクロをユーザーに公開することで、型安全性を追加することができます。 rtクレートに、次のマクロを書くことができます。

$ tail -n12 ../rt/src/lib.rs

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! entry {
    ($path:path) => {
        #[export_name = "main"]
        pub unsafe fn __main() -> ! {
            // 与えられたパスの型チェック
            let f: fn() -> ! = $path;

            f()
        }
    }
}
}

そして、アプリケーション作成者は、このマクロを次のように呼び出します。

$ cat src/main.rs
#![no_std]
#![no_main]

use rt::entry;

entry!(main);

fn main() -> ! {
    let _x = 42;

    loop {}
}

今度は、アプリケーション作成者は、mainのシグネチャをfn()のような発散しない関数に変更すると、 エラーに遭遇するでしょう。

main前の生活

rtは良さそうに見えますが、まだ機能が完全ではありません!rtクレートに対して書かれたアプリケーションは、 static変数や文字列リテラルを使うことができません。rtのリンカスクリプトが、 標準の.bss.data.rodataセクションを定義していないからです。これを直していきましょう!

最初のステップはリンカスクリプトに下記のセクションを定義することです。

$ # ファイルの一部のみを見せます
$ sed -n 25,46p ../rt/link.x
  .text :
  {
    *(.text .text.*);
  } > FLASH

  /* 追加! */
  .rodata :
  {
    *(.rodata .rodata.*);
  } > FLASH

  .bss :
  {
    *(.bss .bss.*);
  } > RAM

  .data :
  {
    *(.data .data.*);
  } > RAM

  /DISCARD/ :

これらは入力セクションを単に再度エスクポートし、各メモリ領域のどこに出力セクションが置かれるかを指定しているだけです。

これらの変更で、下記のプログラムがコンパイル可能になります。

#![no_std]
#![no_main]

use rt::entry;

entry!(main);

static RODATA: &[u8] = b"Hello, world!";
static mut BSS: u8 = 0;
static mut DATA: u16 = 1;

fn main() -> ! {
    let _x = RODATA;
    let _y = unsafe { &BSS };
    let _z = unsafe { &DATA };

    loop {}
}

しかし、実際のハードウェア上でプログラムを実行し、デバッグすると、mainに到達した時点で、 static変数のBSSDATAが、01になっていないことに気づくでしょう。 代わりに、これらの変数はゴミデータを持っています。この問題は、デバイスの電源投入時、RAMがランダムなデータを持つためです。 プログラムをQEMUで実行すると、この現象は観測できません。

実は、プログラムがstatic変数に書き込みを行う前に、その変数を読むことは、未定義動作です。 mainを呼ぶ前に、全てのstatic変数を初期化するように修正しましょう。

RAM初期化のために、リンカスクリプトをさらに微修正しなければなりません。

$ # ファイルの一部のみを見せます
$ sed -n 25,52p ../rt/link.x
  .text :
  {
    *(.text .text.*);
  } > FLASH

  /* 変更! */
  .rodata :
  {
    *(.rodata .rodata.*);
  } > FLASH

  .bss :
  {
    _sbss = .;
    *(.bss .bss.*);
    _ebss = .;
  } > RAM

  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))
  {
    _sdata = .;
    *(.data .data.*);
    _edata = .;
  } > RAM

  _sidata = LOADADDR(.data);

  /DISCARD/ :

変更内容を詳細に見ていきましょう。

    _sbss = .;
    _ebss = .;
    _sdata = .;
    _edata = .;

シンボルを.bssセクションと.dataセクションの開始アドレスと終了アドレスに関連付けます。 これらは後ほど、Rustコードで使用します。

  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))

.rodataセクションの終わりに、.dataセクションのロードメモリアドレス(LMA; Load Memory Address)を設定します。 .dataはゼロでない初期値をもったstatic変数が含まれています。.dataセクションの仮想メモリアドレス(VMA; Virtual Memory Address)は、 RAMのどこかにあります。これは、static変数が配置されている場所です。 しかし、これらのstatic変数の初期値は、不揮発性メモリ(Flash)に割り当てられなければなりません。 LMAは、これらの初期値が格納されるFlashの場所を示しています。

  _sidata = LOADADDR(.data);

最後に、.dataセクションのLMAをシンボルに関連付けます。

Rust側では、.bssセクションをゼロクリアし、.dataセクションを初期化します。Rustコードからリンカスクリプトで作成したシンボルを参照できます。 これらのシンボルのアドレス1は、.bssセクションと.dataセクションの境界になります。

リセットハンドラを、次のように更新します。

$ head -n32 ../rt/src/lib.rs
#![no_std]

use core::panic::PanicInfo;
use core::ptr;

#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    // 追加!
    // RAMの初期化
    extern "C" {
        static mut _sbss: u8;
        static mut _ebss: u8;

        static mut _sdata: u8;
        static mut _edata: u8;
        static _sidata: u8;
    }

    let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize;
    ptr::write_bytes(&mut _sbss as *mut u8, 0, count);

    let count = &_edata as *const u8 as usize - &_sdata as *const u8 as usize;
    ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, count);

    // ユーザーエントリポイントを呼び出します
    extern "Rust" {
        fn main() -> !;
    }

    main()
}

これで、未定義動作なしに、エンドユーザーは直接的にも間接的にもstatic変数を使うことができます。

上記のコードでは、メモリ初期化をバイト単位の方法で初期化しています。.bssセクションと.dataセクションを例えば4バイトでアライメントすることが可能です。 このことは、アライメントチェックなしにワード単位の初期化を行うために、Rustコードで使うことができます。 どのようにやるのか興味がある場合、cortex-m-rtクレートをチェックして下さい。

1

ここで使っているリンカスクリプトシンボルのアドレスを使用する必要があるということは、混乱を招きやすく、直感的ではありません。 この奇妙さについての詳細な説明は、ここにあります。

例外処理

「メモリレイアウト」セクションでは、簡単なところから始め、例外処理を省くことにしました。 このセクションでは、例外処理サポートを追加します。stable Rustでコンパル時にオーバーライド可能な振る舞いを実装する例を示します (すなわち、シンボルをウィークにするunstableの#[linkage = "weak"]アトリビュートに頼りません)。

背景となる情報

一言で言えば、例外は、アプリケーションが(主に外部からの)非同期イベントに応答するための、 Cortex-Mや他のアーキテクチャが提供する機構です。最も有名なほとんどの人々が知っているであろう例外の種別は、 古典的な(ハードウェア)割り込みです。

Cortex-Mの例外機能は次のように動きます。 プロセッサが例外の種別に応じたシグナルもしくはイベントを受信すると、 (コールスタックに現在の状態を入れておくことで)現在のサブルーチンの実行を一時停止し、 関連する例外ハンドラ(別のサブルーチン)の実行を新しいスタックフレームで開始します。 例外ハンドラの実行が終了すると(つまり例外ハンドラから戻ると)、プロセッサは一時停止したサブルーチンの実行を再開します。

プロセッサはどのハンドラを実行するか、を決めるためにベクタテーブルを使います。テーブルの各エントリはハンドラへのポインタです。 そして、各エントリは、異なる例外種別に対応しています。例えば、2つ目のエントリはリセットハンドラで、3つ目のエントリは、 NMI(Non Maskable Interrupt)と言った具合です。

これまで述べた通り、プロセッサはベクタテーブルがメモリ内の所定の位置にあることを期待しています。そして、各エントリは、 実行時にプロセッサによって使用される可能性があります。したがって、エントリは必ず値を持たなければなりません。 加えて、rtクレートにはエンドユーザーが各例外ハンドラの動作をカスタマイズできる柔軟さを持たせたいです。 最後に、ベクタテーブルは読み込み専用メモリ、もしくは、変更が容易でないメモリにあるため、ユーザーは実行時ではなく、 静的にハンドラを登録しなければなりません。

これら全ての制約を満たすため、rtクレートのベクタテーブルの全エントリにデフォルト値を割り当てますが、 このデフォルト値は、ユーザーがコンパイル時にオーバーライドできるようにウィーク相当のものにします。

Rust側

これを全て実装できる方法を見ていきましょう。簡単化のために、ベクタテーブルの最初の16エントリだけを扱います。 これらのエントリは、デバイス固有のものではなく、全てのCortex-Mマイクロコントローラ上に同じ機能があります。

まず最初にやることは、rtクレートのコードにベクタ配列(例外ハンドラへのポインタ)を作ることです。

$ sed -n 56,91p ../rt/src/lib.rs

#![allow(unused)]
fn main() {
pub union Vector {
    reserved: u32,
    handler: unsafe extern "C" fn(),
}

extern "C" {
    fn NMI();
    fn HardFault();
    fn MemManage();
    fn BusFault();
    fn UsageFault();
    fn SVCall();
    fn PendSV();
    fn SysTick();
}

#[link_section = ".vector_table.exceptions"]
#[no_mangle]
pub static EXCEPTIONS: [Vector; 14] = [
    Vector { handler: NMI },
    Vector { handler: HardFault },
    Vector { handler: MemManage },
    Vector { handler: BusFault },
    Vector {
        handler: UsageFault,
    },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: SVCall },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: PendSV },
    Vector { handler: SysTick },
];
}

ベクタテーブル内のいくつかのエントリは予約済みです。ARMのドキュメントには、これらのエントリに0を割り当てなければならないと書いてあります。 そこで、ユニオンを使って正確に実装します。 エントリは外部関数として使えるようにしたハンドラを指している必要があります。 これは、エンドユーザーが実際の関数定義を提供するために重要です。

次に、Rustコードにデフォルトの例外ハンドラを定義します。 エンドユーザーによってハンドラが割り当てられない例外は、このデフォルトハンドラを使います。

$ tail -n4 ../rt/src/lib.rs

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn DefaultExceptionHandler() {
    loop {}
}
}

リンカスクリプト側

リンカスクリプト側では、リセットベクタの直後に新しい例外ベクタを配置します。

$ sed -n 12,25p ../rt/link.x
EXTERN(RESET_VECTOR);
EXTERN(EXCEPTIONS); /* <- 追加 */

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* 1つ目のエントリ。スタックポインタの初期値 */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* 2つ目のエントリ。リセットベクタ */
    KEEP(*(.vector_table.reset_vector));

    /* 続く14エントリは例外ベクタです */
    KEEP(*(.vector_table.exceptions)); /* <- 追加 */
  } > FLASH

rtで未定義のハンドラ(NMIなど)にデフォルト値を与えるため、PROVIDEを使います。

$ tail -n8 ../rt/link.x
PROVIDE(NMI = DefaultExceptionHandler);
PROVIDE(HardFault = DefaultExceptionHandler);
PROVIDE(MemManage = DefaultExceptionHandler);
PROVIDE(BusFault = DefaultExceptionHandler);
PROVIDE(UsageFault = DefaultExceptionHandler);
PROVIDE(SVCall = DefaultExceptionHandler);
PROVIDE(PendSV = DefaultExceptionHandler);
PROVIDE(SysTick = DefaultExceptionHandler);

PROVIDEは、全ての入力オブジェクトファイルを調べた後、=の左辺が未定義のときのみ効果を発揮します。 これは、ユーザーが各例外についてハンドラを実装しなかった場合です。

テスト

これで全てです!これで、rtクレートは例外ハンドラをサポートします。 次のアプリケーションを使って、テストができます。

注記 QEMU上で例外を生成するのは難しいことがわかりました。実際のハードウェアでは、 不正なメモリアドレス(つまりFlashとRAM領域の外側)を読むだけで十分ですが、QEMUは幸運なことにこの操作を受け付け、ゼロを返します。 トラップ命令はQEMUとハードウェア両方で機能しますが、不運なことにstableのRustでは利用できません。 そのため、今回と次の例を動かすために、一時的にnightlyに切り替える必要があります。

#![feature(core_intrinsics)]
#![no_main]
#![no_std]

use core::intrinsics;

use rt::entry;

entry!(main);

fn main() -> ! {
    // これは未定義命令(UDF)を実行し、HardFault例外を引き起こします
    unsafe { intrinsics::abort() }
}
(gdb) target remote :3333
Remote debugging using :3333
Reset () at ../rt/src/lib.rs:7
7       pub unsafe extern "C" fn Reset() -> ! {

(gdb) b DefaultExceptionHandler
Breakpoint 1 at 0xec: file ../rt/src/lib.rs, line 95.

(gdb) continue
Continuing.

Breakpoint 1, DefaultExceptionHandler ()
    at ../rt/src/lib.rs:95
95          loop {}

(gdb) list
90          Vector { handler: SysTick },
91      ];
92
93      #[no_mangle]
94      pub extern "C" fn DefaultExceptionHandler() {
95          loop {}
96      }

完全を期するため、最適化されたバージョンのプログラムの逆アセンブリを見せます。

$ cargo objdump --bin app --release -- -d -no-show-raw-insn -print-imm-hex

app:	file format ELF32-arm-little

Disassembly of section .text:
main:
	trap
	trap

Reset:
	movw	r1, #0x0
	movw	r0, #0x0
	movt	r1, #0x2000
	movt	r0, #0x2000
	subs	r1, r1, r0
	bl	#0xd2
	movw	r1, #0x0
	movw	r0, #0x0
	movt	r1, #0x2000
	movt	r0, #0x2000
	subs	r2, r1, r0
	movw	r1, #0x0
	movt	r1, #0x0
	bl	#0x8
	bl	#-0x3c
	trap

DefaultExceptionHandler:
	b	#-0x4 <DefaultExceptionHandler>
$ cargo objdump --bin app --release -- -s -j .vector_table

app:	file format ELF32-arm-little

Contents of section .vector_table:
 0000 00000120 45000000 7f000000 7f000000  ... E...........
 0010 7f000000 7f000000 7f000000 00000000  ................
 0020 00000000 00000000 00000000 7f000000  ................
 0030 00000000 00000000 7f000000 7f000000  ................

ベクタテーブルは、この本にあるこれまでのコードスニペット全ての結果を象徴しています。まとめると

  • メモリレイアウトの章の調査セクションで、次のことを学びました。
    • ベクタテーブルの1つ目のエントリは、スタックポインタの初期値です。
    • objdumpは、little endinフォーマットで出力され、スタックは0x2001_0000から始まります。
    • 0x0000_0045番地を指す2つ目のエントリは、リセットハンドラです。
      • リセットハンドラのアドレスは、上の逆アセンブリで0x44であることがわかります。
      • 最初のビットが1に設定されていますが、アライメント要件のため、アドレスは変わりません。代わりに、thumbモードで関数が実行されるようになります。
  • その後は、0x7f0x00が交互に現れるアドレスのパターンが見えます。
    • 上の逆アセンブリを見ると、0x7fDefaultExceptionHandler0x7eがthumbモードで実行される)を参照しているのは明らかです。
    • この章の前半で設定したベクタテーブルへのパターン(pub static EXCEPTIONSの定義を見て下さい)とCortex-Mのベクタテーブルレイアウトとを相互参照すると、DefaultExceptionHandlerのアドレスがテーブル内の各ハンドラエントリにあることが明らかです。
    • 次に、Rustコードのベクタテーブルのデータ構造のレイアウトが予約済みスロットも含めて、Cortex-Mベクタテーブルにアライメントされていることも見ることができます。そのため。全ての予約済みスロットは、正しくゼロに設定されています。

ハンドラのオーバーライド

例外ハンドラをオーバーライドするため、ユーザーはEXCEPTIONSで使った名前と完全に一致するシンボルの関数を提供しなければなりません。

#![feature(core_intrinsics)]
#![no_main]
#![no_std]

use core::intrinsics;

use rt::entry;

entry!(main);

fn main() -> ! {
    unsafe { intrinsics::abort() }
}

#[no_mangle]
pub extern "C" fn HardFault() -> ! {
    // ここで何か面白いことをやります
    loop {}
}

QEMUでテストできます。

(gdb) target remote :3333
Remote debugging using :3333
Reset () at /home/japaric/rust/embedonomicon/ci/exceptions/rt/src/lib.rs:7
7       pub unsafe extern "C" fn Reset() -> ! {

(gdb) b HardFault
Breakpoint 1 at 0x44: file src/main.rs, line 18.

(gdb) continue
Continuing.

Breakpoint 1, HardFault () at src/main.rs:18
18          loop {}

(gdb) list
13      }
14
15      #[no_mangle]
16      pub extern "C" fn HardFault() -> ! {
17          // ここで何か面白いことをして下さい
18          loop {}
19      }

今回は、プログラムは、rtクレートのDefaultExceptionHandlerではなく、ユーザーが定義したHardFault関数を実行します。

mainインタフェースでの最初の試みのように、最初の実装は型安全でないという問題があります。 簡単に、例外の名前を間違ってしまいますが、エラーも警告も発しません。 代わりに、ユーザー定義のハンドラは単に無視されます。 これらの問題は、cortex-m-rt v0.5.xのexception!マクロか、cortex-m-rt v0.6.x.のexceptionアトリビュートにより解決できます。

stableでのアセンブリ

ここまで、デバイスの起動と割り込み処理とを、1行のアセンブリも書くことなくうまくやって来ました。 これはかなりの偉業です!しかし、ターゲットアーキテクチャ次第では、 ここまで到達するためにアセンブリが必要になるかもしれません。 他にも、コンテキストスイッチのようなアセンブリを必要とする操作があります。

問題は、インラインアセンブリ(asm!)も自由形式アセンブリ(global_asm!)もunstableなことです。 そして、これらがいつ安定化されるかは分かっていないため、stableでは使えません。 これから説明するように、いくつかのワークアラウンドがあるため、致命的な問題ではありません。

本セクションの動機付けとして、HardFaultハンドラを、 例外を発生させたスタックフレームの情報を提供するように修正します。

やりたいことは下記の通りです。

ベクタテーブルにユーザーがHardFaultハンドラを直接配置する代わりに、 rtクレートがユーザー定義のHardFaultをトランポリンするハンドラをベクタテーブルに配置します。

$ tail -n36 ../rt/src/lib.rs

#![allow(unused)]
fn main() {
extern "C" {
    fn NMI();
    fn HardFaultTrampoline(); // <- 変更点!
    fn MemManage();
    fn BusFault();
    fn UsageFault();
    fn SVCall();
    fn PendSV();
    fn SysTick();
}

#[link_section = ".vector_table.exceptions"]
#[no_mangle]
pub static EXCEPTIONS: [Vector; 14] = [
    Vector { handler: NMI },
    Vector { handler: HardFaultTrampoline }, // <- 変更点!
    Vector { handler: MemManage },
    Vector { handler: BusFault },
    Vector {
        handler: UsageFault,
    },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: SVCall },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: PendSV },
    Vector { handler: SysTick },
];

#[no_mangle]
pub extern "C" fn DefaultExceptionHandler() {
    loop {}
}
}

このトランポリンはスタックポインタを読んで、ユーザーのHardFaultハンドラを呼びます。 トランポリンはアセンブリで次のように書かなければなりません。

  mrs r0, MSP
  b HardFault

ARM ABIでは、このメインスタックポインタ(MSP; Main Stack Pointer)の設定は、HardFault関数/ルーチンの第一引数になります。 このMSPの値は、例外によってスタックにプッシュされたレジスタへのポインタです。 これらの変更により、ユーザーのHardFaultハンドラは、fn(&StackedRegisters) -> !というシグネチャを持たなければなりません。

.sファイル

stableでアセンブリを書く方法の1つは、アセンブリを外部ファイルに書くことです。

$ cat ../rt/asm.s
  .section .text.HardFaultTrampoline
  .global HardFaultTrampoline
  .thumb_func
HardFaultTrampoline:
  mrs r0, MSP
  b HardFault

そして、rtクレートのビルドスクリプト内で、アセンブリファイルをオブジェクトファイル(.o)にアセンブルし、 アーカイブ(.a)にするために、ccクレートを使います。

$ cat ../rt/build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

use cc::Build;

fn main() -> Result<(), Box<Error>> {
    // このクレートのビルドディレクトリです
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // ライブラリサーチパスを追加します
    println!("cargo:rustc-link-search={}", out_dir.display());

    // `link.x`をビルドディレクトリに置きます
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    // `asm.s`ファイルをアセンブルします
    Build::new().file("asm.s").compile("asm"); // <- 追加!

    Ok(())
}
$ tail -n2 ../rt/Cargo.toml
[build-dependencies]
cc = "1.0.25"

これで全てです!

とても簡単なプログラムを書くだけで、ベクタテーブルがHardFaultTrampolineへのポインタを持つことが確認できます。

#![no_main]
#![no_std]

use rt::entry;

entry!(main);

fn main() -> ! {
    loop {}
}

#[allow(non_snake_case)]
#[no_mangle]
pub fn HardFault(_ef: *const u32) -> ! {
    loop {}
}

逆アセンブリの結果は、以下の通りです。HardFaultTrampolineのアドレスを見て下さい。

$ cargo objdump --bin app --release -- -d -no-show-raw-insn -print-imm-hex

app:	file format ELF32-arm-little

Disassembly of section .text:
HardFault:
	b	#-0x4 <HardFault>

main:
	trap

Reset:
	bl	#-0x6
	trap

DefaultExceptionHandler:
	b	#-0x4 <DefaultExceptionHandler>

UsageFault:
 <unknown>

HardFaultTrampoline:
	mrs	r0, msp
	b	#-0x14 <HardFault>

注記 この逆アセンブリ結果を小さくするために、RAMの初期化をコメントアウトしています。

ここで、ベクタテーブルを見ます。 4つ目のエントリは、HardFaultTrampolineに1を足したアドレスになっているはずです。

$ cargo objdump --bin app --release -- -s -j .vector_table

app:	file format ELF32-arm-little

Contents of section .vector_table:
 0000 00000120 45000000 4b000000 4d000000  ... E...K...M...
 0010 4b000000 4b000000 4b000000 00000000  K...K...K.......
 0020 00000000 00000000 00000000 4b000000  ............K...
 0030 00000000 00000000 4b000000 4b000000  ........K...K...

.o / .aファイル

ccクレートを使う欠点は、ビルドマシンにアセンブラプログラムが必要なことです。 例えば、ARM Cortex-Mをターゲットにする時、ccクレートはアセンブラとしてarm-none-eabi-gccを使います。

ビルドマシン上でファイルをアセンブルする代わりに、rtクレートと一緒にあらかじめアセンブルしたファイルを配布できます。 この方法なら、ビルドマシンにアセンブラプログラムは必要ありません。 しかしながら、rtクレートをパッケージして発行するマシン上には、アセンブラが必要です。

アセンブリファイル(.s)と、コンパイルしたオブジェクトファイル(.o)とは、それほど違いがありません。 アセンブラは最適化を行いません。単純にターゲットアーキテクチャ向けに正しいオブジェクトファイル形式を選ぶだけです。

Cargoは、クレートとアーカイブ(.a)をまとめる機能を提供しています。arコマンドを使ってオブジェクトファイルをアーカイブにパッケージできます。 その後、アーカイブをクレートにまとめます。実は、これはccクレートが行っていることなのです。 ccクレートが呼び出しているコマンドは、targetディレクトリのoutputという名前のファイルを探すと見つかります。

$ grep running $(find target -name output)
running: "arm-none-eabi-gcc" "-O0" "-ffunction-sections" "-fdata-sections" "-fPIC" "-g" "-fno-omit-frame-pointer" "-mthumb" "-march=armv7-m" "-Wall" "-Wextra" "-o" "/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/asm.o" "-c" "asm.s"
running: "ar" "crs" "/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/libasm.a" "/home/japaric/rust-embedded/embedonomicon/ci/asm/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/asm.o"
$ grep cargo $(find target -name output)
cargo:rustc-link-search=/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out
cargo:rustc-link-lib=static=asm
cargo:rustc-link-search=native=/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out

アーカイブを作成するために似たことを行います。

$ # `cc`が使う多くのフラグはアセンブル時には意味がないため、それらは取り除きます
$ arm-none-eabi-as -march=armv7-m asm.s -o asm.o

$ ar crs librt.a asm.o

$ arm-none-eabi-objdump -Cd librt.a
In archive librt.a:

asm.o:     file format elf32-littlearm


Disassembly of section .text.HardFaultTrampoline:

00000000 <HardFaultTrampoline>:
   0:	f3ef 8008 	mrs	r0, MSP
   4:	e7fe      	b.n	0 <HardFault>

次に、rt rlibにアーカイブをまとめるために、ビルドスクリプトを修正します。

$ cat ../rt/build.rs
use std::{
    env,
    error::Error,
    fs::{self, File},
    io::Write,
    path::PathBuf,
};

fn main() -> Result<(), Box<Error>> {
    // このクレートのビルドディレクトリです
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // ライブラリサーチパスを追加します
    println!("cargo:rustc-link-search={}", out_dir.display());

    // `link.x`をビルドディレクトリに置きます
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    // `librt.a`にリンクします
    fs::copy("librt.a", out_dir.join("librt.a"))?; // <- 追加!
    println!("cargo:rustc-link-lib=static=rt"); // <- 追加!

    Ok(())
}

ここで、新バージョンが前のシンプルなプログラムと同じ出力をすることをテストできます。

$ cargo objdump --bin app --release -- -d -no-show-raw-insn -print-imm-hex

app:	file format ELF32-arm-little

Disassembly of section .text:
HardFault:
	b	#-0x4 <HardFault>

main:
	trap

Reset:
	bl	#-0x6
	trap

DefaultExceptionHandler:
	b	#-0x4 <DefaultExceptionHandler>

UsageFault:
 <unknown>

HardFaultTrampoline:
	mrs	r0, msp
	b	#-0x14 <HardFault>

注記 前回同様、逆アセンブリの結果を小さくするために、RAMの初期化をコメントアウトしています。

$ cargo objdump --bin app --release -- -s -j .vector_table

app:	file format ELF32-arm-little

Contents of section .vector_table:
 0000 00000120 45000000 4b000000 4d000000  ... E...K...M...
 0010 4b000000 4b000000 4b000000 00000000  K...K...K.......
 0020 00000000 00000000 00000000 4b000000  ............K...
 0030 00000000 00000000 4b000000 4b000000  ........K...K...

あらかじめアセンブルしたアーカイブを配布する欠点は、最悪の場合、 ライブラリがサポートするターゲットごとにビルド生成物を配布しないといけないことです。

シンボルでのロギング

このセクションでは、極めて軽量なロギングを行うために、シンボルとELFフォーマットを利用する方法をお見せします。

任意のシンボル

クレート間で、安定したシンボルインタフェースが必要な場合、no_mangleアトリビュートを主に使用し、 時には、export_nameアトリビュートを使用します。 export_nameアトリビュートは、シンボル名になる文字列を引数に取ります。 一方、#[no_mangle]は、基本的に#[export_name = <item-name>]のシンタックスシュガーです。

引数に取れる文字列は、1単語の名前だけに限定されないことがわかりました。 例えば、文のような任意の文字列をexport_nameアトリビュートの引数として使うことができます。 少なくても出力形式がELFの場合、nullバイトを含まないものならば何でも構いません。

そのことを確認してみましょう。

$ cargo new --lib foo

$ cat foo/src/lib.rs

#![allow(unused)]
fn main() {
#[export_name = "Hello, world!"]
#[used]
static A: u8 = 0;

#[export_name = "こんにちは"]
#[used]
static B: u8 = 0;
}
$ ( cd foo && cargo nm --lib )
foo-d26a39c34b4e80ce.3lnzqy0jbpxj4pld.rcgu.o:
0000000000000000 r Hello, world!
0000000000000000 V __rustc_debug_gdb_scripts_section__
0000000000000000 r こんにちは

これがどこに繋がるか、わかりますか?

エンコードする

やることは、次の通りです。ログメッセージごとにstatic変数を1つ作りますが、 メッセージをその変数の中に格納せずに、変数のシンボル名にメッセージを格納します。 ログ出力するものは、static変数の内容ではなく、そのアドレスです。

static変数のサイズがゼロでない限り、各変数は異なるアドレスを持ちます。 ここで行うことは、各メッセージを一意の識別子(変数のアドレスになります)に効率的にエンコードすることです。 ログシステムの一部は、この識別子をメッセージにデコードしなければなりません。

このアイデアを実現するコードを書いていきましょう。

この例では、I/Oが必要なため、cortex-m-semihostingクレートを使用します。 セミホスティングは、ターゲットデバイスがホストのI/O機能を借りられるようにするための技術です。 今回の場合、QEMUは細かい設定なしでセミホスティングが使えるため、デバッガは不要です。 実機の場合、シリアルポートのようなI/Oが必要になります。 QEMU上でI/Oを使う最も簡単な方法であるため、今回はセミホスティングを使います。

コードは次のとおりです。

#![no_main]
#![no_std]

use core::fmt::Write;
use cortex_m_semihosting::{debug, hio};

use rt::entry;

entry!(main);

fn main() -> ! {
    let mut hstdout = hio::hstdout().unwrap();

    #[export_name = "Hello, world!"]
    static A: u8 = 0;

    writeln!(hstdout, "{:#x}", &A as *const u8 as usize);

    #[export_name = "Goodbye"]
    static B: u8 = 0;

    writeln!(hstdout, "{:#x}", &B as *const u8 as usize);

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

プログラムがQEMUプロセスを終了できるようにするため、debug::exitも使えるようにしてあります。 QEMUプロセスを手動で終了しなくて良いため、便利です。

そして、こちらはCargo.tomlのdependenciesセクションです。

[dependencies]
cortex-m-semihosting = "0.3.1"
rt = { path = "../rt" }

これでプログラムをビルドできます。

$ cargo build

実行するためには、QEMU起動時に、--semihosting-configフラグを付け加えます。

$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -nographic \
      -semihosting-config enable=on,target=native \
      -kernel target/thumbv7m-none-eabi/debug/app
0x1fe0
0x1fe1

注記 これらのアドレスは、あなたが得たアドレスと異なるかもしれません。 static変数のアドレスは、 ツールチェインが更新された時(例えば、最適化が改善されるかもしれません)に変わる可能性があるからです。

コンソールに2つのアドレスが表示されました。

デコードする

どのようにして、このアドレスを文字列に変換するのでしょうか? 答えはELFファイルのシンボルテーブルです。

$ cargo objdump --bin app -- -t | grep '\.rodata\s*0*1\b'
00001fe1 g       .rodata		 00000001 Goodbye
00001fe0 g       .rodata		 00000001 Hello, world!
$ # 1列目はシンボルのアドレス、最終列はシンボル名です。

objdump -tはシンボルテーブルを表示します。このテーブルは全てのシンボルを含んでいますが、 .rodataセクションの中にある1バイトの大きさ(変数の型はu8です)のアドレスだけを詳しく見ていきます。

プログラムを最適化すると、シンボルのアドレスが変わる可能性があるため、注意して下さい。 確認してみましょう。

専門家によるアドバイス target.thumbv7m-none-eabi.runnerを、 長いQEMUコマンド(qemu-system-arm -cpu (..) -kernel)に設定することができます。 Cargo設定ファイル(.cargo/config)にコマンドを書くことで、 cargo runがそのランナーを使ってバイナリを実行します。

$ head -n2 .cargo/config
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
$ cargo run --release
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/app`
0xb9c
0xb9d
$ cargo objdump --bin app --release -- -t | grep '\.rodata\s*0*1\b'
00000b9d g     O .rodata	00000001 Goodbye
00000b9c g     O .rodata	00000001 Hello, world!

常に実行したELFファイル内の文字列を探すようにして下さい。

もちろん、ELFファイルに含まれるシンボルテーブル(.symtabセクション)を解析するツールを使うことで、 ELFファイル内の文字列を探すプロセスを自動化することが可能です。 そのようなツールを実装することは、本書のスコープ外です。 そのため、読者の演習とします。

ゼロコストにする

より良いものにできるでしょうか?もちろんできます!

現在の実装は、static変数を.rodataに配置しています。これは、その変数の値を決して使わないにも関わらず、 Flashの容量を専有することを意味します。 リンカスクリプトの魔法を少し使うことで、Flashの使用量をゼロにできます。

$ cat log.x
SECTIONS
{
  .log 0 (INFO) : {
    *(.log);
  }
}

static変数を新しい.log出力セクションに配置します。 このリンカスクリプトは、入力オブジェクトファイルの.logセクションにある全てのシンボルを集め、 .log出力セクションに置きます。 このパターンは、メモリレイアウトの章でやりました。

少し新しい部分は、(INFO)の部分です。これは、リンカに、このセクションは割当不可セクションであることを教えます。 割当不可セクションは、ELFバイナリにメタデータとして残りますが、ターゲットデバイスにはロードされません。

また、この出力セクションの開始アドレスを.log 0 (INFO)0に指定しています。

他に改善点は、フォーマットされたI/O(fmt::Write)から、バイナリI/Oに切り替えることです。 つまり、文字列としてではなく、バイトとしてホストにアドレスを送ります。

バイナリシリアライゼーションは、複雑になる可能性がありますが、各アドレスを1バイトとしてシリアライズすることで、 極めて簡潔になります。この方法により、エンディアネスやフレーム化について悩まなくて済みます。 この形式の欠点は、1バイトは256のアドレスしか表現できないことです。

これらの変更を加えましょう。

#![no_main]
#![no_std]

use cortex_m_semihosting::{debug, hio};

use rt::entry;

entry!(main);

fn main() -> ! {
    let mut hstdout = hio::hstdout().unwrap();

    #[export_name = "Hello, world!"]
    #[link_section = ".log"] // <- 追加!
    static A: u8 = 0;

    let address = &A as *const u8 as usize as u8;
    hstdout.write_all(&[address]).unwrap(); // <- 変更!

    #[export_name = "Goodbye"]
    #[link_section = ".log"] // <- 追加!
    static B: u8 = 0;

    let address = &B as *const u8 as usize as u8;
    hstdout.write_all(&[address]).unwrap(); // <- 変更!

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

実行する前に、リンカに渡す引数に-Tlog.xを追加しなければなりません。 Cargo設定ファイルで、追加できます。

$ cat .cargo/config
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
rustflags = [
  "-C", "link-arg=-Tlink.x",
  "-C", "link-arg=-Tlog.x", # <- 追加!
]

[build]
target = "thumbv7m-none-eabi"

これで実行することができます!今回、出力はバイナリ形式であるため、 xxdコマンドにパイプし、16進数の文字列に再変換します。

$ cargo run | xxd -p
0001

アドレスは、0x000x01です。では、シンボルテーブルを見てみましょう。

$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log		 00000001 Goodbye
00000000 g     O .log		 00000001 Hello, world!

目的の文字列があります。今回は、アドレスがゼロから開始していることに気づくでしょう。 これは、.log出力セクションに、開始アドレスを設定したためです。

u8を型として使っているため、各変数は1バイトの大きさです。 もしu16のような型を使った場合、全てのアドレスは偶数になり、全てのアドレス空間(0...255)を、 効率的に利用することができないでしょう。

パッケージする

文字列をログ出力するステップは、常に一緒です。そこで、 クレート内でだけ利用可能なマクロにリファクタリングします。 また、I/O部分をトレイトで抽象化することで、ロギングライブラリをより再利用可能にできます。

$ cargo new --lib log

$ cat log/src/lib.rs

#![allow(unused)]
#![no_std]

fn main() {
pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

#[macro_export]
macro_rules! log {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log"]
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}
}

このライブラリが.logセクションに依存することを考えると、このライブラリがlog.xリンカスクリプトの提供に責任を持つべきです。 それでは、そうしましょう。

$ mv log.x ../log/
$ cat ../log/build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

fn main() -> Result<(), Box<Error>> {
    // リンカスクリプトをリンカが見つけられる場所に置きます
    let out = PathBuf::from(env::var("OUT_DIR")?);

    File::create(out.join("log.x"))?.write_all(include_bytes!("log.x"))?;

    println!("cargo:rustc-link-search={}", out.display());

    Ok(())
}

それでは、log!マクロを使って、アプリケーションをリファクタリングしましょう。

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{log, Log};
use rt::entry;

struct Logger {
    hstdout: HStdout,
}

impl Log for Logger {
    type Error = ();

    fn log(&mut self, address: u8) -> Result<(), ()> {
        self.hstdout.write_all(&[address])
    }
}

entry!(main);

fn main() -> ! {
    let hstdout = hio::hstdout().unwrap();
    let mut logger = Logger { hstdout };

    log!(logger, "Hello, world!");

    log!(logger, "Goodbye");

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

新しいlogクレートへの依存を、Cargo.tomlに追加するのを忘れないようにしましょう。

$ tail -n4 Cargo.toml
[dependencies]
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }
$ cargo run | xxd -p
0001
$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log		 00000001 Goodbye
00000000 g     O .log		 00000001 Hello, world!

以前と同じ出力になりました!

おまけ:複数のログレベル

多くのログフレームワークは、異なるログレベルでメッセージをロギングする方法を提供しています。 これらのログレベルは、メッセージの重要度を告げています。「これはエラーです」、「これはただの警告です」、など。 これらのログレベルは、例えばエラーメッセージを検索する時に、重要でないメッセージを除去するために使用されます。

私達のログライブラリを、フットプリントの増加なしに、ログレベルをサポートするように拡張できます。 やることは、次の通りです。

メッセージ用に、0以上、255以下のフラットなアドレス空間があります。 簡単化のために、エラーメッセージと警告メッセージを区別したいだけ、としましょう。 全てのエラーメッセージをアドレス空間の最初に置き、警告メッセージをエラーメッセージのに置きます。 デコーダが最初の警告メッセージのアドレスを知っていれば、メッセージを分類可能です。 このアイデアは、3つ以上のログレベルをサポートするときに拡張できます。

logマクロを、error!warn!の2つの新しいマクロで置き換えて、このアイデアを試してみましょう。

$ cat ../log/src/lib.rs

#![allow(unused)]
#![no_std]

fn main() {
pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

/// エラーログレベルでメッセージをログ出力します
#[macro_export]
macro_rules! error {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log.error"] // <- 変更!
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}

/// 警告ログレベルでメッセージをログ出力します
#[macro_export]
macro_rules! warn {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log.warning"] // <- 変更!
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}
}

メッセージを異なるリンクセクションに配置することでエラーと警告を区別します。

次にやらなければならないことは、エラーメッセージを警告メッセージの前に配置するように、 リンカスクリプトを更新することです。

$ cat ../log/log.x
SECTIONS
{
  .log 0 (INFO) : {
    *(.log.error);
    __log_warning_start__ = .;
    *(.log.warning);
  }
}

エラーと警告との境界に、__log_warning_start__という名前をつけています。 このシンボルのアドレスは、最初の警告メッセージのアドレスになります。

次に、これらの新しいマクロを使うように、アプリケーションを更新します。

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{error, warn, Log};
use rt::entry;

entry!(main);

fn main() -> ! {
    let hstdout = hio::hstdout().unwrap();
    let mut logger = Logger { hstdout };

    warn!(logger, "Hello, world!"); // <- 変更!

    error!(logger, "Goodbye"); // <- 変更!

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

struct Logger {
    hstdout: HStdout,
}

impl Log for Logger {
    type Error = ();

    fn log(&mut self, address: u8) -> Result<(), ()> {
        self.hstdout.write_all(&[address])
    }
}

出力は、それほど変わりません。

$ cargo run | xxd -p
0100

相変わらず2バイトの出力が得られています。 しかし、警告が最初にログ出力されているにも関わらず、エラーが0番地、警告が1番地になっています。

それでは、シンボルテーブルを見てみます。

$ cargo objdump --bin app -- -t | grep '\.log'
00000000 g     O .log		 00000001 Goodbye
00000001 g     O .log		 00000001 Hello, world!
00000001         .log		 00000000 __log_warning_start__

.logセクション内に__log_warning_start__という追加のシンボルがあります。 このシンボルのアドレスは、最初の警告メッセージのアドレスです。 この値より小さいアドレスを持つシンボルは、エラーになります。 それ以外のシンボルは警告です。

適切なデコーダを使うと、これら全ての情報から、次の人間が読みやすい出力を得ることができます。

WARNING Hello, world!
ERROR Goodbye

このセクションを気に入った場合、stlogログフレームワークを確認して下さい。 このアイデアを完全に実装しています。

グローバルシングルトン

このセクションでは、グローバルに共有されるシングルトンの実装方法を説明します。 The embedded Rust bookは、Rust特有のローカルで所有されるシングルトンを説明しました。 グローバルシングルトンは、本質的にCやC++で見かけるシングルトンパターンです。 これは、組込み開発固有のものではありませんが、シンボルに関係するため、embedonomiconに相応しい内容のように思えます。

TODO(resources team) link "the embedded Rust book" to the singletons section when it's up

グローバルシングルトンを説明するために、このセクションでは、前のセクションで開発したロガーを、 グローバルにログ出力できるように拡張します。 結果は、the embedded Rust bookで説明した#[global_allocator]フィーチャと非常に似たものになります。

TODO(resources team) link #[global_allocator] to the collections chapter of the book when it's in a more stable location.

やりたいことを、下記にまとめます。

前のセクションでは、Logトレイトを実装している特定のロガーを通してログメッセージを出力するために、log!マクロを作りました。 log!マクロのシンタックスは、log!(logger, "String")です。 このマクロを、log!("String")でも動くように拡張します。 loggerなしのバージョンを使うと、グローバルロガーを通してメッセージをログ出力しなければなりません。 これは、std::println!が動作する方法と同じです。 また、何がグローバルロガーか、を宣言するための機構が必要です。 これは、#[global_allocator]と似ている部分です。

グローバルロガーが最上位クレートで宣言される可能性があり、 グローバルロガーの型もまた最上位クレートで定義される可能性があります。 この場合、依存関係から正確なグローバルロガーの型を知ることはできません。 この場合をサポートするために、いくらか間接的な方法が必要になります。

logクレートにグローバルロガーの型をハードコーディングする代わりに、logクレート内で、 グローバルロガーのインタフェースだけを宣言します。 そのインタフェースは、logクレートに新しく追加するGlobalLogというトレイトです。 log!マクロもそのトレイトを使うようにします。

$ cat ../log/src/lib.rs

#![allow(unused)]
#![no_std]

fn main() {
// 追加!
pub trait GlobalLog: Sync {
    fn log(&self, address: u8);
}

pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

#[macro_export]
macro_rules! log {
    // 追加!
    ($string:expr) => {
        unsafe {
            extern "Rust" {
                static LOGGER: &'static dyn $crate::GlobalLog;
            }

            #[export_name = $string]
            #[link_section = ".log"]
            static SYMBOL: u8 = 0;

            $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
        }
    };

    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log"]
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}

// 追加!
#[macro_export]
macro_rules! global_logger {
    ($logger:expr) => {
        #[no_mangle]
        pub static LOGGER: &dyn $crate::GlobalLog = &$logger;
    };
}
}

解説することがたくさんあります。

トレイトから始めましょう。


#![allow(unused)]
fn main() {
pub trait GlobalLog: Sync {
    fn log(&self, address: u8);
}
}

GlobalLogLogとは、logメソッドを持っています。違いは、GlobalLog.logがレシーバの共有参照(&self)を取ることです。 グローバルロガーはstatic変数なので、これが必要です。後ほど、詳しく見ます。

もう1つの違う点は、GlobalLog.logResultを返さないことです。 これは、呼び出し側にエラーを報告できないことを意味します。 これはグローバルシングルトンを実装するトレイトを使うための必要条件ではありません。 グローバルシングルトンでエラー処理をすることは良いことですが、グローバルバージョンのlog!マクロの全てのユーザーが、 エラー型に同意する必要があります。 ここでは、GlobalLog実装者がエラーを処理するようにして、インタフェースを少し簡略化します。

さらに別の違いは、GlobalLogが実装者に、スレッド間で共有できるようにするためのSyncを要求する点です。 これは、static変数内の値への要求です。それらの値の型は、Syncを実装しなければなりません。

現時点では、インタフェースがこのようになっていなければならない理由は、完全には明らかではないかもしれません。 クレートの他の部分を見ることで、より明らかになっていきますので、読み進めて下さい。

次はlog!マクロです。


#![allow(unused)]
fn main() {
    ($string:expr) => {
        unsafe {
            extern "Rust" {
                static LOGGER: &'static dyn $crate::GlobalLog;
            }

            #[export_name = $string]
            #[link_section = ".log"]
            static SYMBOL: u8 = 0;

            $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
        }
    };
}

特定の$loggerなしでマクロを呼び出すと、マクロはメッセージをログ出力するためにLOGGERと呼ばれるextern static変数を使います。 この変数どこかで定義されたグローバルロガーです。そのため、externブロックを使っています。 このパターンはメインインタフェースの章で見ました。

LOGGERの型を宣言する必要があります。そうでなければ、コードは型チェックを行いません。 LOGGERの具体的な型はここではわかりませんが、 その型がGlobalLogトレイトを実装していることを知っています(むしろ必要としています)。 そこで、トレイトオブジェクトを使うことができます。

残りのマクロ拡張は、log!マクロのローカルバージョンの拡張ととてもよく似ています。 そのため、前の章で説明したことは、ここでは説明しません。

ここで、LOGGERがトレイトオブジェクトでなければならないことを知っているので、 GlobalLogで関連型のErrorを除去する理由はより明白です。もし除去しなければ、 LOGGERの型シグネチャの中でErrorの型を1つ選ばなければなりません。 これが先程、「log!マクロの全てのユーザーが、エラー型に同意する必要があります。」と書いた意味です。

そして、最後のピースのglobal_logger!マクロです。 これは、手続きマクロアトリビュートにもできますが、macro_rules!でマクロを書くほうが簡単です。


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! global_logger {
    ($logger:expr) => {
        #[no_mangle]
        pub static LOGGER: &dyn $crate::GlobalLog = &$logger;
    };
}
}

このマクロは、log!が使用するLOGGER変数を作ります。安定したABIインタフェースが必要なので、 no_mangleアトリビュートを使用します。 この方法により、LOGGERのシンボル名はlog!マクロが期待する「LOGGER」になります。

他の重要な点は、このstatic変数の型は、log!マクロの展開で使用される型と正確に一致しなければなりません。 もし一致しない場合、ABIの不一致により、良くないことが起こるでしょう。

新しいグローバルロガーの機能を使う例を書いてみましょう。

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m::interrupt;
use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{global_logger, log, GlobalLog};
use rt::entry;

struct Logger;

global_logger!(Logger);

entry!(main);

fn main() -> ! {
    log!("Hello, world!");

    log!("Goodbye");

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

impl GlobalLog for Logger {
    fn log(&self, address: u8) {
        // `static mut`変数へのアクセスを割り込み安全にするため(これはメモリ安全のために要求されます)、
        // クリティカルセクション(`interrupt::free`)を使います。
        interrupt::free(|_| unsafe {
            static mut HSTDOUT: Option<HStdout> = None;

            // 遅延初期化
            if HSTDOUT.is_none() {
                HSTDOUT = Some(hio::hstdout()?);
            }

            let hstdout = HSTDOUT.as_mut().unwrap();

            hstdout.write_all(&[address])
        }).ok(); // `.ok()` = エラーを無視します
    }
}

TODO(resources team) use cortex_m::Mutex instead of a static mut variable when const fn is stabilized.

依存関係にcortex-mを追加する必要があります。

$ tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }

これは、前のセクションで書いた例を移植したものです。 出力は、以前のものと同じです。

$ cargo run | xxd -p
0001
$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log		 00000001 Goodbye
00000000 g     O .log		 00000001 Hello, world!

このグローバルシングルトンの実装がゼロコストでないことが気になる読者も居るかと思います。 なぜなら、トレイトオブジェクトを使用しており、vtableを参照してメソッド呼び出しを行う動的ディスパッチになるためです。

しかし、LLVMは十分に賢く、この動的ディスパッチをコンパイラの最適化 / LTOで消去してくれます。 このことは、シンボルテーブル内のLOGGERを探すことで確認できます。

$ cargo objdump --bin app --release -- -t | grep LOGGER

もしstaticが見つからない場合、vtableがないことと、 LLVMがLOGGER.logの呼び出しをLogger.logの呼び出しに変換できたことを意味します。

Direct Memory Access (DMA)

このセクションは、DMA転送周りのメモリ安全なAPI構築における主要な要件について、説明します。

DMAペリフェラルは、プロセッサの動作(メインプログラムの実行)と並行してメモリ転送を行うために使用されます。 DMA転送は、memcpyを実行するためにスレッドを生成すること(thread::spawnを参照)とほぼ同等です。 メモリ安全なAPIの要件を説明するために、fork-joinのモデルを使用します。

次のDMAプリミティブを考えます。


#![allow(unused)]
fn main() {
/// 1つのDMAチャネル(ここではチャネル1)を表すシングルトンです
/// 
/// このシングルトンは、DMAチャネル1のレジスタへの排他アクセスを持ちます
pub struct Dma1Channel1 {
    // ..
}

impl Dma1Channel1 {
    /// データは`address`に書かれます
    /// 
    /// `inc`は、各転送の後にアドレスをインクリメントするかどうか、を意味します
    /// 
    /// 注記 この関数はvolatileな書き込みを行います
    pub fn set_destination_address(&mut self, address: usize, inc: bool) {
        // ..
    }

    /// データは`address`から読まれます
    /// 
    /// `inc`は、各転送の後にアドレスをインクリメントするかどうか、を意味します
    /// 
    /// 注記 この関数はvolatileな書き込みを行います
    pub fn set_source_address(&mut self, address: usize, inc: bool) {
        // ..
    }

    /// 転送するバイト数です
    /// 
    /// 注記 この関数はvolatileな書き込みを行います
    pub fn set_transfer_length(&mut self, len: usize) {
        // ..
    }

    /// DMA転送を開始します
    /// 
    /// 注記 この関数はvolatileな書き込みを行います
    pub fn start(&mut self) {
        // ..
    }

    /// DMA転送を停止します
    /// 
    /// 注記 この関数はvolatileな書き込みを行います
    pub fn stop(&mut self) {
        // ..
    }

    /// 転送中なら`true`を返します
    /// 
    ///  注記 この関数はvolatileな読み込みを行います
    pub fn in_progress() -> bool {
        // ..
    }
}
}

Dma1Channel1は、Serial1というシリアルポート(別名UARTまたはUSART)#1と1ショットモード(つまりサーキュラーモードでない)でやり取りするように、 静的に設定されていると想定して下さい。 Serial1は次のようなブロッキングするAPIを提供します。


#![allow(unused)]
fn main() {
/// シリアルポート#1を表すシングルトンです
pub struct Serial1 {
    // ..
}

impl Serial1 {
    /// 1バイト読み込みます
    /// 
    /// 注記:読み込めるバイトがないとブロックします
    pub fn read(&mut self) -> Result<u8, Error> {
        // ..
    }

    /// 1バイト送信します
    /// 
    /// 注記:出力FIFOバッファに空きがなければブロックします
    pub fn write(&mut self, byte: u8) -> Result<(), Error> {
        // ..
    }
}
}

例えば、(a)非同期にバッファを送信し、(b)非同期にバッファを埋めるように、Serial1 APIを拡張したいとしましょう。

メモリアンセーフなAPIから出発し、完全にメモリ安全になるまで繰り返し改善していきます。 各ステップで、非同期メモリ操作を扱う際に対処すべき問題を理解するために、 APIがどのように壊れる可能性があるか、を説明します。

最初の挑戦

初心者向けに、Write::write_allを参考に使ってみましょう。 単純化のため、全てのエラー処理を無視します。


#![allow(unused)]
fn main() {
/// シリアルポート#1を表すシングルトンです
pub struct Serial1 {
    // 注記:DMAチャネルシングルトンを追加することで、このstructを拡張します
    dma: Dma1Channel1,
    // ..
}

impl Serial1 {
    /// 与えられた`buffer`を送信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn write_all<'a>(mut self, buffer: &'a [u8]) -> Transfer<&'a [u8]> {
        self.dma.set_destination_address(USART1_TX, false);
        self.dma.set_source_address(buffer.as_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        self.dma.start();

        Transfer { buffer }
    }
}

/// 1回のDMA転送です
pub struct Transfer<B> {
    buffer: B,
}

impl<B> Transfer<B> {
    /// DMA転送が完了すると`true`を返します
    pub fn is_done(&self) -> bool {
        !Dma1Channel1::in_progress()
    }

    /// 転送が完了するまでブロックし、バッファを返します。
    pub fn wait(self) -> B {
        // 転送が完了するまでビジーウェイトします
        while !self.is_done() {}

        self.buffer
    }
}
}

注記 Transferは、上述のAPIの代わりに、フューチャーやジェネレータベースのAPIとして公開できるでしょう。 それは、API設計の問題で、API全体のメモリ安全性にはほとんど関係がありません。 そのため、このテキストでは、詳しく説明しません。

Read::read_exactの非同期バージョンも実装できます。


#![allow(unused)]
fn main() {
impl Serial1 {
    /// 与えられた`buffer`が埋められるまでデータを受信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn read_exact<'a>(&mut self, buffer: &'a mut [u8]) -> Transfer<&'a mut [u8]> {
        self.dma.set_source_address(USART1_RX, false);
        self.dma
            .set_destination_address(buffer.as_mut_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        self.dma.start();

        Transfer { buffer }
    }
}
}

write_all APIの使い方は次のとおりです。


#![allow(unused)]
fn main() {
fn write(serial: Serial1) {
    // 転送を開始して、忘れます
    serial.write_all(b"Hello, world!\n");

    // 他のことをやります
}
}

そして、read_exact APIの使用例です。


#![allow(unused)]
fn main() {
fn read(mut serial: Serial1) {
    let mut buf = [0; 16];
    let t = serial.read_exact(&mut buf);

    // 他のことをやります

    t.wait();

    match buf.split(|b| *b == b'\n').next() {
        Some(b"some-command") => { /* 何かやります */ }
        _ => { /* 何か他のことをやります */ }
    }
}
}

mem::forget

mem::forgetは安全なAPIです。もし私達のAPIが本当に安全なら、 未定義動作を起こさずに両方のAPIを同時に使えるはずです。 しかしながら、そうではありません。次の例を考えます。


#![allow(unused)]
fn main() {
fn unsound(mut serial: Serial1) {
    start(&mut serial);
    bar();
}

#[inline(never)]
fn start(serial: &mut Serial1) {
    let mut buf = [0; 16];

    // DMA転送を開始し、戻り値の`Transfer`をforgetします
    mem::forget(serial.read_exact(&mut buf));
}

#[inline(never)]
fn bar() {
    // スタック変数です
    let mut x = 0;
    let mut y = 0;

    // `x`と`y`を使います
}
}

ここで、スタック上に確保された配列を埋めるために、fooからDMA転送を開始します。 そして、戻り値のTransfermem::forgetします。 その後、fooから戻り、bar関数を実行します。

この一連の操作は、未定義動作を引き起こします。DMA転送はスタックメモリに書き込みますが、 そのメモリはfooから戻った時に解放され、barxyのような変数を確保するために再利用されます。 実行時、xyの値は、ランダムなタイミングで書き換わる可能性があります。 DMA転送はbar関数のプロローグによりスタックにプッシュされた状態(例えば、リンクレジスタ)を上書きする可能性もあります。

mem::forgetを使わずに、mem::dropを使うと、TransferのデストラクタはDMA転送を停止し、 プログラムを安全にすることができることに留意して下さい。 しかし、メモリ安全性を強制するためにデストラクタの実行に頼ることはできません。 なぜなら、mem::forgetとメモリリーク(RCサイクルを参照)はRustでは安全だからです。

両APIのバッファのライフタイムを'aから'staticに変更することで、この問題を解決できます。


#![allow(unused)]
fn main() {
impl Serial1 {
    /// 与えられた`buffer`が埋められるまでデータを受信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn read_exact(&mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        // .. 以前と同じです ..
    }

    /// 与えられた`buffer`を送信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        // .. 以前と同じです ..
    }
}
}

もし前と同じ問題を再現しようとすると、mem::forgetはもはや問題になりません。


#![allow(unused)]
fn main() {
#[allow(dead_code)]
fn sound(mut serial: Serial1, buf: &'static mut [u8; 16]) {
    // 注記 `buf`は`foo`にムーブされます
    foo(&mut serial, buf);
    bar();
}

#[inline(never)]
fn foo(serial: &mut Serial1, buf: &'static mut [u8]) {
    // DMA転送を開始し、戻り値の`Transfer`を忘れます
    mem::forget(serial.read_exact(buf));
}

#[inline(never)]
fn bar() {
    // スタック変数です
    let mut x = 0;
    let mut y = 0;

    // `x`と`y`を使います
}
}

前回同様、Transferの値をmem::forgetした後も、DMA転送は続いています。 今回は、これは問題になりません。なぜならbufは静的に確保されており(例えば、static mut変数)、 スタック上にないからです。

オーバーラップして使う

私達のAPIは、DMA転送を行っている間、ユーザーがSerialインタフェースを使えてしまいます。 これは、DMA転送が失敗するか、データロスを発生させる可能性があります。

オーバーラップしての利用を防ぐ方法は、いくつかあります。 1つの方法は、TransferSerial1の所有権を取得し、waitが呼ばれた時に所有権を返すことです。


#![allow(unused)]
fn main() {
/// 1回のDMA転送です
pub struct Transfer<B> {
    buffer: B,
    // 注記:追加しました
    serial: Serial1,
}

impl<B> Transfer<B> {
    /// 転送が完了するまでブロックし、バッファを返します。
    /// 注記:戻り値が変わっています
    pub fn wait(self) -> (B, Serial1) {
        // 転送が完了するまでビジーウェイトします
        while !self.is_done() {}

        (self.buffer, self.serial)
    }

    // ..
}

impl Serial1 {
    /// 与えられた`buffer`が埋められるまでデータを受信します
    /// 
    /// DMA転送中であることを意味する値を返します
    // 注記 今回は、`self`を値として受け取ります
    pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        // .. 以前と同じです ..

        Transfer {
            buffer,
            // 注記:追加しました
            serial: self,
        }
    }

    /// 与えられた`buffer`を送信します
    /// 
    /// DMA転送中であることを意味する値を返します
    // 注記 今回は、`self`を値として受け取ります
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        // .. 以前と同じです ..

        Transfer {
            buffer,
            // 注記:追加しました
            serial: self,
        }
    }
}
}

ムーブセマンティクスは、DMA転送を行っている間、Serial1へのアクセスを静的に防ぎます。


#![allow(unused)]
fn main() {
fn read(serial: Serial1, buf: &'static mut [u8; 16]) {
    let t = serial.read_exact(buf);

    // let byte = serial.read(); //~ ERROR: `serial` has been moved

    // .. 何かやります ..

    let (serial, buf) = t.wait();

    // .. さらに何かやります ..
}
}

オーバーラップして利用できないようにする方法が他にもいくつかあります。 例えば、Serial1にDMA転送中かどうかを示す(Cell)フラグを追加できます。 もしフラグがセットされている時は、read, write, read_exactおよびwrite_allは、 実行時にエラー(例えば、Error::InUse)を返します。 このフラグはwrite_all / read_exactが使われた時にセットし、Transfer.waitでクリアします。

コンパイラの(誤った)最適化

コンパイラは、よりプログラムを最適化するため、non-volatileなメモリ操作の順番を入れ替えたり、結合する自由があります。 現在のAPIでは、この自由が未定義動作を引き起こします。 次の例を考えます。


#![allow(unused)]
fn main() {
fn reorder(serial: Serial1, buf: &'static mut [u8]) {
    // バッファをゼロクリアします(特別な理由はありません)
    buf.iter_mut().for_each(|byte| *byte = 0);

    let t = serial.read_exact(buf);

    // ... 何か別のことをやります ..

    let (buf, serial) = t.wait();

    buf.reverse();

    // .. `buf`で何かやります ..
}
}

ここで、コンパイラは、自由にt.wait()の前にbuf.reverse()を移動することができます。 この移動は、プロセッサとDMAが同時にbufを修正するデータ競合を起こします。 同様に、コンパイラはゼロクリア操作をread_exactの後に移動するかもしれません。 それもデータ競合を起こします。

これらの問題ある順番の入れ替えを起こさないために、compiler_fenceを使えます。


#![allow(unused)]
fn main() {
impl Serial1 {
    /// 与えられた`buffer`が埋められるまでデータを受信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        self.dma.set_source_address(USART1_RX, false);
        self.dma
            .set_destination_address(buffer.as_mut_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        // 注記:追加しました
        atomic::compiler_fence(Ordering::Release);

        // 注記:これはvolatileな*書き込み*です
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }

    /// 与えられた`buffer`を送信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        self.dma.set_destination_address(USART1_TX, false);
        self.dma.set_source_address(buffer.as_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        // 注記:追加しました
        atomic::compiler_fence(Ordering::Release);

        // 注記:これはvolatileな*書き込み*です
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }
}

impl<B> Transfer<B> {
    /// 転送が完了するまでブロックし、バッファを返します。
    pub fn wait(self) -> (B, Serial1) {
        // 注記: これはvolatileな*読み込み*です
        while !self.is_done() {}

        // 注記:追加しました
        atomic::compiler_fence(Ordering::Acquire);

        (self.buffer, self.serial)
    }

    // ..
}
}

volatileな書き込みをするself.dma.start()後ろに先行するメモリ操作が移動されないように、 read_exactwrite_allではOrdering::Releaseを使います。

同様に、volatileな読み込みをするself.is_done()前に後続のメモリ操作が移動されないように、 Transfer.waitではOrdering::Acquireを使います。

フェンスの効果をより理解しやすくするために、前回セクションの例を少し修正したバージョンを示します。 フェンスを追加しており、メモリ操作の順序はコメントで記述しています。


#![allow(unused)]
fn main() {
fn reorder(serial: Serial1, buf: &'static mut [u8], x: &mut u32) {
    // バッファをゼロクリアします(特別な理由はありません)
    buf.iter_mut().for_each(|byte| *byte = 0);

    *x += 1;

    let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲

    // 注記:プロセッサはフェンスの間、`buf`にアクセスできません
    // ... 何か別のことをやります ..
    *x += 2;

    let (buf, serial) = t.wait(); // compiler_fence(Ordering::Acquire) ▼

    *x += 3;

    buf.reverse();

    // .. `buf`で何かやります ..
}
}

Releaseフェンスのおかげで、ゼロクリアする操作は、read_exactより後ろに動かすことができません。 同様に、Acquireフェンスのおかげで、reverse操作はwaitより前に動かすことができません。 両フェンスのにあるメモリ操作は、フェンスを超えて自由に順序を入れ替えることができますが、 bufに関わるような操作はありません。そのため、順序の入れ替えは、未定義動作を起こしません

compiler_fenceは求められているものより少し強いことに注意して下さい。例えば、 このフェンスは、bufxとがオーバーラップしない(Rustのエイリアス規則のため)ことが分かっているにも関わらず、 xに対する操作が結合されないようにします。しかしながら、 compiler_fenceより細かい粒度のintrinsicは存在していません。

メモリバリアは不要なのですか?

ターゲットアーキテクチャによります。Cortex M0とM4Fコアについて、AN321は次のように言っています。

3.2 一般的な使い方

(..)

DMBの使用はCortex-Mプロセッサではほとんど必要ありません。なぜならCortex-Mプロセッサは メモリトランザクションの順序を変更しないからです。しかし、ソフトウェアが他のARMプロセッサ、 特に複数のマスターがあるシステム、で再利用される場合は必要です。例えば、

  • DMAコントローラ設定。バリアは、CPUのメモリアクセスとDMA操作との間で必要です。

(..)

4.18 複数のマスターがあるシステム

(..)

47ページの図41や図42でDMBやDSB命令を除去すると、何らかのエラーが発生します。なぜなら、Cortex-Mプロセッサは

  • メモリ転送の順序を入れ替えない
  • オーバーラップした2つの書き込み転送を許可しない

ここで、図41は、DMAトランザクションを開始する前に使用されるDMB(メモリバリア)命令を示しています。

Cortex-M7コアの場合、データキャッシュ(DCache)を使っていれば、 DMAで使用されるバッファを手動で無効化しない限り、メモリバリア(DMB/DSB)が必要になります。

もしターゲットがマルチコアシステムの場合、メモリバリアが必要になる可能性が非常に高いです。

もしメモリバリアが必要な場合、compiler_fenceの代わりにatomic::fenceを使わなければなりません。 これは、Cortex-MデバイスではDMB命令を生成するはずです。

ジェネリックバッファ

私達のAPIは要件よりも制約が強いです。例えば、 次のプログラムは正しいですが、対応できません


#![allow(unused)]
fn main() {
fn reuse(serial: Serial1, msg: &'static mut [u8]) {
    // メッセージを送信します
    let t1 = serial.write_all(msg);

    // ..

    let (msg, serial) = t1.wait(); // `msg`は現在`&'static [u8]`です

    msg.reverse();

    // 今度は、逆順に送ります
    let t2 = serial.write_all(msg);

    // ..

    let (buf, serial) = t2.wait();

    // ..
}
}

このようなプログラムに対応するため、バッファの引数をジェネリックにできます。


#![allow(unused)]
fn main() {
// as-slice = "0.1.0"
use as_slice::{AsMutSlice, AsSlice};

impl Serial1 {
    /// 与えられた`buffer`が埋められるまでデータを受信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn read_exact<B>(mut self, mut buffer: B) -> Transfer<B>
    where
        B: AsMutSlice<Element = u8>,
    {
        // 注記:追加しました
        let slice = buffer.as_mut_slice();
        let (ptr, len) = (slice.as_mut_ptr(), slice.len());

        self.dma.set_source_address(USART1_RX, false);

        // 注記:微妙に変更しました
        self.dma.set_destination_address(ptr as usize, true);
        self.dma.set_transfer_length(len);

        atomic::compiler_fence(Ordering::Release);
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }

    /// 与えられた`buffer`を送信します
    /// 
    /// DMA転送中であることを意味する値を返します
    fn write_all<B>(mut self, buffer: B) -> Transfer<B>
    where
        B: AsSlice<Element = u8>,
    {
        // 注記:追加しました
        let slice = buffer.as_slice();
        let (ptr, len) = (slice.as_ptr(), slice.len());

        self.dma.set_destination_address(USART1_TX, false);

        // 注記:微妙に変更しました
        self.dma.set_source_address(ptr as usize, true);
        self.dma.set_transfer_length(len);

        atomic::compiler_fence(Ordering::Release);
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }
}

}

注記AsSlice<Element = u8> (AsMutSlice<Element = u8)の代わりに、 AsRef<[u8]> (AsMut<[u8]>)を使うことができます。

これで、reuseプログラムに対応できます。

固定バッファ

この修正でAPIは値として配列(例えば、[u8; 16])を受け取れるようになります。 しかし、配列を使用すると、ポインタが不正になる可能性があります。 次のプログラムを考えます。


#![allow(unused)]
fn main() {
fn invalidate(serial: Serial1) {
    let t = start(serial);

    bar();

    let (buf, serial) = t.wait();
}

#[inline(never)]
fn start(serial: Serial1) -> Transfer<[u8; 16]> {
    // このフレームで確保された配列です
    let buffer = [0; 16];

    serial.read_exact(buffer)
}

#[inline(never)]
fn bar() {
    // スタック変数です
    let mut x = 0;
    let mut y = 0;

    // `x`と`y`を使います
}
}

read_exact操作は、start関数にあるbufferのアドレスを使います。 このローカルbufferは、startから戻った時に解放され、read_exactで使われているポインタは不正になります。 unsoundの例と似たような状況になるでしょう。

この問題を避けるため、APIで使用するバッファに、ムーブされてもメモリの位置を保ち続けることを要求します。 Pinニュータイプは、このような保証を提供します。 全てのバッファがあらかじめ「pin」されていることを要求するように、APIを更新します。

注記: 以降のプログラムをコンパイルするためには、Rust 1.33.0以上が必要です。 執筆時点(2019-01-04)では、nightlyチャネルの使用を意味します。


#![allow(unused)]
fn main() {
/// 1回のDMA転送です
pub struct Transfer<B> {
    // 注記:変更しました
    buffer: Pin<B>,
    serial: Serial1,
}

impl Serial1 {
    /// 与えられた`buffer`が埋められるまでデータを受信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        // 注記:境界を変更しました
        B: DerefMut,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. 以前と同じです ..
    }

    /// 与えられた`buffer`を送信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        // 注記:境界を変更しました
        B: Deref,
        B::Target: AsSlice<Element = u8>,
    {
        // .. 以前と同じです ..
    }
}
}

注記: Pinニュータイプの代わりにStableDerefトレイトを使うことができますが、 Pinは標準ライブラリで提供されるため、Pinを選びました。

この新しいAPIでは、&'static mut参照、Box化したスライス、Rc化されたスライスなどを使えます。


#![allow(unused)]
fn main() {
fn static_mut(serial: Serial1, buf: &'static mut [u8]) {
    let buf = Pin::new(buf);

    let t = serial.read_exact(buf);

    // ..

    let (buf, serial) = t.wait();

    // ..
}

fn boxed(serial: Serial1, buf: Box<[u8]>) {
    let buf = Pin::new(buf);

    let t = serial.read_exact(buf);

    // ..

    let (buf, serial) = t.wait();

    // ..
}
}

'static境界

Pinを使うことで、スタックに割り当てられた配列を安全に使えるのでしょうか? 答えは、ノーです。次の例を考えます。


#![allow(unused)]
fn main() {
fn unsound(serial: Serial1) {
    start(serial);

    bar();
}

// pin-utils = "0.1.0-alpha.4"
use pin_utils::pin_mut;

#[inline(never)]
fn start(serial: Serial1) {
    let buffer = [0; 16];

    // `buffer`をこのスタックフレームにピン留めします
    // `buffer`は`Pin<&mut [u8; 16]>`の型を持ちます
    pin_mut!(buffer);

    mem::forget(serial.read_exact(buffer));
}

#[inline(never)]
fn bar() {
    // スタック変数
    let mut x = 0;
    let mut y = 0;

    // `x`と`y`を使います
}
}

これまで何回も見た通り、スタックフレームの破壊により、上記のプログラムは未定義動作に陥ります。

このAPIは、Pin<&'a mut [u8]>(ここで'astaticではありません)の型を持つバッファに対して、 安全ではありません。 この問題を解決するため、どこかに'static境界を追加しなければなりません。


#![allow(unused)]
fn main() {
impl Serial1 {
    /// 与えられた`buffer`が埋められるまでデータを受信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        // 注記:'static境界を追加しました
        B: DerefMut + 'static,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. 以前と同じです ..
    }

    /// 与えられた`buffer`を送信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        // 注記:'static境界を追加しました
        B: Deref + 'static,
        B::Target: AsSlice<Element = u8>,
    {
        // .. 以前と同じです ..
    }
}
}

これで問題のプログラムは拒絶されます。

デストラクタ

これでAPIはBoxやデストラクタを持つ型を受け入れることができます。 Transferが早めにドロップされたときに何をすべきか決める必要があります。

通常、Transferの値は、waitメソッドを使って消費されます。しかし、転送が完了する前に、 暗黙的もしくは明示的に、値をdropすることも可能です。 例えば、Transfer<Box<[u8]>>の値をドロップすると、バッファは解放されます。 これは、まだ転送中であれば、DMAが解放済みのメモリに書き込むため、未定義動作を引き起こします。

このような状況では、Transfer.dropでDMA転送を止めることが1つの選択肢です。 他の選択肢は、Transfer.dropが転送完了を待つことです。 より簡単なので、前者を選びます。


#![allow(unused)]
fn main() {
/// 1回のDMA転送です
pub struct Transfer<B> {
    // 注記:常に`Some`ヴァリアントです
    inner: Option<Inner<B>>,
}

// 注記:以前は、`Transfer<B>という名前でした
struct Inner<B> {
    buffer: Pin<B>,
    serial: Serial1,
}

impl<B> Transfer<B> {
    /// 転送が完了するまでブロックし、バッファを返します。
    pub fn wait(mut self) -> (Pin<B>, Serial1) {
        while !self.is_done() {}

        atomic::compiler_fence(Ordering::Acquire);

        let inner = self
            .inner
            .take()
            .unwrap_or_else(|| unsafe { hint::unreachable_unchecked() });
        (inner.buffer, inner.serial)
    }
}

impl<B> Drop for Transfer<B> {
    fn drop(&mut self) {
        if let Some(inner) = self.inner.as_mut() {
            // 注記:これはvolatileな書き込みです
            inner.serial.dma.stop();

            // Acquireフェンスを有効化するため、ここで読み込みが必要です
            // `dma.stop`がRMW操作をするのであれば、これは*不要*です
            unsafe {
                ptr::read_volatile(&0);
            }

            // `Transfer.wait`と同じ理由でフェンスが必要です。
            atomic::compiler_fence(Ordering::Acquire);
        }
    }
}

impl Serial1 {
    /// 与えられた`buffer`が埋められるまでデータを受信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        B: DerefMut + 'static,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. 以前と同じです ..

        Transfer {
            inner: Some(Inner {
                buffer,
                serial: self,
            }),
        }
    }

    /// 与えられた`buffer`を送信します
    /// 
    /// DMA転送中であることを意味する値を返します
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        B: Deref + 'static,
        B::Target: AsSlice<Element = u8>,
    {
        // .. 以前と同じです ..

        Transfer {
            inner: Some(Inner {
                buffer,
                serial: self,
            }),
        }
    }
}
}

これで、バッファが解放される前にDMA転送が中断されます。


#![allow(unused)]
fn main() {
fn reuse(serial: Serial1) {
    let buf = Pin::new(Box::new([0; 16]));

    let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲

    // ..

    // これはDMA転送を中断し、メモリを解放します
    mem::drop(t); // compiler_fence(Ordering::Acquire) ▼

    // これは、前のメモリ割り当てを再利用する可能性が高いです
    let mut buf = Box::new([0; 16]);

    // `buf`で何かやります
}
}

まとめ

まとめると、メモリ安全なDMA転送を行うために、これら全てを考えなければなりません。

  • Pin<B>という固定バッファと間接参照を使います。あるいは、StableDerefトレイトを使用できます。
  • B: 'staticというバッファの所有権をDMAに渡す必要があります。
  • メモリ安全性をデストラクタの実行に頼ってはいけません。 APIとmem::forgetが一緒に使われるとどうなるか、考えて下さい。
  • DMA転送を中断するカスタムデストラクタを追加、もしくは、転送完了まで待機、するようにして下さい。 APIとmem::dropが一緒に使われるとどうなるか、考えて下さい。

このテキストでは製品レベルのDMA抽象を構築するために要求される詳細を省略しています。 例えば、DMAチャネルの設定(ストリーム、サーキュラー vs ワンショットモードなど)、バッファのアライメント、 エラー処置、デバイスに依存しない抽象の作り方などについてです。 これらの点は、読者 / コミュニティの演習とします (:P)。

コンパイラサポートに関する覚書

本書はthumbv7m-none-eabiというコンパイラ組込みのターゲットを使いました。 このターゲットに対しては、Rustチームが、 corestdのようなコンパイル済みのクレート一式をrust-stdコンポーネントとして配布しています。

本書の内容を異なるターゲットアーキテクチャで再現したい場合、 Rustが(コンパイル)ターゲットに対して提供している異なるサポートレベルを考慮しなければなりません。

LLVMサポート

Rust 1.28現在、公式のRustコンパイラであるrustcは、(機械語)コード生成にLLVMを使用しています。 あるアーキテクチャに対して、Rustが提供する最低レベルのサポートは、rustcで有効化されているLLVMバックエンドがあることです。 次のコマンドを実行することで、LLVMを通して、rustcがサポートする全てのアーキテクチャを見ることができます。

$ # このコマンドを実行するためには、`cargo-binutils`のインストールが必要です
$ cargo objdump -- -version
LLVM (http://llvm.org/):
  LLVM version 7.0.0svn
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: skylake

  Registered Targets:
    aarch64    - AArch64 (little endian)
    aarch64_be - AArch64 (big endian)
    arm        - ARM
    arm64      - ARM64 (little endian)
    armeb      - ARM (big endian)
    hexagon    - Hexagon
    mips       - Mips
    mips64     - Mips64 [experimental]
    mips64el   - Mips64el [experimental]
    mipsel     - Mipsel
    msp430     - MSP430 [experimental]
    nvptx      - NVIDIA PTX 32-bit
    nvptx64    - NVIDIA PTX 64-bit
    ppc32      - PowerPC 32
    ppc64      - PowerPC 64
    ppc64le    - PowerPC 64 LE
    sparc      - Sparc
    sparcel    - Sparc LE
    sparcv9    - Sparc V9
    systemz    - SystemZ
    thumb      - Thumb
    thumbeb    - Thumb (big endian)
    wasm32     - WebAssembly 32-bit
    wasm64     - WebAssembly 64-bit
    x86        - 32-bit X86: Pentium-Pro and above
    x86-64     - 64-bit X86: EM64T and AMD64

LLVMが興味のあるアーキテクチャをサポートしており、rustcがそのバックエンドを無効化してビルドされた(Rust 1.28でのAVR)場合、 ターゲットを有効化するためにRustのソースコードを修正しなければなりません。 Pull Request rust-lang/rust#52787の最初の2つのコミットが、必要な変更のヒントになります。

その一方、LLVMがアーキテクチャをサポートしていませんが、LLVMのフォークがサポートできる場合、 rustcをビルドする前に、オリジナルのLLVMをフォークで置き換えなければなりません。 Rustビルドシステムはこれができるようになっており、原則としては、フォークを指すようにllvmサブモジュールを単に変更するだけです。

もしベンダ提供のGCCでしかターゲットアーキテクチャがサポートされていない場合、mrustcを使う選択肢があります。 これは、非公式のRustコンパイラで、RustプログラムをCコードに変換し、その後、GCCを使ってコンパイルします。

組込みのターゲット

コンパイルターゲットは、アーキテクチャだけではありません。各ターゲットは関連する仕様があり、 特に、アーキテクチャ、オペレーティングシステム、デフォルトリンカが記載されています。

Rustコンパイラはいくつかのターゲットについて知っています。これらのターゲットは、コンパイラに組み込まれている、 と呼ばれており、次のコマンドでリストを表示できます。

$ rustc --print target-list | column
aarch64-fuchsia                 mips64el-unknown-linux-gnuabi64
aarch64-linux-android           mipsel-unknown-linux-gnu
aarch64-unknown-cloudabi        mipsel-unknown-linux-musl
aarch64-unknown-freebsd         mipsel-unknown-linux-uclibc
aarch64-unknown-linux-gnu       msp430-none-elf
aarch64-unknown-linux-musl      powerpc-unknown-linux-gnu
aarch64-unknown-openbsd         powerpc-unknown-linux-gnuspe
arm-linux-androideabi           powerpc-unknown-netbsd
arm-unknown-linux-gnueabi       powerpc64-unknown-linux-gnu
arm-unknown-linux-gnueabihf     powerpc64le-unknown-linux-gnu
arm-unknown-linux-musleabi      powerpc64le-unknown-linux-musl
arm-unknown-linux-musleabihf    s390x-unknown-linux-gnu
armebv7r-none-eabihf            sparc-unknown-linux-gnu
armv4t-unknown-linux-gnueabi    sparc64-unknown-linux-gnu
armv5te-unknown-linux-gnueabi   sparc64-unknown-netbsd
armv5te-unknown-linux-musleabi  sparcv9-sun-solaris
armv6-unknown-netbsd-eabihf     thumbv6m-none-eabi
armv7-linux-androideabi         thumbv7em-none-eabi
armv7-unknown-cloudabi-eabihf   thumbv7em-none-eabihf
armv7-unknown-linux-gnueabihf   thumbv7m-none-eabi
armv7-unknown-linux-musleabihf  wasm32-experimental-emscripten
armv7-unknown-netbsd-eabihf     wasm32-unknown-emscripten
asmjs-unknown-emscripten        wasm32-unknown-unknown
i586-pc-windows-msvc            x86_64-apple-darwin
i586-unknown-linux-gnu          x86_64-fuchsia
i586-unknown-linux-musl         x86_64-linux-android
i686-apple-darwin               x86_64-pc-windows-gnu
i686-linux-android              x86_64-pc-windows-msvc
i686-pc-windows-gnu             x86_64-rumprun-netbsd
i686-pc-windows-msvc            x86_64-sun-solaris
i686-unknown-cloudabi           x86_64-unknown-bitrig
i686-unknown-dragonfly          x86_64-unknown-cloudabi
i686-unknown-freebsd            x86_64-unknown-dragonfly
i686-unknown-haiku              x86_64-unknown-freebsd
i686-unknown-linux-gnu          x86_64-unknown-haiku
i686-unknown-linux-musl         x86_64-unknown-l4re-uclibc
i686-unknown-netbsd             x86_64-unknown-linux-gnu
i686-unknown-openbsd            x86_64-unknown-linux-gnux32
mips-unknown-linux-gnu          x86_64-unknown-linux-musl
mips-unknown-linux-musl         x86_64-unknown-netbsd
mips-unknown-linux-uclibc       x86_64-unknown-openbsd
mips64-unknown-linux-gnuabi64   x86_64-unknown-redox

次のコマンドを使って、これらターゲットの仕様を表示できます。

$ rustc +nightly -Z unstable-options --print target-spec-json --target thumbv7m-none-eabi
{
  "abi-blacklist": [
    "stdcall",
    "fastcall",
    "vectorcall",
    "thiscall",
    "win64",
    "sysv64"
  ],
  "arch": "arm",
  "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
  "emit-debug-gdb-scripts": false,
  "env": "",
  "executables": true,
  "is-builtin": true,
  "linker": "arm-none-eabi-gcc",
  "linker-flavor": "gcc",
  "llvm-target": "thumbv7m-none-eabi",
  "max-atomic-width": 32,
  "os": "none",
  "panic-strategy": "abort",
  "relocation-model": "static",
  "target-c-int-width": "32",
  "target-endian": "little",
  "target-pointer-width": "32",
  "vendor": ""
}

ターゲットシステムに対して適切な組込みのターゲットが無い場合、JSON形式のファイルにターゲット仕様を記述するカスタムターゲットを作らなければなりません。 推奨する方法は、ターゲットシステムに似ている組込みターゲットの仕様をファイルに書き出し、ターゲットシステムに適合するように微調整することです。 そのために、先程見せたrustc --print target-spec-jsonコマンドを使用します。 Rust 1.28では、ターゲット仕様の各フィールドが何を意味するか説明する最新のドキュメントがコンパイラソースコード以外ありません。

ターゲット仕様ファイルを作れば、ファイルパスを指定するか、カレントディレクトリか$RUST_TARGET_PATHにあるのであれば、 その名前で参照することができます。

$ rustc +nightly -Z unstable-options --print target-spec-json \
      --target thumbv7m-none-eabi \
      > foo.json

$ rustc --print cfg --target foo.json # もしくは単に --target foo
debug_assertions
target_arch="arm"
target_endian="little"
target_env=""
target_feature="mclass"
target_feature="v7"
target_has_atomic="16"
target_has_atomic="32"
target_has_atomic="8"
target_has_atomic="cas"
target_has_atomic="ptr"
target_os="none"
target_pointer_width="32"
target_vendor=""

rust-stdコンポーネント

いくつかの組込みターゲットに対して、Rustチームはrustup経由でrust-stdコンポーネントを配布しています。 このコンポーネントは、コンパイル済みのcorestdといったクレート一式です。 そして、このコンポーネントは、クロスコンパイルに必要です。

次のコマンドを実行すると、rustup経由で利用可能なrust-stdコンポーネントを持つターゲット一覧が得られます。

$ rustup target list | column
aarch64-apple-ios                       mips64-unknown-linux-gnuabi64
aarch64-linux-android                   mips64el-unknown-linux-gnuabi64
aarch64-unknown-fuchsia                 mipsel-unknown-linux-gnu
aarch64-unknown-linux-gnu               mipsel-unknown-linux-musl
aarch64-unknown-linux-musl              powerpc-unknown-linux-gnu
arm-linux-androideabi                   powerpc64-unknown-linux-gnu
arm-unknown-linux-gnueabi               powerpc64le-unknown-linux-gnu
arm-unknown-linux-gnueabihf             s390x-unknown-linux-gnu
arm-unknown-linux-musleabi              sparc64-unknown-linux-gnu
arm-unknown-linux-musleabihf            sparcv9-sun-solaris
armv5te-unknown-linux-gnueabi           thumbv6m-none-eabi
armv5te-unknown-linux-musleabi          thumbv7em-none-eabi
armv7-apple-ios                         thumbv7em-none-eabihf
armv7-linux-androideabi                 thumbv7m-none-eabi
armv7-unknown-linux-gnueabihf           wasm32-unknown-emscripten
armv7-unknown-linux-musleabihf          wasm32-unknown-unknown
armv7s-apple-ios                        x86_64-apple-darwin
asmjs-unknown-emscripten                x86_64-apple-ios
i386-apple-ios                          x86_64-linux-android
i586-pc-windows-msvc                    x86_64-pc-windows-gnu
i586-unknown-linux-gnu                  x86_64-pc-windows-msvc
i586-unknown-linux-musl                 x86_64-rumprun-netbsd
i686-apple-darwin                       x86_64-sun-solaris
i686-linux-android                      x86_64-unknown-cloudabi
i686-pc-windows-gnu                     x86_64-unknown-freebsd
i686-pc-windows-msvc                    x86_64-unknown-fuchsia
i686-unknown-freebsd                    x86_64-unknown-linux-gnu (default)
i686-unknown-linux-gnu                  x86_64-unknown-linux-gnux32
i686-unknown-linux-musl                 x86_64-unknown-linux-musl
mips-unknown-linux-gnu                  x86_64-unknown-netbsd
mips-unknown-linux-musl                 x86_64-unknown-redox

ターゲットにrust-stdコンポーネントがない場合、あるいは、カスタムターゲットを使っている場合、 coreクレートをCargoでコンパイルさせるために、Xargoのようなツールが必要です。 Xargoはnightlyツールチェインを要求することに注意して下さい。長期計画では、Xargoの機能はCargoに取り込まれ、 最終的には安定版でその機能が利用可能になるはずです。