例外
例外と割り込みは、プロセッサが非同期イベントと致命的なエラー(例えば、不正な命令の実行)を扱うためのハードウェアの仕組みです。 例外はプリエンプションを意味し、例外ハンドラを呼び出します。例外ハンドラは、イベントを引き起こした信号に応答して実行されるサブルーチンです。
cortex-m-rt
クレートは、例外ハンドラを宣言するために、exception
アトリビュートを提供しています。
// Exception handler for the SysTick (System Timer) exception
// SysTick(システムタイマ)例外のための例外ハンドラ
#[exception]
fn SysTick() {
// ..
}
exception
属性の他は、例外ハンドラは普通の関数のように見えます。しかし、もう1つ違いがあります。
exception
ハンドラはソフトウェアから呼び出すことができません。前述の例では、SysTick();
というステートメントは、
コンパイルエラーになります。
この動作は、非常に意図的なものです。
これはexception
ハンドラ内で宣言されたstatic mut
変数の利用を安全にする、という機能を提供するためのものです。
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
// `COUNT` has type `&mut u32` and it's safe to use
// `COUNT`は`&mut u32`の型をもっており、その利用は安全です
*COUNT += 1;
}
ご存知かもしれませんが、static mut
変数を関数内で使うことは、その関数を再入不可能にします。
直接的または間接的に、複数の例外・割り込みハンドラから、もしくは、main
と1つ以上の例外・割り込みハンドラから、
再進入不可能な関数を呼び出すことは、未定義動作です。
安全なRustは、決して未定義動作になりません。そのため、再入不可能な関数は、unsafe
とマークされなければなりません。
それでも、exception
ハンドラはstatic mut
な変数を安全に使える、と述べました。これが可能なのは、どうしてでしょうか。
exception
ハンドラはソフトウェアから呼び出すことができないため、再入する可能性はありません。だから、安全に使えるのです。
完全な例
SysTick
例外を大体1秒毎に発生させるシステムタイマの例を使います。
SysTick
例外ハンドラは、呼び出された回数をCOUNT
変数に記録し、
セミホスティングを使ってホストコンソールにCOUNT
の値を出力します。
注記:この例は、どのCortex-Mデバイスでも実行できます。QEMU上でも実行可能です。
#![deny(unsafe_code)] #![no_main] #![no_std] extern crate panic_halt; use core::fmt::Write; use cortex_m::peripheral::syst::SystClkSource; use cortex_m_rt::{entry, exception}; use cortex_m_semihosting::{ debug, hio::{self, HStdout}, }; #[entry] fn main() -> ! { let p = cortex_m::Peripherals::take().unwrap(); let mut syst = p.SYST; // configures the system timer to trigger a SysTick exception every second // 毎秒SysTick例外を起こすためのシステムタイマを設定します syst.set_clock_source(SystClkSource::Core); // this is configured for the LM3S6965 which has a default CPU clock of 12 MHz // デフォルトのCPUクロックが12MHzのLM3S6965向けの設定です syst.set_reload(12_000_000); syst.enable_counter(); syst.enable_interrupt(); loop {} } #[exception] fn SysTick() { static mut COUNT: u32 = 0; static mut STDOUT: Option<HStdout> = None; *COUNT += 1; // Lazy initialization // 遅延初期化 if STDOUT.is_none() { *STDOUT = hio::hstdout().ok(); } if let Some(hstdout) = STDOUT.as_mut() { write!(hstdout, "{}", *COUNT).ok(); } // IMPORTANT omit this `if` block if running on real hardware or your // debugger will end in an inconsistent state // 重要。実際のハードウェアで実行するときは`if`ブロックを削除して下さい。そうでなければ、 // デバッガが不整合な状態に陥るでしょう。 if *COUNT == 9 { // This will terminate the QEMU process // QEMUプロセスを終了します debug::exit(debug::EXIT_SUCCESS); } }
$ tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-rt = "0.6.3"
panic-halt = "0.2.0"
cortex-m-semihosting = "0.3.1"
$ cargo run --release
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
123456789
Discoveryボードでこのコードを実行すると、OpenOCDコンソールに出力を確認できるでしょう。 プログラムは、カウントが9に到達しても停止しません。
デフォルト例外ハンドラ
exception
アトリビュートが実際に行っていることは、特定の例外を処理するデフォルト例外ハンドラのオーバーライドです。
特定の例外について、ハンドラをオーバーライドしない場合、DefaultHandler
関数がその例外を処理します。
DefaultHandler関数は下記の通りです。
fn DefaultHandler() {
loop {}
}
この関数は、cortex-m-rt
クレートによって提供されており、#[no_mangle]
とマークされています。
そのため、「DefaultHandler」にブレイクポイントを設定することができ、未処理の例外を捕捉することができます。
exception
アトリビュートを使うことで、DefaultHandler
をオーバーライドできます。
#[exception]
fn DefaultHandler(irqn: i16) {
// custom default handler
// カスタムデフォルトハンドラ
}
irqn
引数は、どの例外が処理されているかを示します。負の値は、Cortex-Mの例外が処理されていることを意味します。
ゼロまたは正の値は、デバイス固有の例外、すなわち、割り込みが処理されていること、を示しています。
ハードフォールトハンドラ
HardFault
例外は、少し特別です。この例外は、プログラムが不正な状態になった場合に発生します。
そのため、このハンドラはリターンすることができず、未定義動作を引き起こす可能性があります。
ランタイムクレートは、デバッグ性を向上するために、ユーザ定義のHardFault
ハンドラが呼び出される前に、少し仕事をします。
その結果、HardFault
ハンドラは、fn(&ExceptionFrame) -> !
のシグネチャを持つ必要があります。
ハンドラの引数は、例外によってスタックにプッシュされたレジスタへのポインタです。
これらのレジスタは、例外が発生した瞬間のプロセッサステートのスナップショットで、ハードフォールトの原因を突き止めるのに便利です。
不正な操作を行う例を示します。存在しないメモリ位置への読み込みです。
注記:このプログラムは、QEMU上ではうまく動きません。つまり、クラッシュしません。
qemu-system-arm -machine lm3s6965evb
はメモリの読み込みをチェックしないため、 無効なメモリを読み込むと、幸いにも、0
を返します。
#![no_main] #![no_std] extern crate panic_halt; use core::fmt::Write; use core::ptr; use cortex_m_rt::{entry, exception, ExceptionFrame}; use cortex_m_semihosting::hio; #[entry] fn main() -> ! { // read a nonexistent memory location // 存在しないメモリ位置を読み込みます unsafe { ptr::read_volatile(0x3FFF_FFFE as *const u32); } loop {} } #[exception] fn HardFault(ef: &ExceptionFrame) -> ! { if let Ok(mut hstdout) = hio::hstdout() { writeln!(hstdout, "{:#?}", ef).ok(); } loop {} }
HardFault
ハンドラは、ExceptionFrame
の値を表示します。実行すると、
OpenOCDコンソールに次のような表示が見えるでしょう。
$ openocd
(..)
ExceptionFrame {
r0: 0x3ffffffe,
r1: 0x00f00000,
r2: 0x20000000,
r3: 0x00000000,
r12: 0x00000000,
lr: 0x080008f7,
pc: 0x0800094a,
xpsr: 0x61000000
}
pc
の値は、例外発生時のプログラムカウンタの値で、例外を引き起こした命令を指しています。
プログラムのディスアセンブル結果を見ます。
$ cargo objdump --bin app --release -- -d -no-show-raw-insn -print-imm-hex
(..)
ResetTrampoline:
8000942: movw r0, #0xfffe
8000946: movt r0, #0x3fff
800094a: ldr r0, [r0]
800094c: b #-0x4 <ResetTrampoline+0xa>
ロード命令(ldr r0, [r0]
)が例外を発生させたことがわかります。そして、この時のr0
レジスタの値は、
0x3fff_fffe
です。この値は、ExceptionFrame
のr0
フィールドと一致します。