学校では教えてくれないこと

例外が発生しました

2007.7

組込み機器の場合に限らず、コンピュータ上(CPU上)でソフトウェアを動かすと、例外(Exception)が発生することがあるよね。でも、そもそも「例外」って何なんだろう?

Windowsを使っていて、「0x77f7fe40" の命令が "0x00000000" のメモリを参照しました。メモリが "read" になることはできませんでした。」というような意味不明のメッセージを見たことはないかな?実際これが発生したところで、『どないせーっちゅうねん!』 と怒りを覚えることになるけど・・・

実はこれも、「例外が発生している」状態なんだ。Windowsに限らず、組込み機器向けのあらゆるOS上で例外は発生するよ。コンピュータの世界(というよりは、CPUでの、ということになるけど)の例外とは、通常の処理を続行することができない状態になったことを指すんだ。

例外というと想定外の現象が発生したようなイメージがあるけど、実は例外には想定内の例外と想定外の例外がある。また例外は、CPUの種類によって動作が違ったり、あるCPUでは例外になる現象が他のCPUでは例外にならなかったり(もしくは別の種類の例外となる)することもあるから、CPUごとに例外の意味を正しく知っておく必要があるね。ここで、いくつか例を見てみよう!

例1 ゼロ除算

10/0のように、ゼロで割り算を行うことはできないよね。だけど、実際には

数式

というようなプログラムを書いていて、b=0だった場合にはゼロで割り算をすることになり、ゼロ除算例外が発生することになってしまうよね。基本的には、ゼロ除算が発生しないようにプログラムを設計すべきで、ゼロ除算が発生する可能性があるプログラムはバグと考えるべきなんだ。

例2 アドレス例外

メモリやデバイスが存在しない領域にアクセスしようとしたり、読み出し専用に指定されている領域に書き込みを行おうとしたりした場合に発生する。冒頭で述べた Windowsの意味不明のメッセージはこれに当たるんだ。

例3 アライメント例外

CPUによっては、32bit幅(4バイト)でアクセスする場合には、アドレスが4の倍数でなければならない(同様に、16bit幅アクセスでは、アドレスが2の倍数でなければならない)という決まりがある場合がある。この規則から外れるようなアクセスを行った場合に起きるのがアラインメント例外なんだ。例えば以下のようなコードだ。

コード例

例4 プログラム例外

CPUが理解できない命令を実行しようとした場合に発生する。例えば Pentium4で初めて使えるようになったアセンブラ命令を、Pentium3で実行しようとした場合などに起きるんだ。またプログラムにバグがあり、メモリ中のプログラムを書きつぶしてしまった場合などにも発生するよ。

例5 内部、外部割り込み

CPU内部もしくは外部で割り込みが発生した場合も例外として扱われるんだ。例えば内蔵タイマーがタイムアウトした、内蔵のLANやシリアルでデータを受信した、スイッチが操作された、などの場合だ。このうち例1~例4は、基本的に正常系では起こってはいけない例外になり、例5はむしろ積極的に利用すべき例外ということが言えるね。

さて、ここまでは例外の例(ちょっと変な言い方だね)をあげて説明したけど、整理できたかな? 続いて、例外が発生した時にCPUでは何が起きているのか、例外を処理するためのプログラムを作るときにどんなことを考えておかないといけないのか、といったことを紹介していこう。

ゼロ除算や外部割り込みなどの例外が発生した場合、CPUはそれまで実行していた処理を中断し、「例外処理」を開始する。つまり、例外が発生した場合、その処理は通常の処理に優先して実行されることになるんだ。そして、例外処理の後に、中断していた処理を再開することもあれば、ハングアップしたり、リセットがかかったりする場合もある。どのような動きをするかは、CPU、例外の種類、例外処理のプログラムの仕方によって異なってくるよ。

前半の Windows の「Read になれませんでした」という状態は、その状況を引き起こしたアプリケーションだけが影響を受けるから、メッセージ表示とともにアプリケーションが強制終了するという処理が行われるんだ。もっと致命的な例外の場合は、画面が真っ青になって意味不明のメッセージが表示されて、リセットするしかない、という状況に遭遇した人も多いんじゃないかな。

例外発生時のCPUの挙動

例外が発生した場合に開始される「例外処理」とは、実際にはどのような動きなんだろうか。CPUによって多少の違いはあるけど、一般的には例外の種類によりあらかじめ決められたアドレスに強制的にジャンプし、そこに書いてあるプログラムを実行するようになっているんだ。「そこに書いてあるプログラム」のことを例外ハンドラとか例外処理ルーチンと呼ぶよ。

処理内容によってOSが用意している場合と、アプリケーションで用意しなければならない場合があるんだ。いずれにしても、誰かが用意する必要があるね。割り込みハンドラがなければ、その割り込みが発生した時点でCPUはどう処理してよいかわからず、暴走してしまうからね。

少なくとも、意図して起こす例外の内部・外部割り込みの場合には、アプリケーションやデバイスドライバで例外ハンドラ(割り込みの場合は特に割り込みハンドラと呼ぶ)を用意しておく必要があるのは分かるよね。

例外ハンドラでは、何をすればいい?

