私はLinux カーネル 3.7.1のソースを手元に持っているので、それらのソースに基づいてあなたの質問に答えようと思います。コードにあるもの。には、次のコード行が見つかるarch\x86\kernel\traps.c
関数があります。early_trap_init()
set_intr_gate(X86_TRAP_DE, ÷_error);
ご覧のとおり、set_trap_gate()
は に置き換えられましたset_intr_gate()
。次のターンでこの呼び出しを展開すると、次のことが達成されます。
_set_gate(X86_TRAP_DE, GATE_INTERRUPT, ÷_error, 0, 0, __KERNEL_CS);
_set_gate
は、次の 2 つのことを担当するルーチンです。
- IDT記述子の構築
構築された記述子を IDT 記述子配列のターゲット セルにインストールします。2 つ目は単なるメモリのコピーであり、私たちにとっては興味深いものではありません。しかし、指定されたパラメーターから記述子を構築する方法を見ると、次のことがわかります。
struct desc_struct{
unsigned int a;
unsigned int b;
};
desc_struct gate;
gate->a = (__KERNEL_CS << 16) | (÷_error & 0xffff);
gate->b = (÷_error & 0xffff0000) | (((0x80 | GATE_INTERRUPT | (0 << 5)) & 0xff) << 8);
または最後に
gate->a = (__KERNEL_CS << 16) | (÷_error & 0xffff);
gate->b = (÷_error & 0xffff0000) | (((0x80 | 0xE | (0 << 5)) & 0xff) << 8);
記述子の構築の最後でわかるように、メモリには次の 8 バイトのデータ構造があります。
[0xXXXXYYYY][0xYYYY8E00], where X denotes digits of kernel code segment selector number, and Y denotes digits of address of the divide_error routine.
これらの 8 バイトのデータ構造は、プロセッサ定義の割り込み記述子です。特定のベクトルでの割り込みの受け入れに応答して、どのアクションを実行する必要があるかを識別するためにプロセッサによって使用されます。Intel がx86プロセッサ ファミリ用に定義した割り込み記述子の形式を見てみましょう。
80386 INTERRUPT GATE
31 23 15 7 0
+-----------------+-----------------+---+---+---------+-----+-----------+
| OFFSET 31..16 | P |DPL| TYPE |0 0 0|(NOT USED) |4
|-----------------------------------+---+---+---------+-----+-----------|
| SELECTOR | OFFSET 15..0 |0
+-----------------+-----------------+-----------------+-----------------+
この形式では、SELECTOR:OFFSETのペアは、割り込みの受け入れに応答して制御を取得する関数のアドレスを (長い形式で) 定義します。私たちの場合、これは__KERNEL_CS:divide_error
であり、divide_error()
ゼロによる除算例外の実際のハンドラーです。
Pフラグは、ディスクリプタが OS によって正しくセットアップされた有効なディスクリプタと見なされることを指定します。DPL -divide_error()
ソフト割り込みを使用して機能をトリガーできるセキュリティ リングを指定します。その分野の役割を理解するには、ある程度の背景が必要でした。
一般に、割り込みソースには次の 3 種類があります。
- OS からサービスを要求する外部デバイス。
- プロセッサ自体が異常な状態に陥ったことが判明すると、OS にその状態からの脱出を支援するように要求します。
- OS の制御下でプロセッサ上で実行されるプログラムで、OS に特別なサービスを要求します。
最後のケースには、専用命令 int XX の形式でプロセッサからの特別なサポートがあります。プログラムは、OSサービスを必要とするたびに、リクエストを記述したパラメータを設定し、OSがサービス提供のために使用する割り込みベクタを記述したパラメータでint命令を発行します。int 命令の発行により発生する割り込みをソフト割り込みと呼びます。したがって、ここでは、プロセッサはソフト割り込みを処理する場合にのみ DPL フィールドを考慮し、プロセッサ自体または外部デバイスによって生成された割り込みの場合は完全に無視します。DPL は、アプリケーションがデバイスをシミュレートすることを禁止し、これによってシステムの動作を暗示するため、非常に重要な機能です。
たとえば、あるアプリケーションが次のようなものを作成すると想像してください。
for(;;){
__asm int 0xFF;
//where 0xFF is vector used by system timer, to notify the kernel that the
another one timer tick was occurred
}
その場合、コンピューターの時間は実際の生活よりもはるかに速く進み、あなたが期待し、システムが期待します。その結果、システムは非常に誤動作します。ご覧のとおり、プロセッサと外部デバイスは信頼できると見なされますが、ユーザー モード アプリケーションの場合はそうではありません。ゼロ除算例外の場合、Linux は、この例外がリング 0 からのみ、つまりカーネルからのみソフト割り込みによってトリガーされるように指定します。その結果、int 0 命令がカーネル空間で実行される場合、プロセッサは制御をdivide_error()
ルーティーン。同じ命令がユーザー空間で実行される場合、カーネルはこれを保護違反として処理し、一般保護違反例外ハンドラーに制御を渡します (これはすべての無効なソフト割り込みのデフォルト アクションです)。ただし、プロセッサ自体が何らかの値をゼロで除算しようとしてゼロによる除算例外が生成された場合、divide error()
誤った除算が発生したスペースに関係なく、制御はルーチンに切り替えられます。一般に、アプリケーションがソフト割り込みによって Division By Zero 例外をトリガーできるようにしても、大きな害はないようです。しかし、最初は醜いデザインになり、2 番目はロジックが舞台裏にある可能性があります。これは、0 による除算例外が実際の不正な除算操作によってのみ生成されるという事実に依存しています。
TYPEフィールドは、割り込みの受け入れに応答してプロセッサが実行する必要がある補助アクションを指定します。実際には、割り込み記述子とトラップ記述子の 2 種類の例外記述子のみが使用されます。それらは一面だけが異なります。割り込み記述子は、プロセッサに将来の割り込みの受け入れを無効にするように強制しますが、トラップ記述子はそうしません。正直なところ、Linux カーネルがゼロ除算の例外処理に割り込み記述子を使用することにした理由がわかりません。トラップ記述子は、私にとってより合理的に聞こえます。
テストプログラムの紛らわしい出力に関する最後の注意
Floating point exception (core dumped)
歴史的な理由により、Linux カーネルは、ゼロ除算を試行したプロセスにSIGFPE (SIGnal Floating Point Exception の読み取り) シグナルを送信することで、除算ゼロ例外に応答します。はい、SIGDBZ ではありません( SIGnal Division By Zero を読んでください)。私はこれが十分に混乱していることを知っています。このような動作の理由は、Linux が元のUNIXの動作を模倣しており (この動作は POSIX で凍結されていたと思います)、元のUNIXが「ゼロによる除算」例外を「浮動小数点例外」と見なす理由です。どうしてか分かりません。