GCCによる割り込みハンドラのプログラミング
割り込みハンドラはOSが存在している場合はその流儀に従ってプログラミングすればよいのですが、 OSが存在しないベアメタルの場合にはCPUアーキテクチャや割り込み処理内で許可する動作を踏まえた専門的なプログラムが必要になります。
Arm用GCCでは関数にinterruptアトリビュートを指定すると 割り込みハンドラの入口と出口で必要になるCPUアーキテクチャに依存したプログラムをGCCに肩代わりさせることができます。 今回はarm-none-eabi-gccでinterruptアトリビュートを使ったときにどこまでできるかを確認してみます。
GCCのinterruptアトリビュート
Arm用GCCでは関数にinterruptアトリビュートを付けると、 割り込み処理をする関数ので必要となるCPUアーキテクチャに依存したコードを生成させることができます。
関数にinterruptアトリビュートを指定する場合は必ずinterruptアトリビュート付きで関数を宣言した後に関数の定義をプログラムします。 またinterruptアトリビュートはいわゆる割り込みであるIRQだけでなく、 IRQ,FIQ,SWI,ABORT,UDEFを引数に指定することで例外処理全般に対応させることができます。
関数にIRQのinterruptアトリビュートを設定する例は次のようになります。
void irq_handler() __attribute__ ((interrupt ("IRQ"))); void irq_handler() { ... }
interruptアトリビュートが生成するコード
実際にinterruptアトリビュートを設定した関数が生成するコードを確認してみます。 割り込みハンドラは確認しやすいように関数を呼び出すだけのシンプルな構造にしています。
extern void irq_main(void); void irq_handler() __attribute__ ((interrupt ("IRQ"))); void irq_handler() { irq_main(); }
コンパイルオプションは次のとおりです。
arm-none-eabi-gcc -mcpu=cortex-a9 -mfloat-abi=hard -Os -S irq_handler.c
生成されたコードは次のようになりました。見やすくするため主要な部分だけ切り出しています。
.cpu cortex-a9 .text .arch armv7-a .fpu neon-fp16 irq_handler: @ Interrupt Service Routine. @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 sub lr, lr, #4 push {r0, r1, r2, r3, ip, lr} bl irq_main ldmfd sp!, {r0, r1, r2, r3, ip, pc}^ .ident "GCC: (GNU) 8.4.0"
生成されたコードを順を追って確認していきます。
sub lr, lr, #4
割り込みからの復帰アドレスを補正しています。LRレジスタには割り込みがあった時処理していたPCのアドレス+8が格納されるため、割り込みから復帰後にその次の命令に戻るため-4しています。
push {r0, r1, r2, r3, ip, lr}
スクラッチレジスタを退避しています。 この他のレジスタはEABIでは関数の呼び出し先で必要に応じて退避することになっているので、 ここでは最低限必要なレジスタだけを退避しているようです。
またここで気になる点として.fpuがneon-fp16になっているのですがNEONで拡張された浮動小数点レジスタであるd8-d15はおろかd0-d7さえも退避されていないことがあげられます。 GCC全体として言えることなのですが、浮動小数点レジスタについては関数では呼び出し規約どおりに保護されたコードを生成するのですが、コンテキストが切り替わるときの保護はOS任せになっている感があり、この割り込みハンドラでもそのように扱われているようです。 ここはCortex-Aクラスのコアを直叩きで使用する使用する場合には注意が必要なところであると言えます。
bl irq_main
IRQを処理するメインの関数を呼び出しています。 irq_main関数は特にinterruptアトリビュートを指定する必要はなく通常の関数として問題ありません。
irq_main関数では少なくとも次の処理をすることが必要があります。
- 割り込み要因のクリア
- 割り込みコントローラーの処理
割り込み要因のクリアはOSが存在するときの割り込みハンドラでも行われる一般的な割り込み処理です。 ただし割り込み番号は自分で割り込みコントローラに問い合わせして特定する必要があります。
割り込みコントローラーの処理は割り込みコントローラによっては必要ないかもしれません。 MPCoreでよく使われているGICであれば割り込み処理が完了したことをEnd of Interruptレジスタに通知する必要があります。
ldmfd sp!, {r0, r1, r2, r3, ip, pc}^
退避していたレジスタを復旧し、CPUを割り込みモードから割り込みが起こる前のCPUモードへ復帰しています。 ldm命令はレジスタリストの最後に^を付けるとSPSRレジスタからCPSRレジスタへの復旧も行われる割り込み処理を意識した便利な仕様になっています。
また補正済みのLRレジスタがPCレジスタへ復旧されるようにstmとldmで微妙にレジスタリストが調整されていることろもポイントになります。 もし割り込みを契機に元のプログラム実行位置に戻らず、別のプログラムに遷移することがあり得る場合はinterruptアトリビュートは使えないことになります。
例外ベクタから割り込みハンドラの呼び出し
これまで確認してきたように割り込みハンドラの関数はGCCのinterruputアトリビュートを使うと割り込みハンドラでアーキテクチャに依存するコードのプログラムを省略できるようになりますが、 その関数を呼び出す部分を直接例外ベクタのアドレスに配置することはCコンパイラだけではできません。
そのためかなり低レベルな方法でIRQベクタの飛び先をinterruptアトリビュートを指定した関数に向ける必要があります。 armv7-aアーキテクチャでアセンブラを使う場合の典型的な例は次のようになります。
LDR PC,=irq_handler /* 例外ベクタの先頭+0x18番地の命令 */
まとめ
関数にinterruptアトリビュートを指定することにより次の処理を行うコードをGCCに生成させることができます。
- レジスタの退避避 (interruptの引数で指定した割り込みモードに対応)
- CPUの割り込みモードからの復旧
- 割り込みが発生した命令の次の命令からの再実行
これらの処理は通常はCPUアーキテクチャを理解しうえでアセンブラでプログラムする必要があるためかなりコストが下がったことになります。
しかしながら割り込みコントローラ等のハードウェア構成や、割り込みハンドラで浮動小数点レジスタの使用を認めるか否か等ポリシーに係わるところは当然のことながらユーザー任せになっていました。