(2023/9/6)
[連載12] 例外が発生した場合
今回は、例外が発生した場合どうなるのか?どうすべきなのか?について、考えていきます。
今まで書いてきた中で、よい例があります。
連載3を振り返ってみましょう。
スタックオーバーフローについての記事でした。
適度な疲労感を味わって頂けたのではないかと思います。
この記事は、SOLIDを使用する上では、「スタックフェンス機能」によりスタックオーバーフローを検出し通知してくれる機能があるよ、という事をご説明する意図でした。
「SOLID-OSにスタックの管理をお任せ」してしまえばラクになる、という事でした。
ですが仮に何らかの理由がありスタック管理は自分でしたい!というケースでは、「スタックフェンス機能」が動作しません。
スタックオーバーフローして何かのメモリ領域を壊してしまったら、、、
もう、どう動くかわかりません。
長い溜息しか出てきません。
例えば。
それっぽい命令をフェッチし実行して、なんとかよろよろ動いた挙句、あり得ないメモリアクセスが発生してデータアボート例外が発生するとか、、、
変なコードを命令フェッチしてしまって、未定義例外が発生するとか、、、
しかもどこで発生するかはその時たまたま実行した命令(っぽいコード)によります。
例外発生した後は、その例外ベクタ関数の仕様、によります。
システムダウンとするのか、無限ループに入ってるのか、それとももうリセットしてしまうのか。。。
例外って、「スーバーバイザコール例外」以外は、あまり意図して発生させてはおらず、大体においてはプログラムのバグであるケース、ですよね。
開発者側から見れば、
「突然なんか変な動きした!」
からのデバッグになることが多く、筆者も毎年毎年これには悩まされ続けています。
デバッグの後原因を突き詰めると、
スタックオーバーフローしてる!とか、
あ、NULLポインタだった!とか、、、
そんな、初歩的なミスだったりするんですよね。。。
で、長い溜息がでる。
今回は、そんな厄介な「例外発生時の対処」に、視点を当てたいと思います。
通常、デバッガを接続している場合、変な動きをしたら(大体において突然リセット)いろんな例外ベクタにブレークを仕掛けておいて、ブレークするのを待ちます。
例外ベクタに飛んでくるとブレークしますので、例外発生元のPCを調査し、その命令コード(あればソースコード)をチェックします。
SOLIDの場合は、わざわざブレークを仕掛けなくとも、例外ベクタに飛んでくるとブレークし、直前のコード(例外の原因になったコード)を表示し要因を教えてくれます。
大事なことなのでもう一度言います。
ブレークし「例外の原因になったコードを表示」します。
試しに、データアボード例外を発生させてみます。
ブレークしました。
例外発生する原因となったコードが表示されていますね。
そして右側の、ちょっとグレー色になっているウインドウでは、
「アドレス0番地へのメモリライトしようとしてるよ。」
と教えてくれるので、すぐにバグ内容がわかります。
データアボート例外だけではありません。
その他の例外については、以下URLを見てみてください。
https://solid.kmckk.com/SOLID/doc/latest/user_guide/exceptions.html
さらに、CortexのAArch64の場合、データアボート例外が単独で存在しません。
ですので、例外発生要因を知るためには、ベクタ関数の中で、要因が格納されているレジスタを読まないとわかりません。
ご参考ですが、以下URLに、AArch64の例外について説明がなされています。
単独の例外ベクタが存在しないのは、手間かかりますね。
これも、デバッグの際にこんなことを考えなくてもいいように、SOLID-IDE側で要因まで調査し表示してくれます。
プログラムが完成に近づいてきたら、デバッガを外しての動作確認を行う事が多くなってきます。
という事は、各例外処理は自作関数でハンドルすることになります。
SOLIDでは、自作関数を例外ハンドラとして登録すれば、ブレークはせずにその関数でハンドルができます。
※無償提供されているSOLID for Raspberry Pi4では自作ハンドラ関数を登録することはできません。
以下URLで解説されています。https://solid.kmckk.com/SOLID/doc/latest/user_guide/exceptions.html#id7
試してみましょう。
ここからは、Cortex-A9搭載ボード×PARTNER Jet2の組み合わせで動かしてみます。
先程のソースコードを変更し、例外ハンドラを登録してみます。
使用しているボードに搭載されているCPUはCortex-A9なので、AArch32です。
したがって、ベクタ番号は以下になります。
https://solid.kmckk.com/SOLID/doc/latest/os/cs/vector.html#aarch32
ちょっとすみませんが抜粋させていただきます。
AArch32のベクタ番号:
参考に、64ビットアーキテクチャの場合のベクタ番号は以下です。
https://solid.kmckk.com/SOLID/doc/latest/os/cs/vector.html#aarch64
ついでにこちらも抜粋させていただきます。
AArch64のベクタ番号:
今回はAArch32なので、SOLID_VECTOR_DATAABORTに対応する例外ベクタを登録します。
まず始めに、自作ハンドラの動作を見てみましょう。
① データアボート例外ハンドラ関数を書く
my_exception_handlerという関数名にしました。
後々のために、solid_vector.hをインクルートしておきます。
ハンドラ関数の中身は、今回は動作を見るだけなので空でいいでしょう。
それと、ここに来たらブレークしてくれるといいですね。
ブレークポイントを設定しました。
② この関数を例外ハンドラとして登録する
ベクタハンドラ構造体を、myExceptionHandlerという名称で定義してみました。
ベクタハンドラ構造体については以下URLに記載されています。
https://solid.kmckk.com/SOLID/doc/latest/os/cs/vector.html#solid-vector-handler
そしてSOLID_VECTOR_Register関数を使って登録します。
SOLID_VECTOR_Register関数については、以下URLに記載されています。
https://solid.kmckk.com/SOLID/doc/latest/os/cs/vector.html#solid-vector-register
今回の対象とする例外は、データアボート例外なので、「SOLID_VECTOR_DATAABORT」を指定します。
そしてこの、register_exception関数をルートタスクでコールしてみます。
そうすると、データアボート例外が発生するとmy_exception_handler関数に飛んできて、仕掛けたブレークポイントでヒットするはずです。
では実行してみましょう。
自作例外ハンドラ関数に飛んできました。
下にある「呼び出し履歴」を見てみると、4つ下にchile_task()がいますね。
ダブルクリックしてみましょう。
例外を発生させている箇所が表示されました。
念のため、このプログラムでベクタハンドラ構造体を登録しなかったらどうなるでしょうかを試してみます。
そしてこの、register_exception関数をコールしている行をコメントアウトするだけです。
実行してみます。
「自作例外ハンドラ関数が登録されていない」という状態なワケですから、SOLID側でデバッグ例外を検出してブレークしました。
自作ハンドラで例外発生時の情報が知りたいですよね。
そうするとデバッガを接続しない状態であっても、ある程度例外の情報がわかります。
よくある感じの、例えばLinuxがクラッシュした時のようなイメージです。
試してみましょう。
まず、例外ハンドラ関数内で、UART経由でContextを表示するコードを付けます。
Contextには例外を発生させたアドレス等の情報が含まれています。
以下URLにContextの構造体について記載されています。
http://solid.kmckk.com/doc/skit/current/os/cs/type.html#c.SOLID_CPU_CONTEXT
Context内のrarmに各レジスタ情報が格納されています。
中でもrarm[15]には例外発生の起因となった命令のあるPCが格納されていますので、こちらを表示してみましょう。
プログラムはこうなりました。
実行してみましょう。
例外発生個所を確認しておきます。
例外発生元のPCはここですね。0x80010554です。
シリアルターミナルのログ出力はどうなっているでしょうか。
0x80010554と出ていますね。
では最後に、シリアルターミナルに例外発生時のレジスタ情報すべてを表示させてみましょう、
プログラムはこんな感じです。
/* データアボートハンドラ */
static int my_exception_handler(void *param, SOLID_CPU_CONTEXT *pContext)
{
syslog(LOG_INFO, "Data Abort!!!");
syslog(LOG_INFO, "Jump from 0x%x (Context->rarm[15])", pContext->rarm[15]);
syslog(LOG_INFO, "Register information:");
syslog(LOG_INFO, " r0=0x%x:", pContext->rarm[0]);
syslog(LOG_INFO, " r1=0x%x:", pContext->rarm[1]);
syslog(LOG_INFO, " r2=0x%x:", pContext->rarm[2]);
syslog(LOG_INFO, " r3=0x%x:", pContext->rarm[3]);
syslog(LOG_INFO, " r4=0x%x:", pContext->rarm[4]);
syslog(LOG_INFO, " r5=0x%x:", pContext->rarm[5]);
syslog(LOG_INFO, " r6=0x%x:", pContext->rarm[6]);
syslog(LOG_INFO, " r7=0x%x:", pContext->rarm[7]);
syslog(LOG_INFO, " r8=0x%x:", pContext->rarm[8]);
syslog(LOG_INFO, " r9=0x%x:", pContext->rarm[9]);
syslog(LOG_INFO, " r10=0x%x:", pContext->rarm[10]);
syslog(LOG_INFO, " r11=0x%x:", pContext->rarm[11]);
syslog(LOG_INFO, " r12=0x%x:", pContext->rarm[12]);
syslog(LOG_INFO, " r13=0x%x:", pContext->rarm[13]);
syslog(LOG_INFO, " r14=0x%x:", pContext->rarm[14]);
syslog(LOG_INFO, " r15=0x%x:", pContext->rarm[15]);
syslog(LOG_INFO, " cpsr=0x%x:", pContext->cpsr);
return SOLID_ERR_OK;
}
実行してみます。
このように表示されました。
デバッガの助けがなくとも、例外ハンドラでこの情報を見ることができました。
初期開発時、すなわち例外ハンドラのことまでまだ考えていない時、そういう時に例外って出ますよね。
Proof Of Concept用のシステムを短時間で作っている時など、よし動いた!デモでお披露目だ!というとき、長時間動かしていたら謎のリセットが発生したりしません?
こういう場合、デバッガを接続し、長時間放っておくと、いつのまにかブレークしていて、要因が表示されているのは、とても嬉しいですよね。
(デモ途中にそれはちょっと恥ずかしいかも、ですが)
そして開発中期、すなわちエラーケースの対応を行うようなフェーズになった場合。
自作のエラーハンドリング関数が完成しているのであれば、逆に、例外発生時はこちらに飛んできて欲しいです。
SOLID-IDEの場合、どちらのケースにでも使い良いようにと考えられています。
使う側は、フェーズによって使い方を変えていけば良いです。
今回は、例外が発生した場合のハンドリングについて見てみました。
思わぬ例外が発生した場合、いかにデバッグするか、は、組み込みエンジニアの腕の見せ所だと思います。逆に言うと、それなりの経験がないと、解析が難しい事象です。
ツールを使って、迅速に解決ができるのであれば、それが一番良い事だと思います。
次回は引き続き、例外時のデバッグにおけるもう一つの便利な機能をご紹介します。