例外処理とは、それまで行っていた処理を中断して、例外を優先的に処理するものなので、当然ながら例外処理が終わった後には元の処理を再開しなくちゃいけないんだ。そのためには、まず「元の処理に戻る」ための準備をしておく必要がある。

具体的には、元の処理のアドレス、レジスタをスタックに積んでおくこと。割り込みハンドラを抜けるときに、スタックに積んでおいたレジスタを元に戻し、元の処理のアドレスにジャンプするんだ。つまり、例外ハンドラでは

  1. 元の処理に戻るための準備
  2. 例外ハンドラで実際に行いたい処理
  3. 元の処理に戻るため

の処理 を行うことになるんだ。

このうち、CPUによっては通常処理と例外処理で完全に別のレジスタを使用するため、レジスタのスタック退避が不要だったり、元の処理のアドレスを専用レジスタが覚えているため、元の処理のアドレスのスタック退避が不要なものもある。

また、OSによっては、1. 3.をOS側で行ってくれ、プログラマーは 2.だけ作ればよい、というケースもあるんだ。ただ、どちらの場合も、実際には1.~3.の処理がされていることを意識しておくようにしようね。

では、例外ハンドラで実際に行う処理とは?

これも、例外の種類によって大きく変わってくるんだ。前回の例で言うと、ゼロ除算、アドレス例外、アラインメント例外、プログラム例外はいずれも本来起こってはいけない例外だから、発生するということは、プログラムにバグがあるかハードウェアがおかしい、ということになる。

このときにどのような状態になるかは、例外の種類やOSによってさまざまなケースがあるんだ。いきなりリセットがかかってしまう場合もあるし、発生した例外の種類を画面に表示した後にハングアップしたり、例外を起こしたタスクだけがハングアップしてOSや他のタスクは動作を続ける、という実装になっているOSもあるんだよ。

内部、外部割り込みの場合は、プログラマーが自分で例外ハンドラを記述しなければいけないのはさっき言ったとおり。意図して例外を発生させたんだから、その後の処理も意図したものでなくてはならないよね。

例えば LANでデータを受信して割り込みが発生した場合は、LANコントローラからデータを取り出し、データを処理するタスクに渡す、といった処理が考えられるだろう。また、CPUアーキテクチャや割り込み発生デバイスの種類によっては、割り込みを受け付けたことを発生源に知らせてやり、割り込みを取り下げさせる(クリアする)、という処理が必要になる場合があるよ。これを行わない場合、割り込みが入りっぱなしになり、一見ハングアップしたように見える、という状況になってしまうんだ。デバイスドライバを作成する際に陥りやすいトラブルといえるだろう。

「割り込みをクリアする方法」はハードウェアによって違うため一概には言えないけど、一般的には割り込み発生デバイスに専用のレジスタが用意されていて、それらのレジスタを操作することで行うんだ。

割り込みハンドラを作るにあたり、注意しなければならないこと

割り込みハンドラは、通常の処理を中断して優先的に行われる処理だから、 割り込みハンドラ内で実行してはいけない禁止処理や配慮するべきことがあるんだ。

1)他の処理が終了するまで待ちに入る

割り込み処理とは、くどいようだけど他の処理に優先して実行される処理だから、割り込み処理内で他の処理待ちに入ると、「他の処理」は永遠に実行されないため、デッドロック(全てのタスクや割り込み処理が、他の処理の終了待ちに入ってしまい、誰も動けなくなること)に陥ってしまうんだ。

具体的には、あるグローバル変数を他の処理が書き換えてくれるのを待つ、という場合の他に、「マルチタスクとは」の回で解説したような、セマフォ等のイベント待ち(場合によってはイベントを発行する機能も)等のタスク間通信機能がこれにあたるね。そのため、タスク間通信機能は、通常のタスクと割り込み処理内で使用できる関数が分けられている場合もあるんだ。

また、printf()のような関数も、内部的にタスク間通信機能を使用している場合があって、例外ハンドラ内では使用不可となっている OS があるので、OSの仕様を良く理解してプログラムする必要があるね。

2)時間がかかる処理を行う

割り込みをトリガにして行なうべき処理を全て割り込み処理で行なうと、その間、優先度の低い割り込みやタスクの処理は終わるまで待つことになるんだ。例えば、割り込みに同期して時間がかかる演算などを行なう場合を考えてみよう。

演算は割り込みをトリガ(=開始の合図)にして行うのだけど、演算自体は割り込み発生から少し遅れて完了してもよい(=優先度はそれほど高くない)かもしれないよね。優先度が高くない演算を割り込み処理内で行うと、その間は他の優先度の高い処理が待たされることになってしまうんだ。このような場合は、演算を行うタスクを別に用意しておき、実際の処理は演算タスクに行わせる、という設計にするんだ。このようにすると、効率的に処理を分散する事ができるよ。

説明図

例外といってもいろいろあって一言では説明できないけれど、意図しない例外の場合には、CPUやOSは暴走しないための最低限の仕組みを用意してくれていることが多い。メッセージが出る場合もあるから、例外発生の原因を取り除くことが目的になるよね。

組込みシステムの場合は、意図して例外を発生させる割り込みをよく利用する。この場合の割り込みハンドラの作り方で、バグを作ってしまったり、逆に効率のよいプログラムを組めたりするので、しっかり理解しよう。

ページのトップへ