例外処理

「メモリレイアウト」セクションでは、簡単なところから始め、例外処理を省くことにしました。 このセクションでは、例外処理サポートを追加します。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アトリビュートにより解決できます。