Linux における割り込み・例外について基礎的な内容を整理する。
概要
割り込みと例外に関連する用語を次にまとめる。
- ベクタ
- 割り込み・例外の識別番号
- ハンドラ
- ベクタに応じて呼び出す手続
- 割り込みベクタテーブル or 割り込みデスクリプタテーブル
- Real mode or Protect mode / 64bit mode で用いられるベクタとハンドラの対応表
割り込みは主にキーボードやクロックタイマなどの外部ハードウェアから発生する。
たとえば Linux ではタイマー割り込みをプロセススケジューリングに利用している。
ハードウェア割り込みは CPU がそれを受けるピンによって区別され、INTR/NMI に分類される。
INTR ピンから信号をうけた CPU は割り込みのベクタを認識して対応するハンドラをキックする。
NMI ピンはメモリモジュールにつながっており、そのベクタは必ず 2 となる。
割り込みはソフトウェアから発生させることも可能で、32bit Linux の場合は int 命令で実現できる。
おもにシステムコールや BIOS の機能を呼び出すために使われる。
例外は実行中プログラムのエラーや保護違反によって発生する。
その種別は次の通り。
- Fault
- ハンドラが処理をすることで命令を再実行できるタイプ
- 特に頻発するのは Page Fault
- OS がページフレームをディスクからロードしたりすれば解決できる
- Segmentation Fault もよく発生する
- これは CPU 的には実行再開可能だが OS が停止させるもの
- Fault が起きると IP がハンドラに切り替わったのちに再度 Fault を起こした時点の内容に復元される
- それを再開するかどうかは OS のスケジューリング実装にもよる
- Trap
- 例外の原因となった命令を再実行しないタイプ
- たとえばプログラムのデバッグでは条件を満たしたら into や int 3 によってソフトウェア的に trap を起こすなどの動作をしている
- ハンドラが処理をおこなうと IP は例外を起こした命令の次の命令を指している
- Abort
- 致命的なエラーであり実行を終了するタイプ
- Pentium 以降の CPU ではチップ内部の状態をチェックする機構がありこれに違反すると Abort が起きる
割り込みのマスク
CPU は INTR ピンから信号を受けるとハンドラを呼び出しに行くが、状況によっては割り込みされたくないことがある。
この場合は、EFLAGS レジスタの IF フラグをセットすることで INTR からの割り込みを無視するよう CPU の動作を設定することができる。
ただし NMI ピンからの割り込みはこれではマスクできない。
しかし NMI の割り込み処理が入れ子にならないようにしたいケースもある。
その場合は NMI の送信元に対して送らないように要請するような実装を用いることでマスクを実現できる。
たとえば CPU のモード移行においてマスクが用いられる。
モード移行には幾つかのステップが必要であり、この間に割り込みが行われるとハンドラが正しく機能しない。
次のコードはマスクをしている部分の抜粋。
cli
movb $0x80, %al
outb %al, $0x70
cli で IF フラグをリセットし、movb+outb で IO ポートの 0x70 に 0x80 を書き込んで NMI マスク信号を書き込んでいる。
割り込み処理プロセス
Real mode
- CS/IP/FLAGS レジスタの内容をスタックにプッシュする
- FLAGS レジスタの IF フラグをリセットする(割り込みマスク)
- ベクタに対応するハンドラをテーブルから引き当てて CS/IP レジスタにセット
- ハンドラが実行される
割り込みベクタテーブルはアドレスとサイズが固定されている。
テーブルのエントリは 4byte の大きさで、ハンドラのアドレスがセグメントとオフセットのペアで格納されている。
それぞれは 16bit なので合計 32bit = 4byte になる。
Linux kernel のブートローダは CPU が Real mode で動作している間にソフトウェア割り込みを利用して BIOS の機能を呼び出し、ディスクからの読み込みなどを行っている。
BIOS を呼ぶために使われる割り込みベクタテーブルはユーザで作成する必要がなく POST が終わった時点で作成されている。
Protect mode
Protect mode は途中まで Real mode と同じだが次の点で異なる。
- 割り込みデスクリプタテーブル(IDT: Interrupt Descriptor Table)を用いる
- IDT のアドレスと大きさを指定できる
- ただし割り込み・例外は最大255個しかないので大きさに関しては最大 255 エントリ
- IDT をメモリに作成してから lidt: Load IDT 命令でアドレスとサイズを IDTR レジスタに書き込むことで利用可能となる
- ゲート・デスクリプタを利用する
- IDT のエントリのこと
- 全てのデスクリプタは CPL=3 からはアクセスできない
- ただしベクタ 3, 4, 5, 128 に関しては int3, into, bound, int 0x80 のために許可される
- 大きさは 8byte
- 3つの種類がある
- タスクゲート・デスクリプタ
- Linux では未使用- 割り込みゲート・デスクリプタ
- 割り込みマスクをする
- 割り込みハンドラに用いる
- トラップゲート・デスクリプタ
- 割り込みマスクをしない
- 例外ハンドラに用いる
- 割り込みゲート・デスクリプタ
- IDT のエントリのこと
- CS/EIP/EFLAGS レジスタのプッシュ先が異なる
- ソフトウェア割り込みの権限を設定できる
処理の流れは次の通り
- 割り込み・例外のベクタ i を得る
- IDT から i 番目のエントリを読み取る
- デスクリプタからセグメント・デスクリプタを読み取る
- GDT/LDT からセグメントデスクリプタを読み取る
- セグメント・デスクリプタの DPL と CPL を比較
- DPL > CPL である場合は一般保護例外(ベクタ 13)を発生
- CPL=0 の例外を DPL=3 のハンドラで処理させてしまうと保護機能の生合成が取れなくなるため- ソフトウェア的には設定できてしまうがハードウェアで許さない
- DPL > CPL である場合は一般保護例外(ベクタ 13)を発生
- 割り込みがソフトウェア的に発生させられた場合はゲートデスクリプタの DPL と CPL を比較
- CPL > DPL であれば一般保護例外を発生
- 特権レベルの低いプログラムが任意のハンドラを呼び出すことを防ぐ
- 上に述べた通り 3, 4, 5, 128 のみがデスクリプタの DPL=3 に設定されている
- ハードウェア的な割り込みに対してはこのチェックが行われることはない
- EFLAGS, CS, EIP の内容を現在のスタックに積む
- CPL とハンドラのコードセグメントの DPL によって積む先が変わる
- 特権レベルの移行が発生しない場合は現在のスタックをそのまま用いる
- 特権レベルの移行が発生する場合
- CPL とハンドラの DPL が異なるとスタックが変わる
- TSS セグメントからセグメントセレクタとスタックポインタを読む
- CPL=3 の例外を DPL=0(カーネルモード)で処理する場合
- SS0, ESP0 の指す領域にスタックが切り替わる
- SS, ESP, EFLAGS, CS, EIP レジスタの内容をスタックにつむ
- スタックポインタの位置を SS, ESP レジスタにロードする
- 例外にエラーコードを伴う場合はスタックにプッシュする
- ゲート・デスクリプタからセグメント・セレクタとオフセットを読み取り CS, EIP レジスタにロードする
- ハンドラ手続の終わりに IRET 命令を実行して割り込み発生前の位置に戻る
64bit mode
同様にゲート・デスクリプタを用いるが次の点が異なる。
- 退避されるレジスタの内容は「すべて」64bit
- 32bit の SS レジスタも 0 でパディングして 64bit 分の領域を用いる
- デスクリプタのサイズは 16byte に拡張される
- ハンドラの仮想アドレスを 64bit で指定できるようにするため - ハンドラのスタックポインタをゲート・デスクリプタで選択できる
- Linux kernel では 1(debug), 3(break point)の場合にデバッグ用のスタック領域が使用される
- スタックを指定するには TSS セグメントの IST(Interrupt Stack Table)に7つまでのアドレスを登録
- デスクリプタの IST フィールドでこの登録を指定することで使い分けが可能となる