例外

例外と割り込みは、プロセッサが非同期イベントと致命的なエラー(例えば、不正な命令の実行)を扱うためのハードウェアの仕組みです。 例外はプリエンプションを意味し、例外ハンドラを呼び出します。例外ハンドラは、イベントを引き起こした信号に応答して実行されるサブルーチンです。

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です。この値は、ExceptionFramer0フィールドと一致します。