例外処理
「メモリレイアウト」セクションでは、まずは単純に始めるため、例外の処理は扱わないことにしました。このセクションでは、それらを処理するためのサポートを追加します。これは、stable Rust でコンパイル時にオーバーライド可能な動作を実現する方法の一例にもなります(つまり、シンボルを弱くする不安定な #[linkage = "weak"] 属性に依存しません)。
背景情報
要するに、例外 とは、Cortex-M などのアーキテクチャがアプリケーションに対して非同期の、通常は外部からのイベントに応答させるために提供している仕組みです。多くの人になじみのある最も代表的な例外の種類は、古典的な(ハードウェア)割り込みです。
Cortex-M の例外機構は次のように動作します。 プロセッサがある種類の例外に関連付けられた信号やイベントを受け取ると、現在のサブルーチンの実行を中断し(状態をコールスタックに退避して)、その後、新しいスタックフレーム上で対応する例外ハンドラー、つまり別のサブルーチンを実行します。例外ハンドラーの実行が終了すると(つまり、そこからリターンすると)、プロセッサは中断されていたサブルーチンの実行を再開します。
プロセッサは、どのハンドラーを実行するかを決めるためにベクターテーブルを使います。テーブル内の各エントリにはハンドラーへのポインターが含まれており、各エントリはそれぞれ異なる種類の例外に対応しています。たとえば、2 番目のエントリはリセットハンドラー、3 番目のエントリは NMI(非マスカブル割り込み)ハンドラー、といった具合です。
前に述べたとおり、プロセッサはベクターテーブルがメモリ上の特定の場所にあることを前提としており、その中の各エントリは実行時にプロセッサによって使用される可能性があります。したがって、エントリには常に有効な値が入っていなければなりません。さらに、rt クレートは柔軟であってほしいので、エンドユーザーが各例外ハンドラーの動作をカスタマイズできるようにしたいところです。最後に、ベクターテーブルは読み出し専用メモリ、あるいはより正確には簡単には変更できないメモリ上に置かれるため、ユーザーはハンドラーを実行時ではなく静的に登録しなければなりません。
これらすべての制約を満たすために、rt クレートではベクターテーブルのすべてのエントリに デフォルト 値を割り当てます。ただし、その値を一種の weak なものにして、エンドユーザーがコンパイル時にそれらをオーバーライドできるようにします。
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(),
}
unsafe extern "C" {
fn NMI();
fn HardFault();
fn MemManage();
fn BusFault();
fn UsageFault();
fn SVCall();
fn PendSV();
fn SysTick();
}
#[unsafe(link_section = ".vector_table.exceptions")]
#[unsafe(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 を割り当てるべきとされているので、そのために union を使います。ハンドラーを指さなければならないエントリでは、外部 関数を使用します。これは、エンドユーザーが実際の関数定義を 提供 できるようにするために重要です。
次に、Rust コード内でデフォルトの例外ハンドラーを定義します。エンドユーザーによってハンドラーが割り当てられていない例外は、このデフォルトハンドラーを使います。
$ tail -n4 ../rt/src/lib.rs
#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
pub extern "C" fn DefaultExceptionHandler() {
loop {}
}
}
リンカスクリプト側
リンカスクリプト側では、これらの新しい例外ベクターをリセットベクターの直後に配置します。
$ sed -n 12,25p ../rt/link.x
EXTERN(RESET_VECTOR);
EXTERN(EXCEPTIONS); /* <- NEW */
SECTIONS
{
.vector_table ORIGIN(FLASH) :
{
/* First entry: initial Stack Pointer value */
LONG(ORIGIN(RAM) + LENGTH(RAM));
/* Second entry: reset vector */
KEEP(*(.vector_table.reset_vector));
/* The next 14 entries are exception vectors */
KEEP(*(.vector_table.exceptions)); /* <- NEW */
} > 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 はその操作を何事もなく受け入れて 0 を返してしまいます。トラップ命令は QEMU と実機の両方で動作しますが、 残念ながら純粋な Rust コードとしては利用できません。ここではインラインアセンブリを 使ってトラップ命令を生成します。後の章ではアセンブリの使い方を より詳しく説明します。
#![no_main]
#![no_std]
use core::arch::asm;
use rt::entry;
entry!(main);
fn main() -> ! {
// this executes the undefined instruction (UDF) and causes a HardFault exception
unsafe { asm!("udf #0", options(noreturn)) };
}
(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-littlearm
Disassembly of section .text:
00000040 <main>:
40: push {r7, lr}
42: mov r7, sp
44: udf #0x0
46: trap
00000048 <Reset>:
48: push {r7, lr}
4a: mov r7, sp
4c: movw r1, #0x0
50: movw r0, #0x0
54: movt r1, #0x2000
58: movt r0, #0x2000
5c: subs r1, r1, r0
5e: bl 0x8a <__aeabi_memclr> @ imm = #0x28
62: movw r1, #0x0
66: movw r0, #0x0
6a: movt r1, #0x2000
6e: movt r0, #0x2000
72: subs r2, r1, r0
74: movw r1, #0x44e
78: movt r1, #0x0
7c: bl 0x15a <__aeabi_memcpy> @ imm = #0xda
80: bl 0x40 <main> @ imm = #-0x44
00000084 <UsageFault>:
84: push {r7, lr}
86: mov r7, sp
88: b 0x88 <UsageFault+0x4> @ imm = #-0x4
$ cargo objdump --bin app --release -- -s -j .vector_table
app: file format elf32-littlearm
Contents of section .vector_table:
0000 00000120 49000000 85000000 85000000 ... I...........
0010 85000000 85000000 85000000 00000000 ................
0020 00000000 00000000 00000000 85000000 ................
0030 00000000 00000000 85000000 85000000 ................
ベクタテーブルは、ここまで本書で扱ってきたすべてのコードスニペットの結果と一致するものになりました。 要約すると:
- 以前のメモリの章の Inspecting it セクションでは、次のことを学びました:
- ベクタテーブルの最初のエントリには、スタックポインタの初期値が含まれます。
- Objdump は
little endian形式で表示するため、スタックは0x2001_0000から始まります。 - 2 番目のエントリは、Reset ハンドラであるアドレス
0x0000_0049を指します。- Reset ハンドラのアドレスは上の逆アセンブルで確認でき、
0x48です。 - 最下位ビットが 1 に設定されていても、アラインメント要件のため アドレス自体は変わりません。代わりに、その関数が Thumb モード で実行されるようになります。
- Reset ハンドラのアドレスは上の逆アセンブルで確認でき、
- その後には、
0x85と0x00が交互に現れるアドレスのパターンが見て取れます。- 上の逆アセンブルを見ると、
0x85がUsageFault(0x84を Thumb モードで実行したもの)を指していることは明らかです。 - このパターンを、この章の前半で設定したベクタテーブル(
pub static EXCEPTIONSの定義を参照)と the vector table layout for the Cortex-M に 照らし合わせると、テーブル内に対応するハンドラエントリが存在するたびに、UsageFaultのアドレスが存在していることが分かります。 - しかし、各エントリに
UsageFaultを明示的に挿入したわけではありません。 代わりに、各ハンドラをDefaultExceptionHandlerのエイリアスにしました。 逆アセンブラは同じ場所に複数のシンボルを見つけ、そのうちの 1 つを 関数名として選択しました。 - また、Rust コード内のベクタテーブルのデータ構造のレイアウトが、 Cortex-M ベクタテーブル内のすべての予約スロットに対応していることも 分かります。したがって、すべての予約スロットは正しく値 0 に設定 されています。
- 上の逆アセンブルを見ると、
ハンドラのオーバーライド
例外ハンドラをオーバーライドするには、ユーザーは、シンボル名が正確に
EXCEPTIONS で使用した名前と一致する関数を提供しなければなりません。
#![no_main]
#![no_std]
use core::arch::asm;
use rt::entry;
entry!(main);
fn main() -> ! {
unsafe { asm!("udf #0", options(noreturn)) };
}
#[unsafe(no_mangle)]
pub extern "C" fn HardFault() -> ! {
// do something interesting here
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 属性を使うことで解決できます。