x86_64アーキテクチャ

割り込み・例外

割り込みや例外の発生は、システム中にある特定の状態が生じたことを示す。割り込みは、ハードウェアからの信号等により、プログラムの実行と非同期に(ie. ランダムなタイミングで)起こる。また、INT命令(INT n, INT 3, INTO)を使うことで、ソフトウェア的に割り込みを発生させることもできる(詳細は後述)。一方で例外は、プロセッサが異常な状態を検知したときに起こる。

ハンドラ(=handler)

割り込みや例外が発生すると、発生した割り込み・例外に対応するハンドラ(=handler)が、現在実行中のプログラムを中断して実行される。

割り込みディスクリプタテーブル(=IDT)

f:id:babyron64:20171225165852j:plain f:id:babyron64:20171225171834j:plain ハンドラは、割り込みディスクリプタテーブル(=IDT)のエントリ(=IDT descriptor)をコールすることで呼び出される。IDTのアドレスは、IDTRレジスタに保持される。IDTディスクリプタには、

  • タスクゲート(=task-gate)
  • 割り込みゲート(=interrupt-gate)
  • トラップゲート(=trap-gate)

の三種類がある。

f:id:babyron64:20171225172031j:plain タスクゲートでは、タスクスイッチが行われる。割り込みゲートとトラップゲートの仕組みは、コールゲートと同じような感じ。ただし、コールゲートの呼び出しと違うところもある。まず、[R]EFLAGSレジスタがスタックに保存される。そして、エラーコードがあるならそれもスタックに保存される。ただし、INT n命令でソフトウェア的に割り込みを発生させたときには、エラーコードがあっても(ie. 通常の発生の場合に保存されるとしても)保存されない。さらに、64-Bitにおいては特権レベルの変化があるか無いかにかかわらず、SSとRSPがスタックに保存される。ただし、これらは保存されるだけで、新しい値がロードされるわけではない。この仕組みにより、特権レベルが変化するかにかかわらずハンドリングは一定のスタックサイズを使う。IDTディスクリプタのDPLは、INT命令で割り込み・例外が発生したときのみチェックされる。すなわち、ハードウェアからの割り込みやプロセッサが検出した例外のハンドリングでは、IDTディスクリプタのDPLは無視される。ハンドラからは、IRET命令でリターンする。IRET命令は、RET命令と似ているが、保存されている[R]EFLAGSレジスタを復元する点で異なる。エラーコードが保存されていたとしても、IRET命令ではポップ(=pop)されないので、ハンドラ内でポップしておかなければならない。64-Bitでは、IRETは特権レベルの変化があるかないかにかかわらず、SSとRSP分のスタックをポップする。

64-Bitではハンドリングによるスタックスイッチの方法として、割り込みゲート・トラップゲートではコールゲートに用いられる手法のほか、IST(=interrupt stack table)という仕組みを用いることもできる。ISTでは、ゲートディスクリプタ内のISTフィールドの値に対応するスタックポインタをTSSから取得する。ISTフィールドが0の場合、従来の手法が使われる。ISTは、ある特定のハンドラのスタックを特別な場所1に確保するために使われる。

割り込みゲートとトラップゲートの違いは、割り込みゲートは呼ばれると、EFLAGSレジスタのIFフラグをクリアするが、トラップゲートはクリアしないところ。割り込みゲートから戻るときには、IFフラグは、保存されていたEFLAGSレジスタによって復元される。タスクゲートについては割愛。

割り込み・例外ベクタ(=vector)

f:id:babyron64:20171225170526j:plain 発生した割り込み・例外がどのエントリに対応しているのかは、各割り込み・例外に割り当てられて番号で決まる。この番号をベクタといい、プロセッサで定義されている。ベクタは、IDT内の対応するエントリのインデックスになっている。ベクタのうち、0番から31番までは用途が決まっており(上の表では、14番までしか載っていない)、32番から255番までは自由に用途を決めて使える。256番以降のベクタは使えない。

エラーコード(=error code)

エラーコードは、例外が特定のセグメントに関係しているときに、どのセグメントと関わりがあるのかを示すために使われる。ハンドラが呼ばれると、エラーコードはハンドラのスタックにプッシュ(=push)される。

割り込み(=interrupt)

割り込みの発生には、以下の二種類ある。

  • ソフトウェア割り込み(=software interruption)
  • 外部割り込み(=external interruption or interruption external to program)

ソフトウェア割り込みを行うには、INT命令を利用する。ソフトウェア割り込みは、IFフラグでマスクすることができない。外部割り込みは、ハードウェアからのシグナルがプロセッサを割り込みピン(LINT・INTER・NMI)が受信することなどで起こる2

割り込みのマスク(=mask)

マスク可能ハードウェア割り込み(=maskable hardware interrupt)

INTRピンやAPIC2を通して起こる外部割り込みは、マスク可能ハードウェア割り込みと呼ばれる。この割り込みは、EFALGSレジスタのIFフラグがセットされている場合にのみ発生し、それ以外の場合は、抑制される。

マスク不可割り込み(=nonmaskable interrupt / NMI)

NMIピンを通して発生した割り込みや、NMIモード(詳細は割愛)で発生した割り込みはマスク不可割り込みと呼ばれる。この割り込みが発生した場合には、IDTの2番エントリが指し示すハンドラが実行される。この割り込みの処理中は他の割り込みを受け付けない。マスク不可で示されているように、この割り込みはIFフラグがクリアされていても発生する。INT命令で2番ベクタを指定した場合に発生する割り込みは、確かにIDTの2番エントリのハンドラが実行されるが、これはNMIではない。具体的には、他の割り込みを禁止しない。

例外

例外の発生には、以下の3種類がある。

  • プロセッサ例外(=processor-detected program-error exception)
  • ソフトウェア例外(=software-generated exception)
  • 機械チェック例外(=machine-check exception)

ソフトウェア例外は、対応するベクタをINT命令のオペランドに取ることで発生させることができるが、エラーコードはスタックに保存されない。そのため、ハンドラの実行が失敗する可能性がある。また、例外に対応するINTRピンにシグナルを入れることで割り込みを発生させる場合にも、エラーコードはスタックに保存されない。

例外の種類には、以下の3種類がある。

  • fault
  • trap
  • abort

これらは、ハンドルする例外の特徴やハンドリングからの復帰方法が違う。以下の説明では、境界命令(=instruction boundary)という語を使うが、これは例外が発生した命令を指す。

fault

faultは、修正が効くような例外に対して使われる(eg. page fault)。修正後には、例外が発生した命令から実行を継続することができる。すなわち、例外が発生した時の[R|E]IPを、復帰時の[R|E]IPとしてスタックに保存している。

trap

trapは、プログラムを中断して別の処理を挿入するために用いられる(eg. breakpoint)。再開時には、例外が発生した命令の次の命令から実行を継続することができる。すなわち、例外が発生した命令の次の命令を指す[R|E]IPを、復帰時の[R|E]IPとしてスタックに保存している。

abort

abortでは、境界命令の位置は保存されないことがあり、実行の再開はしない。abortは、致命的な不具合を報告し(=report)、プログラムをできるだけ穏便に停止させるために用いられる。

補足

1: 特別な場所とはおそらく、アクセス権の制限が通常のスタックと異なっている場所などだろう。

2: 他にも、APIC(advanced programmable interrupt controller) を通しても外部割り込みは起こる。APICによる割り込みに使えるベクタは、16番から255番。詳細は割愛。