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-rt
、msp430-rt
、riscv-rt
のようなランタイムクレートの珍しい実装方法について興味がある人々。
要求事項
本書は、自己完結しています。読者は、Cortex-Mアーキテクチャについて詳しかったり、Cortex-Mマイクロコントローラを持っている必要はありません。 本書内の例は、全てQEMU上でテストできます。しかしながら、本書内の例を実行したり調査するため、次のツールをインストールする必要があります。
- 本書内の全コードは、2018エディションを使います。2018エディションの機能やイディオムを知らない場合は、
エディションガイド
を確認して下さい。
- Rust 1.31以上のツールチェインとARM Cortex-Mコンパイルのサポート
-
cargo-binutils
。v0.1.4以上。
- 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
-
arm-none-eabi-gdb。 GDBを含むGNU Arm Embeddedツールチェイン
(オプションのステップ)(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つのメモリブロックが定義されています。FLASH
とRAM
です。これらは、ターゲットで利用可能な物理メモリと関連しています。
ここで使用されている値は、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
変数のBSS
とDATA
が、0
と1
になっていないことに気づくでしょう。
代わりに、これらの変数はゴミデータを持っています。この問題は、デバイスの電源投入時、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
クレートをチェックして下さい。
ここで使っているリンカスクリプトシンボルのアドレスを使用する必要があるということは、混乱を招きやすく、直感的ではありません。 この奇妙さについての詳細な説明は、ここにあります。
例外処理
「メモリレイアウト」セクションでは、簡単なところから始め、例外処理を省くことにしました。
このセクションでは、例外処理サポートを追加します。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モードで関数が実行されるようになります。
- リセットハンドラのアドレスは、上の逆アセンブリで
- その後は、
0x7f
と0x00
が交互に現れるアドレスのパターンが見えます。- 上の逆アセンブリを見ると、
0x7f
がDefaultExceptionHandler
(0x7e
が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
アドレスは、0x00
と0x01
です。では、シンボルテーブルを見てみましょう。
$ 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); } }
GlobalLog
とLog
とは、log
メソッドを持っています。違いは、GlobalLog.log
がレシーバの共有参照(&self
)を取ることです。
グローバルロガーはstatic
変数なので、これが必要です。後ほど、詳しく見ます。
もう1つの違う点は、GlobalLog.log
はResult
を返さないことです。
これは、呼び出し側にエラーを報告できないことを意味します。
これはグローバルシングルトンを実装するトレイトを使うための必要条件ではありません。
グローバルシングルトンでエラー処理をすることは良いことですが、グローバルバージョンの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 astatic mut
variable whenconst 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転送を開始します。
そして、戻り値のTransfer
をmem::forget
します。
その後、foo
から戻り、bar
関数を実行します。
この一連の操作は、未定義動作を引き起こします。DMA転送はスタックメモリに書き込みますが、
そのメモリはfoo
から戻った時に解放され、bar
でx
とy
のような変数を確保するために再利用されます。
実行時、x
とy
の値は、ランダムなタイミングで書き換わる可能性があります。
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つの方法は、Transfer
がSerial1
の所有権を取得し、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_exact
とwrite_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
は求められているものより少し強いことに注意して下さい。例えば、
このフェンスは、buf
とx
とがオーバーラップしない(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]>
(ここで'a
はstatic
ではありません)の型を持つバッファに対して、
安全ではありません。
この問題を解決するため、どこかに'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チームが、
core
やstd
のようなコンパイル済みのクレート一式を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
コンポーネントを配布しています。
このコンポーネントは、コンパイル済みのcore
やstd
といったクレート一式です。
そして、このコンポーネントは、クロスコンパイルに必要です。
次のコマンドを実行すると、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に取り込まれ、
最終的には安定版でその機能が利用可能になるはずです。