Operating Systems Development Series
Errors, Exceptions, Interruptions
by Mike, 2008, 2009
注意: このチュートリアルはソフトウェア割込み処理について述べており、ハードウェア割込み処理については述べていません。もし、ハードウェア割り込みをお探しなら、8259A PICチュートリアルをご覧ください。ハードウェア割り込みのソフトウェア側での処理については、こちらで説明しています。

はじめに

お帰りなさい。:)

前回のチュートリアルで、私たちはシステムの基本的な設計をカバーしました。そう、ここまではかなり基本的で簡単だったでしょう?

いつになったらシステムレベルのコードに戻るのだろう、と思っているかもしれません。さて....*お帰りなさい:)

このチュートリアルでは、非常に重要な概念を扱います。エラー処理エラー処理には、単に問題を処理するだけでなく、問題を捕捉することも含まれます。例外 処理には 割り込みが必要なので、割り込み処理も扱います。

割り込みはアーキテクチャに依存します。このため、私たちは、1337という巨大な、しかし(現時点では)非常に空のハードウェア抽象化レイヤーを通して割り込みを管理するためのインタフェースを開発し、私たち自身のトラップゲートをインストールするためにカーネルとインタフェースをとる予定です。これはプロセッサ例外エラーをキャッチし、現在そして永久にトリプルフォルトを防ぐために使われるとともに、完全にハードウェア独立であることができます。

面白そうでしょ?では、ここからが本題です。

  • エラーハンドリング
  • 例外処理
  • IRs、IRQs、ISRs
  • ゲートトラップ、割り込み、タスク
  • IDTとIVT
  • IDTRプロセッサレジスタ
  • LIDTおよびSIDT命令
  • FLIHとSLIH
  • 割り込みの仕組み、スタック、エラーコード
  • カーネルパニックエラー画面の開発、すなわちBSoD

...いろいろありますね、では始めましょう。

エラー、エラー、エラー

さて、現実を直視しましょう。完璧な人間なんていません。コンピュータの場合、これはさらに真実です。私たちはカーネルランドという素晴らしい世界で仕事をしているので、単純なエラーがソフトウェアやハードウェアに予測できない問題を引き起こす可能性があるため、状況はさらに悪化します。

読者の多くは、すでにTriple Faultsで経験したことがあると思います。アプリケーション・プログラミングでは、ハードウェアを直接操作することはありません。そのため、エラーになるような問題はあまりありません。しかし、カーネルの世界では少し違います。トリプルフォールトは、命令やデータのエラーによって引き起こされます。もしプロセッサが解決できない問題があれば、悪化する前にシステムを再起動します。

OSの開発において、トリプルフォールトが発生し、エラーハンドリングを行わないというのは、非常にまずいことです。エラー処理の重要性を知ることは、これらの問題を解決し、システムが最終的にリリースされるまで安定であることを保証するために重要です。

例外処理

例外処理には2つの種類があります。プログラミング言語の構成要素(例えば、標準的なC++? try/catch/throwキーワード。コンパイラによっては、_exceptのような追加のキーワードや、SEHやVEHのようなメカニズムも含まれています)。もう1つの味は、私たちが興味を持っているものです。現在の実行の流れを変える(中断する)ように設計されたハードウェアメカニズム。この実行の流れを変える条件は、例外と呼ばれます。例外は、エラー(例外的な)状態を知らせるためにのみ使用されるべきで、通常の動作に使用される条件文には使用されない。

例外が発生すると、実行の流れが変わり、サブルーチン(例外ハンドラ)が実行される。これにより、サブルーチンは何らかの方法でエラー状態を処理することができる。通常、ハンドラが呼ばれる前に現在の状態を保存しておく。これにより、可能であれば、ハンドラは後で実行を継続することができる。

例外はハードウェアから設計されていること、すなわちハードウェアメカニズムであることを忘れないでください。このことは、ハードウェア割り込みと、割り込み処理の基本が関連していることに似ています。

このため、例外処理をハードウェアから理解するためには、割り込みを見る必要があります。次にそれを見てみましょう。

割り込みの処理

割り込み

インタラプトとは、ソフトウェアやハードウェアが注意を必要とする外部非同期信号のことです。より重要なことを実行するために、現在のタスクを中断させる方法を可能にします。

難しいことではありません。割り込みは、ゼロ除算のような問題を捕捉するための手段です。プロセッサが現在実行中のコードに問題を発見した場合、その問題を修正するために実行する代替コードをプロセッサに提供します。

他の割り込みは、ルーチンとしてソフトウェアをサービスする方法を提供するために使用されるかもしれません。これらの割り込みは、システム内の任意のソフトウェアから呼び出すことができます。これは、リング3アプリケーションにリング0レベルのルーチンを実行させる方法を提供するシステムAPIによく使われます。

割り込みは、特に非同期で状態を変える可能性のあるハードウェアから情報を受け取る方法として、多くの用途があります。

割り込みの種類

割り込みには、ハードウェア割り込みとソフトウェア割り込みの2種類があります。8259A PICのチュートリアルでは、ハードウェア割り込みを取り上げました。このチュートリアルでは、ソフトウェア割り込みに焦点を当てます。

ハードウェア割り込み

ハードウェア割り込みは、ハードウェアデバイスによって起動される割り込みです。通常、注意が必要なハードウェアデバイスです。ハードウェア割り込みハンドラは、このハードウェア要求を処理することが必要になります。

このチュートリアルでは、ハードウェア特有の割り込み処理については説明しません。x86 アーキテクチャでは、ハードウェア割り込みは 8259A PIC (Programmable Interrupt Controller) のプログラムによって処理されます。ハードウェア割り込み処理の詳細については、8259A PICチュートリアルを参照してください。

スプリアス割り込み

割り込み線への電気的干渉や、ハードウェアの不具合によって発生するハードウェア割り込みです。このような割り込みは発生させないようにしましょう。

ソフトウェア割込み

ここが楽しいところです

ソフトウェア割り込みは、ソフトウェアで実装され、トリガされる割り込みです。通常、プロセッサの命令セットには、ソフトウェア割り込みを処理するための命令が含まれています。x86アーキテクチャの場合、これらは通常INT immとINT 3であり、IRETとIRETD命令も使用します。

INT immとINT 3命令は割り込みを発生させるために使用され、IRETクラスの命令は割り込みルーチン(IR)から復帰するために使用されます。

例えば、ここではソフトウェア命令で割り込みを発生させます。

int 3 ; generates software interrupt 3
これらの命令は、ソフトウェア割込みを生成し、ソフトウェアで割込みルーチン(IR)を実行するために使用されることがあります。

ご存知のように、ソフトウェア割り込みはリアルモードでも利用可能でした。しかし、プロテクトモードへジャンプした途端、IVT(Interrupt Vector Table)が無効になってしまいました。このため、割り込みは使えません。その代わり、自作する必要があります。

このチュートリアルでは、ソフトウェア割込み処理について説明します。

割込みルーチン(IR)

割込みルーチン(IR)は、割込み要求(IRQ)を処理するために使用される特別な関数です。

プロセッサがINTのような割り込み命令を実行すると、割り込みベクタテーブル(IVT)内のその位置にある割り込みルーチン(IR)を実行します。

つまり、私たちが定義したルーチンが実行されるだけです。難しいことではありませんね。この特別なルーチンは、AXレジスタの値に基づいて、通常実行すべきインタラプトファンクションを決定します。これにより、1つの割り込みコールで複数の関数を定義することができます。例えば、DOSのINT21h関数0x4c00のようなものです。

覚えておいてください。割り込みの実行は、作成した割り込みルーチンを実行するだけです。例えば、INT 2という命令を実行すると、IVTのインデックス2のIRが実行されます。かっこいいでしょう?

IRは一般にInterrupt Requests (IRQs)とも呼ばれます。しかし、ISAバス内ではIRの命名規則がまだ使われていますので、両方の名前を理解することが重要です。

インタラプトリクエスト(IRQ)

IRQとは、コントロールバスのIRラインまたは8259Aプログラマブル割り込みコントローラ(PIC)のIRラインを使用してシステムに信号を送り、イベントを中断させることを指します。

8259 PICを1個搭載したシステムでは、IRQラインが8本あり、IR0 IR7とラベル付けされています。2つの8259 PICを持つシステムでは、IR0 IR15というラベルの付いた16本のIRQがあります。システムのISAバス上では、これらのラインはIRQ0 IRQ15とラベル付けされています。

新しいintelベースのシステムは、コントローラあたり255のIRQを可能にするAPIC (Advanced Programmable Interrupt Controller)デバイスを統合しています。

IRQの詳細については、8259A PICチュートリアルまたはAPICチュートリアルのいずれかを参照してください。

これが意味するところは、8259A PICはプロセッサのIRラインをアクティブにすることによって、ハードウェアデバイスを通してソフトウェア割り込みコールを生成し、プロセッサが正しい割り込みハンドラを実行するよう信号を送ることができるということです。これにより、ハードウェアデバイスの要求をソフトウェアで処理することができます。 これについては、8259A PICチュートリアルを参照してください...これを理解することは非常に重要です。

割込みサービスルーチン(ISR)

Interrupt Service Routines (ISR) は、割り込みハンドラです。これらは理解するのに重要なので、詳しく見てみましょう。

インタラプトハンドラ

割り込みハンドラとは、割り込みやIRQを処理するためのIRのことです。つまり、ハードウェア割り込みとソフトウェア割り込みの両方を処理するために定義したコールバックメソッドなのです。

ISRには2つのタイプがあります。FLIHと SLIHです。

ファーストレベルインタラプトハンドラ(FLIH)

デバイスドライバやカーネルの下半分の部分と考えられています。これらの割り込みハンドラはプラットフォーム固有で、通常ハードウェアの要求を処理し、割り込みルーチン(IR)や割り込み要求(IRQ)と同様に実行されます。実行時間は短いです。主な役割は、割り込みの処理、または割り込みが発生したときにのみ利用可能なプラットフォーム固有の情報の記録です(下位レベルで実行されているため)。

セカンドレベル割り込みハンドラ(SLIH)

この割り込みハンドラはFLIHより長寿命です。この点で、タスクやプロセスに似ています。SLIHは通常、カーネルプログラムまたはFLIHによって実行および管理されます。

ネストされた割込みハンドラ

割込みハンドラが実行され、割込みフラグ(IF)が設定されると、現在の割込みの間にも割込みを実行することができます。これをネスト型割込みと呼びます。

リアルモードでの割り込み

Real Modeにおける割り込みは、IVT(Interrupt Vector Table)を通じて処理されます。IVT(Interrupt Vector Table)は、Interrupt Vectorのリストです。IVTには256個の割り込みが含まれます。

IVTマップ

IVTは物理メモリの最初の1024バイト(アドレス0x0~0x3FF)に配置されています。IVT内の各エントリは4バイトで、以下のような形式です。
  • Byte 0: Offset Low Address of the Interrupt Routine (IR)
  • Byte 1: Offset High Address of the IR
  • Byte 2: Segment Low Address of the IR
  • Byte 3: Segment High Address of the IR
IVTの各エントリには、呼び出すべきIRのアドレスが単純に含まれていることに注意してください。これにより、メモリ上の任意の場所に簡単な関数を作成することができます(Our IR)。IVTに関数のアドレスが含まれていれば、全て問題なく動作します。

では、IVTについて見ていきましょう。最初の数個の割り込みは予約されており、そのままです。

x86 Interrupt Vector Table (IVT)
Base Address Interrupt Number Description
0x000 0 Divide by 0
0x004 1 Single step (Debugger)
0x008 2 Non Maskable Interrupt (NMI) Pin
0x00C 3 Breakpoint (Debugger)
0x010 4 Overflow
0x014 5 Bounds check
0x018 6 Undefined Operation Code (OPCode) instruction
0x01C 7 No coprocessor
0x020 8 Double Fault
0x024 9 Coprocessor Segment Overrun
0x028 10 Invalid Task State Segment (TSS)
0x02C 11 Segment Not Present
0x030 12 Stack Segment Overrun
0x034 13 General Protection Fault (GPF)
0x038 14 Page Fault
0x03C 15 Unassigned
0x040 16 Coprocessor error
0x044 17 Alignment Check (486+ Only)
0x048 18 Machine Check (Pentium/586+ Only)
0x05C 19-31 Reserved exceptions
0x068 - 0x3FF 32-255 Interrupts free for software use

ハードではありません。これらの割り込みは、それぞれIVT内のベースアドレスに配置されています。

プロテクトモードでの割り込み

私たちはプロテクトモードオペレーティングシステムを開発しているので、この点は重要です。これは私たちにとって重要なことです。ご存知のように、プロテクトモードでは様々な理由でIVTにアクセスすることができません。このため、これ以上割り込みにアクセスしたり、使用したりすることはできません。そこで、代わりに自分自身で割り込みを作成する必要があります。

...そして、それはすべてInterrupt Descriptor Tableから始まります。

インタラプトディスクリプタテーブル(IDT)

Interrupt Descriptor Table (IDT) はプロセッサがIRを管理するために使用する特別なテーブルです。IDT自体は256個のディスクリプタの配列で、LDTやGDTと同じようなものです。

リアルモード

リアルモードでは、IDTはIVTとも呼ばれます。詳しくは、上記のセクションのIVTの説明をご覧ください。

プロテクトモード

プロテクトモードでのIDTの動作はリアルモードとは大きく異なります(プロテクトモードでIVTを使用できない多くの理由のうちの1つです)。しかし、IVTはまだ使用されています。

IDTは8バイトのディスクリプタを256個連続してメモリに格納した配列で、IVT内の割込みベクタをインデックスとしています。次に、これらのディスクリプタ、ディスクリプタの種類、IDTの詳細について説明します。

インタラプトディスクリプタ構造

IDTのディスクリプタは以下のような形式をとります。どのような種類の記述子であるかによって、一部フォーマットが変わります。
  • Bits 0...15:
    • Interrupt / Trap Gate: Offset address Bits 0-15 of IR
    • Task Gate:
    • Not used.
  • Bits 16...31:
    • Interrupt / Trap Gate: Segment Selector (Useually 0x10)
    • Task Gate: TSS Selector
  • Bits 31...35: Not used
  • Bits 36...38:
    • Interrupt / Trap Gate: Reserved. Must be 0.
    • Task Gate: Not used.
  • Bits 39...41:
    • Interrupt Gate: Of the format 0D110, where D determins size
      • 01110 - 32 bit descriptor
      • 00110 - 16 bit descriptor
    • Task Gate: Must be 00101
    • Trap Gate: Of the format 0D111, where D determins size
      • 01111 - 32 bit descriptor
      • 00111 - 16 bit descriptor
  • Bits 42...44: Descriptor Privedlge Level (DPL)
    • 00: Ring 0
    • 01: Ring 1
    • 10: Ring 2
    • 11: Ring 3
  • Bit 45: Segment is present (1: Present, 0:Not present)
  • Bits 46...62:
    • Interrupt / Trap Gate: Bits 16...31 of IR address
    • Task Gate: Not used

それだけですか?そうです、これで全部です。)

あとは、GDTと同じように、IDTを埋めて、インストールするだけです。IDTはGDTよりずっとシンプルなので、さらに簡単です :) 上のリストが完全な記述形式です。ここでは、割り込みゲートを開発することだけを考え、それだけに焦点を当てます。

割り込みディスクリプタ。例

GDTと同じように、ビットレベルで例を作り、すべてがどのように動くかを正確に説明するのに役立ちます。

まず、割込みディスクリプタの例を見てみましょう。これはアセンブリで表示されるので、よりよく全体を見ることができます。

idt_descriptor: .m_baseLow dw 0 .m_selector dw 0x8 .m_reserved db 0 .m_flags db 010001110b .m_baseHi dw 0
そう、これがディスクリプタのすべてなんだ。そんなに難しくはないでしょう?

上の表とどのような関係があるのか、各ビットを分解して見てみましょう。

00000000 00000000 00000000 00001000 00000000 10001110 00000000 00000000
これが私たちのディスクリプタですが、バイナリ形式です。ほとんどの部分はすべて0なので、これは簡単です。

最初の2バイトは、上のコードで示したm_baseLowメンバーです。上の表から、これはディスクリプタの最初の16ビットであることがわかります。これは割り込みゲートなので、IRのベースアドレスの0-15ビットに相当します。つまり、これが私たちのフィールドであれば、IRはアドレス0に位置することになります(IRの位置は様々なので、通常はそうではありません。しかし、この例ではうまくいきます。)

次の2バイトは、m_selectorフィールドです。これはディスクリプタの16〜31バイト目です。このテーブルを見ると、これがセグメントセレクタを表していることがわかります。割り込みハンドラにはコードが含まれているので、コードセレクタのいずれかを使用する必要があります。これは GDT 内のオフセット 0x8 で定義されており、これがセグメントセレクタです。

次の数ビットは使用されません。31-35ビットは使用されず、36-38ビットは割り込みゲート用に0でなければなりません。 このため、31-38ビットは0であると安全に言うことができます。

次のバイトは、面白いことが起こるところです。文字どおり、1ビットずつ分解してみましょう。

10001110
さて...今、私たちはビット39にいます。上の表を見ると、ビット39-41は0D110でなければならないことがわかります。Dビットがセットされていれば、これは32ビットディスクリプタです。これは01110と同じなので、確かに32bitのディスクリプタです。

次の2ビット(上の00)は、ディスクリプタのバイト42-45で、プライバシーのレベル(DPL)を表しています。これは00なので、DPLはリング0で実行されることになります。

この例の中の最後の2バイトは、上記のテーブル内の最後の2バイトです。これはIRベースアドレスの上位16ビット(この例では0です)です。

見てわかるように、ここではそれほど多くのことは行われていません。セレクタは常にGDT内のコードセレクタ(ここでは0x8)になり、あとはフラグビットとIRベースアドレスをm_baseLowと m_baseHiに設定するだけです。 後ほど、すべてを理解するのに役立つ完全な例を示します。

IDTR プロセッサレジスタ

IDTRレジスタは、IDTのベースアドレスを格納するプロセッサレジスタです。

IDTRレジスタは次のような形式になっています。

IDTR Register
Bits 16...46 (IDT Base Address) Bits 0...15 (IDT Limit)

十分シンプルでしょう?IDTのベースアドレスはこのレジスタに格納されています。 プロセッサはこのレジスタを使って、IDTがどこにあるかを判断しています。

このレジスタにはリミットとベース・アドレスの両方が格納されているため、このフォーマットを知っておくことは非常に重要です。このため、単にIDTのベースアドレスを与えても動作しません。この問題は、通常、次のようなフォーマットで新しい構造体を作成することで解決されます。

idt_ptr: .limit dw idt_end - idt_start ; bits 0...15 is size of idt .base dd idt_start ; base of idt ; load register with idt_ptr

あれ、このレジスタはどうやってアクセスするんだっけ?そうなんです。

LIDT命令 - IDTをロードする

この命令は、IDTの新しいアドレスをIDTRレジスタに格納するために使用されます。この命令は、電流保護レベル(CPL)が0(Ring0)の場合のみ使用できます。使い方はとても簡単です。
lidt [idt_ptr]
それがすべてです。idt_baseがIDTのベースアドレスであれば、そのアドレスがIDTRにコピーされます。

SIDT命令 - IDTを格納する

この命令はIDTRの値を6バイトのメモリに格納するために使用されます。この命令は、リング0とリング3の両方のアプリケーションで使用することができます。
sidt [idt_ptr]

割り込みの仕組み詳細

呼び出す割込みプロシージャを探す

割り込みや例外が発生すると、プロセッサはその例外番号や割り込み番号をIDTのインデックスとして使用します。 ご存知のように、IDTは上図のような256個のディスクリプタの配列に過ぎません。IDTR.baseAddressはIDTRの上位ビットに格納されているIDTのベースアドレスで、IDTR.indexは割込み番号です。これにより、プロセッサは割り込みハンドラのディスクリプタのインデックスのベースアドレスを取得することができます。IDTR.limitに格納されているIDTのリミットサイズよりも計算値が大きい場合、プロセッサはIDTのサイズを超える呼び出しになるため、GPF(General Protection Fault)を実行します。

ディスクリプタは割込み、トラップ、タスクゲートのいずれかであることを忘れないでください。インデックスが割込みゲートまたはトラップゲートを指している場合、プロ セッサは例外ハンドラまたは割込みハンドラを呼び出します。これはコールゲートをCALLするのと同じように行われます。インデックスがタスクゲートを指している場合、プロセッサはタスクゲートへのCALLと同様に、例外または割込みハンドラタスクへのタスクスイッチを実行します。

ハンドラの情報とアドレスは、この記述子内に格納されます。プロセッサがスイッチを実行するとき

ハンドラの実行

  • ハンドラが低権限レベル(ディスクリプタのビット42-45)で実行される場合、スタックスイッチが発生する。
    1. ハンドラが使用するスタックのセグメントセレクタとスタックポインタは、現在実行中のタスクのTSSから取得されます。プロセッサは、割り込みハンドラのスタック・セグメント・セレクタとスタック・ポインタをこの新しいスタックにプッシュします。
    2. プロセッサは、EFLAGS、CS、EIPの現在の状態を新しいスタックに保存する。
    3. 例外によってエラーコードが保存される場合、エラーコードはEIPの後に新しいスタックにプッシュされます。
  • ハンドラが同じ特権レベルで実行されようとしている場合(現在の特権レベル(cpl)が(ディスクリプタのビット42-45)と同じ場合
    1. プロセッサは、EFLAGS、CS、EIPの現在の状態を現在のスタックに保存する。
    2. 例外によってエラーコードが保存される場合、エラーコードはEIPの後にカレントスタックにプッシュされる

割り込みハンドラが呼ばれたとき、スタックがどのようにプッシュされるのか、また、どのような例外がエラーコードをプッシュするのかを知ることは非常に重要なことです。この点については、次に説明します。

割込みハンドラの内部

割込みハンドラの位置はディスクリプタに格納されているので、プロセッサはハンドラを実行することができるようになりました。

ご存知のように、プロセッサはハンドラを実行するとき、いくつかの特別な情報をスタックにプッシュします。もしハンドラが同じリングレベルで実行されているなら(そうなる)、プロセッサは EFLAGS、CS、EIPエラーコードを 現在のスタックにプッシュすることを覚えておく必要があります。これにより、もし実行可能であれば、実行を継続することができます。

これらのことをまとめると、ハンドラが呼ばれたとき、スタックは次のようにセットアップされることになります。

+---------------+ -- Bottom of stack | EFLAGS | +---------------+ | Return CS | +---------------+ | Return EIP | +---------------+ | Error Code | +---------------+ -- ESP points here when handler is executed. If there is no error code, ESP points to return EIP
この情報を元にハンドラから戻り、何が原因で例外が発生したのか(エラーコードがある場合)を判断します。

割り込みハンドラ内部エラーコードの書式

また、エラーコードがスタックに格納されている場合は、その情報を元にエラーの原因を特定します。

エラーコードは以下のようなフォーマットになっています。

  • ビット0:外部イベント
    • 0: 内部またはソフトウェアイベントによってエラーが発生した。
    • 1:外部イベントまたはハードウェアイベントがエラーのトリガーとなった。
  • ビット1:記述位置
    • 0: エラーコードのインデックス部分は,GDT または現在の LDT のディスクリプタを参照する。
    • 1: エラーコードのインデックス部分は、IDTのゲートディスクリプタを参照する。
  • ビット2:GDT/LDT。ディスクリプタの位置が0の場合のみ使用する。
    • 0:エラーコードのインデックス部分が、現在のGDT内のディスクリプタを参照していることを示します。
    • 1:エラーコードのインデックス部分が、LDTのセグメントまたはゲートディスクリプタを参照していることを示します。
  • ビット3~15セグメント・セレクタ・インデックス。これは、IDT、GDT、または現在のLDTにおける、エラーコードによって参照されるセグメントまたはゲートセレクタをもたらすインデックスである。
  • 16-31ビット予約済み

エラーコードは、外部で発生した例外(INTR、LINT0、LINT1ピン経由)、またはINT n命令ではスタックにプッシュされません。

ページフォルト例外エラーの場合、エラーコードの形式が異なります。それについては、次のセクションで見ていきます。

ハンドラからの復帰

すべてのハンドラは、IRETか IRETDのどちらかの命令を使って戻らなければなりません。IRETは、保存されたEFLAGS(ハンドラの実行時にスタックにプッシュされた)を復元することと、EFLAGSのIOPLフィールドが、現在の保護レベル(CPL)が0の場合にのみ0に設定されることを除いて、RETと同じです。 IFフラグは、CPLがIOPL以下の場合のみ変更されます。

ハンドラ実行時にスタックスイッチが発生した場合、IRETは中断された手順のスタックにも切り替えます。

x86の例外

例外をリストアップ

すべての例外は、IVTまたはIDT内の最初の数回の割り込みとして定義されています。x86クラスのプロセッサから生成される例外の完全なリストを以下に示します。

  • エラー- リターンアドレス(ハンドラ呼び出し時にスタックにプッシュされたCS:EIPを返す。詳細は割込みハンドラの内部を参照)は、例外を発生させた命令を指す。例外ハンドラは、問題を修正した後、プログラムを再起動し、何事もなかったかのように見せかけることができる。
  • Trap- リターンアドレスは、直前に終了した命令の次の命令を指します。
  • Abort- リターンアドレスは常に確実に提供されるとは限りません。Abortするようなプログラムは、決して継続されることはない。

x86 Processor Exceptions
Interrupt Number Class Description Error Code
0 Fault Divide by 0 None
1 Trap or Fault Single step (Debugger) None. Can be retrived from debug registers
2 Unclassed Non Maskable Interrupt (NMI) Pin Not applicable
3 Trap Breakpoint (Debugger) None
4 Trap Overflow None
5 Fault Bounds check None
6 Fault Unvalid OPCode None
7 Fault Device not available None
8 Abort Double Fault Always 0
9 Abort (Reserved, do not use) Coprocessor Segment Overrun None
10 Fault Invalid Task State Segment (TSS) See error code below
11 Fault Segment Not Present See error code below
12 Fault Stack Fault Exception See error code below
13 Fault General Protection Fault (GPF) See error code below
14 Fault Page Fault See error code below
15 - Unassigned -
16 Fault x87 FPU Error None. x87 FPU provides own error information
17 Fault Alignment Check (486+ Only) Always 0
18 Abort Machine Check (Pentium/586+ Only) None. Error information abtained from MSRs
19 Fault SIMD FPU Exception None
20-31 - Reserved -
32-255 - Avilable for software use Not applicable

IRQ 0とシステムタイマ

ご存知のように、プロテクトモードに入る場合、すべての割り込みは無効化されていなければなりません。これを行わないと、次のクロックティックですぐにトリプルフォルトが発生します。これはなぜでしょうか?

システムタイマーは、通常8253プログラマブルインターバルタイマー(PIT)の一形態で、IRQ 0を使用して、クロックティックが発生したときにそれを知らせます。このデバイスは、システムBIOSによってこのように設定されています。

しかし、ちょっと待ってください。*上の表を見てください*、それはDivide by 0エラーではありませんか?ビンゴです。

保護モードに切り替えたために、テーブルが無効になってしまったのですから、この先どうなることやら。このため、次のシステムティックで即トリプルフォルトとなり、切り替える前に割り込みを無効にしなければならない理由です。

また、8253プログラマブルインターバルタイマ(PIT)はハードウェアデバイスであり、上の表で例外(IRQ 0)を発生させることがわかりますか?実際のエラーなのか、それとも単なるチックなのか、どうすればわかるのでしょうか?

もっと詳しく見てみましょう。

8259Aプログラマブル割り込みコントローラ(PIC)のリマッピング

8259A PICは、ハードウェア割り込みを制御するために使用される標準的なコントローラです。ハードウェアマイコンは、PICに接続されたそれぞれのIRラインでPICに信号を送ります。これにより、PICはハードウェアデバイスが注意を必要としていることを「知る」ことができ、デバイスの要求を処理するためにプロセッサに割り込みを発生させるよう信号を送ることができます。

上記の例では、8253 PITが8259A PICにシステムティックを処理するよう信号を送り、IRQ 0(8253 PITはIRQ 0を使用することを覚えておいてください)を発生させました。これは、a)0による除算例外と、b)まだ書いていないため無効なコードであり、トリプルフォルトを引き起こしました。

この問題を解決するには、8259A PICマイクロコントローラーを再プログラムして、ハードウェアデバイスが異なるIRQを使用するように再マッピングする必要があります。

IFはハードウェア割り込みにしか適用されないため、IFが0(割り込み禁止)の場合でもソフトウェア割り込みを使用できることに留意してください。しかし、ハードウェア割り込みを再び有効にしたい場合は、PICを再プログラムする必要があります。

8259A PICは、プログラムするのがかなり複雑なマイコンです。幸いなことに、そのモードのほとんどは私たちに適用されることはありません。

最後のデモは、PICを再プログラムし、割り込みを再有効化するものです。このチュートリアルを最大限に活用するために、8259A Programmable Interrupt Controllerのチュートリアルを読むことをお勧めします。

ハードウェアの抽象化

このデモには、今まで見たことのない多くの追加ファイルが含まれています。このため、コードダンプのようなもので、これはここで避けたいことです。その多くは非常にシンプルで、私たちがこれまで見てきたものであり、ブートローダにも実装されているものです。idt.hidt.cpp の中には、ここで学んだことをカバーする新しいコードもあります:割り込み記述子テーブル (IDT).

これはまた、私たちのハードウェア抽象化レイヤ(HAL)の始まりでもあります!

ご存知のように、私はこの連載を始めるにあたって、ハードウェアの抽象化とその重要性を強調してきました。その理由は、HALの構築を続けていくうちに、すぐにおわかりいただけると思います。Hal をカーネルから完全に独立させることの利点も、ここでわかるかもしれませんね。

さて、それでは HAL の主要なインターフェースの始まりを見てみましょう。

Hal - include/hal.h - HAL のためのプラットフォーム非依存型インターフェース

これはHALとカーネルとの間のインターフェースです。これは標準のインクルードディレクトリの一部であり、その実装とは完全に分離されています。ヘッダーファイルは、その中のルーチンを定義する任意のインプリメンテーションによって使用されることを意図しているため、すべてのルーチンはexternと宣言されています。インプリメンテーションはアーキテクチャに依存しますが、インターフェースは特定のインプリメンテーションに結合されることはなく、完全にハードウェアに依存しないものとなっています。

実装自体はアーキテクチャに依存しますが、我々は単純に異なるアーキテクチャのための実装を構築することができます。各インプリメンテーションはこの共通のインターフェースを使い、(hal.dllのような)ダイナミックローディングをサポートできるので、a)異なるアーキテクチャ用に構築するときにどの静的HALインプリメンテーションを使うかリンクするか、b) 異なるHALを独立して構築し、スタートアップ時にどのHALを使用するかを選択できるのです。これらはすべて同じインタフェース (Hal.h) を使っているので、異なる実装 (したがって異なるハードウェアセットアップ) を使うためにカーネルを変更する必要はありません。

今のところ2つの関数しかありません。必要ならもっと追加する予定です。

//! Initialize and shutdown hal extern int Hal_Initialize (); extern int Hal_Shutdown ();
これらのルーチンのプロトタイプは、スタートアップとシャットダウンのパラメータを指定できるように変更する予定です。いずれにせよ、これらは非常に汎用的なルーチンで、インプリメンテーションのために必要であれば、ハードウェアのセットアップとシャットダウンの方法を提供することを意図しています。

halの中にはgdt, idt, cpuとhal.cppのための非常にシンプルなレイヤーのソフトウェアがいくつか存在します。これらは下のレイヤーを初期化するだけなので(Hal.cppはcpu initializeルーチンを呼び出し、それがgdtとidt initializeメソッドを呼び出します)、このチュートリアルを必要以上に複雑にしてしまうかもしれないので、ここに掲載するつもりはありません。

代わりに、halの大部分に注目しましょう:gdtセットアップコード、idtセットアップコード(これはこのチュートリアルで見たものの大部分を含んでいます)、そしてkenrelのmain()ルーチンです。いかがですか?

GDTの詳細については説明しません。GDTの完全な説明については、チュートリアル8を参照してください。

Hal - hal/gdt.h - グローバルディスクリプタテーブル

ディスクリプタテーブル ...再び

そうです、GDTはあなたを悩ますために戻ってきたのです!...そうです、あなたです!

GDTはかなり複雑な構造をしていますね。ご存知のように、GDTはディスクリプタの配列です。GDTのディスクリプタの形式は何だったっけ?そう、じゃあ、いいや...。

  • ビット56-63ベースアドレスの24-32ビット
  • ビット55:粒度
    • 0:なし
    • 1: 制限値が4K倍される
  • ビット54:セグメントタイプ
    • 0:16ビット
    • 1:32ビット
  • ビット53:予約-0でなければならない
  • ビット52:OS用予約
  • ビット48-51:セグメント制限のビット16-19
  • ビット47セグメントはメモリ内にある(仮想メモリで使用)。
  • ビット45-46ディスクリプタ特権レベル
    • 0:(リング 0) 最高
    • 3:(リング3)最低
  • ビット44:ディスクリプタ・ビット
    • 0:システム記述子
    • 1:コードまたはデータディスクリプタ
  • ビット41~43ディスクリプタタイプ
    • ビット43:実行可能セグメント
      • 0:データセグメント
      • 1:コードセグメント
    • ビット42:展開方向(データセグメント)、準拠(コードセグメント)
    • ビット41:読み出し可能、書き込み可能
      • 0:読み出しのみ(データセグメント)、実行のみ(コードセグメント)
      • 1:読出しと書込み(データセグメント)、読出しと実行(コードセグメント)
  • ビット40:アクセスビット(仮想メモリで使用)
  • ビット16~39ビット16~39:ベースアドレスのビット0~23
  • ビット0-15:セグメントリミットのビット0-15
*恐怖で悲鳴を上げながら走り去る*。

はいはい、もうやめときますね :)しかし、真面目な話、Intelはこの構造をもっときれいに作れたと思いませんか?:)

C言語の構造体を作る

この構造は、Cの組み込み型を使って、Cスタイルの素敵な構造の後ろに隠すことができます。最初の15ビットがセグメントの制限(uint16_tのサイズ)であることを知っている、それがデータメンバ1である。次の16ビットはベースアドレスの0-23ビットで、これは1つのuint16_tまたは2つのuint8_tとして表現できます。次の16ビット(GDTの41-56ビット)は、16ビットです。これはフラグ値を含む醜い構造の大部分で、もちろん2つのuint8_tまたは1つのuint16_tを使って表現することができる。これが次のデータ・メンバーです。最後の1バイトはベースアドレスです。これが最後のデータ・メンバーです。

以上のように、あの醜い構造体は、構造体の中の4〜5個の素敵なメンバーで表現することができます。これが私たちの構造体です。この構造体を上の説明や表と比較して、どこに何が収まっているかを確認してみてください。 また、この構造体は1バイトにパックされているので、64ビットの大きさが保証されていることも覚えておいてください。

#ifdef _MSC_VER #pragma pack (push, 1) #endif //! gdt descriptor. A gdt descriptor defines the properties of a specific //! memory block and permissions. struct gdt_descriptor { //! bits 0-15 of segment limit uint16_t limit; //! bits 0-23 of base address uint16_t baseLo; uint8_t baseMid; //! descriptor bit flags. Set using bit masks above uint16_t flags; //! bits 24-32 of base address uint8_t baseHi; }; #ifdef _MSC_VER #pragma pack (pop, 1) #endif
簡単ですね。構造体内のフラグバイトを構築するために設定できるビットフラグがたくさんあります。 ヘッダーファイルを見て、それらがどのように機能するかを確認してください。基本的には、設定したいビットフラグをビットごとに OR することになります。次のセクションで、この方法を説明します。

gdtrの抽象化

チュートリアルで、プロテクトモード、gdt、gdtrについて説明しました。 gdtrは、使用するGDTを指定するためのプロセッサ内部のレジスターです。48ビットのポインターで、次のような書式に従わなければなりません。
  • Bits 0-15: size of entire gdt
  • Bits 16-48: base address of gdt
なるほど、これはC言語の構造体に変換するのが簡単ですね。上記の形式に従っていることに注目してください。
#ifdef _MSC_VER #pragma pack (push, 1) #endif //! processor gdtr register points to base of gdt. This helps //! us set up the pointer struct gdtr { //! size of gdt uint16_t m_limit; //! base address of gdt uint32_t m_base; }; #ifdef _MSC_VER #pragma pack (pop, 1) #endif // Global Descriptor Table (GDT) static struct gdt_descriptor _gdt [MAX_DESCRIPTORS]; //! gdtr data static struct gdtr _gdtr;
ここで新しいGDTと_gdtrを見ることができますが、これはプロセッサのGDTRレジスタをセットアップするときに参照するために使用されます。

gdt_install():gdtr に gdt をインストールする。

このルーチンは非常に単純なものです。lgdt 命令を使用して GDTR を gdtr ポインタでロードしているだけです。 CS は決して変化しないので、ここで far jump を行う必要はありません。
//! installs gdtr static void gdt_install () { #ifdef _MSC_VER _asm lgdt [_gdtr] #endif }

gdt_set_descriptor():gdt に新しいディスクリプタを設定します。

このルーチンはGDTに新しいディスクリプタをインストールするために使用されます。ほとんどの場合、これはそれほど難しいことではありません。
//! Setup a descriptor in the Global Descriptor Table void gdt_set_descriptor(uint32_t i, uint64_t base, uint64_t limit, uint8_t access, uint8_t grand) { if (i > MAX_DESCRIPTORS) return; //! null out the descriptor memset ((void*)&_gdt[i], 0, sizeof (gdt_descriptor)); //! set limit and base addresses _gdt[i].baseLo = base & 0xffff; _gdt[i].baseMid = (base >> 16) & 0xff; _gdt[i].baseHi = (base >> 24) & 0xff; _gdt[i].limit = limit & 0xffff; //! set flags and grandularity bytes _gdt[i].flags = access; _gdt[i].grand = (limit >> 16) & 0x0f; _gdt[i].grand |= grand & 0xf0; }

i86_gdt_initialize() - gdt を初期化する。

このルーチンはすべてを一つにまとめます。GDTR 構造を設定し、いくつかのデフォルトのディスクリプタを GDT にインストールし、最後に GDT をインストールするだけです。この GDT は、ブートローダと同じものです。ベースアドレスは0、リミット(最大アドレス)は4GB(0xffffff)です。フラグはすべてgdt.hで定義されています。これらは可読性を高め、醜いマジックナンバーを取り除くために定義されています。フラグを使えば、ディスクリプタが何のためにあるのか、もっと簡単にわかるはずです!
//! initialize gdt int i86_gdt_initialize () { //! set up gdtr _gdtr.m_limit = (sizeof (struct gdt_descriptor) * MAX_DESCRIPTORS)-1; _gdtr.m_base = (uint32_t)&_gdt[0]; //! set null descriptor gdt_set_descriptor(0, 0, 0, 0, 0); //! set default code descriptor gdt_set_descriptor (1,0,0xffffffff, I86_GDT_DESC_READWRITE|I86_GDT_DESC_EXEC_CODE|I86_GDT_DESC_CODEDATA|I86_GDT_DESC_MEMORY, I86_GDT_GRAND_4K | I86_GDT_GRAND_32BIT | I86_GDT_GRAND_LIMITHI_MASK); //! set default data descriptor gdt_set_descriptor (2,0,0xffffffff, I86_GDT_DESC_READWRITE|I86_GDT_DESC_CODEDATA|I86_GDT_DESC_MEMORY, I86_GDT_GRAND_4K | I86_GDT_GRAND_32BIT | I86_GDT_GRAND_LIMITHI_MASK); //! install gdtr gdt_install (); return 0; }

Hal: インタラプトディスクリプタテーブル

ここが面白いところです!IDTインタフェースは、idt.hとidt.cppのソースファイルの中にあります。

hal.h - idt_descriptor

これは割込みディスクリプタの構造です。このチュートリアルで見たディスクリプタの形式と比較してみてください。
#ifdef _MSC_VER #pragma pack (push, 1) #endif //! interrupt descriptor struct idt_descriptor { //! bits 0-16 of interrupt routine (ir) address uint16_t baseLo; //! code selector in gdt uint16_t sel; //! reserved, shold be 0 uint8_t reserved; //! bit flags. Set with flags above uint8_t flags; //! bits 16-32 of ir address uint16_t baseHi; }; #ifdef _MSC_VER #pragma pack (pop, 1) #endif
各メンバが何を表し、割込みディスクリプタのどの位置にあるかを見てみましょう。
  • baseLo- 割り込みルーチン(IR)のベースアドレスの最初の16ビットです。
    • これは全体の割り込み記述子内のビット0~15です。割り込みディスクリプタに記載されている表と比較してください。構造
  • sel- セグメントセレクタ
    • 割り込みディスクリプタ全体のうち、16~31ビット目です。
  • reserved- えー...非常に有用な情報です。)
    • これは、割り込みディスクリプタ全体の31-38ビットです。
  • flags- 面白い情報があるところです!
    • 割り込みディスクリプタの39-41ビットです。これは、ビットフラグがある場所です。
    • インタラプトディスクリプタ ビート42-45。これはDPL(Descriptor Priveldge Level)です。
  • baseHi- IRのベースアドレスのビット16-31。
    • これは、全体の割り込みディスクリプタ内のビット46-64です。
簡単ですね。この構造体は、割り込みディスクリプタのレイアウトと完全に一致していることに注意してください。 さて、割り込みディスクリプタの説明ができたので、IDTをインストールする方法を見てみましょう。

idt.cpp - idtr

gdtr 構造体と同じように、idtr 構造体も用意しました。この構造体は、idtr レジスタの構造体と全く同じであることに注意してください。
#ifdef _MSC_VER #pragma pack (push, 1) #endif //! describes the structure for the processors idtr register struct idtr { //! size of the interrupt descriptor table (idt) uint16_t limit; //! base address of idt uint32_t base; }; #ifdef _MSC_VER #pragma pack (pop, 1) #endif //! interrupt descriptor table static struct idt_descriptor _idt [I86_MAX_INTERRUPTS]; //! idtr structure used to help define the cpu's idtr register static struct idtr _idtr;
さて...IDT は割り込みディスクリプタの配列に過ぎないことを思い出してください。これによって、_idtrは参照用として、プロセッサのIDTRレジスタに現在の情報を保存し、私たちが使用できるようにします。基本的に、私たちがここからしなければならないことは、IDTと_idtrをセットアップすることです; それからIDTをインストールすることです!難しいことではありません :)

idt_install() - 新しい IDT をインストールします。

これはIDTをIDTRにインストールするために使用されます。これは、コンパイラ間の移植性を高めるために、インラインアセンブリ言語(コンパイラに依存します)を共通のインターフェースの後ろに抽象化するために使用されるヘルパーメソッドです。
//! installs idtr into processors idtr register static void idt_install () { #ifdef _MSC_VER _asm lidt [_idtr] #endif}

i86_default_handler() - デフォルトのインタラプトハンドラ

私たちのIDTインタフェースは、独自の割り込み処理ルーチンをIDTに直接インストールする方法を提供します。256個の割り込みがあるので、256個の割り込みハンドラがあります。そのため、256個の割り込みがあり、256個の割り込みハンドラがあります。では、カーネルがまだ扱っていない割り込みが発生した場合はどうなるのでしょうか?

これはそのためのものです!これは IDT インターフェースがインストールする基本的な未処理の例外ハンドラです (これについては後で説明します。) このハンドラが行うことは、デバッグモード用にビルドされている場合、エラーを表示することだけです。そして、システムを停止させます。

//! default handler to catch unhandled system interrupts. void i86_default_handler () { #ifdef _DEBUG DebugClrScr (0x18); DebugGotoXY (0,0); DebugSetColor (0x1e); DebugPrintf ("*** [i86 Hal] i86_default_handler: Unhandled Exception"); #endif for(;;); }

割り込みから復帰する...

CやC++では、IRから戻るときに自動的にスタックから値をポップアウトしてRET命令を発行しています。これはまずい!このため、IRET命令で復帰する方法を独自に発行する必要があります。

geninterrupt() - 割り込みコールを生成する

これはちょっとやっかいです。これは、より多くのコンパイラの移植性を高めるために、インラインアセンブリ言語を共通のインタフェースの背後に抽象化するために提供される別のヘルパーメソッドです。しかし、それはまた、アブリタリ割り込みコールを生成するという課題を隠蔽するものでもあります。

問題は、割り込み(INT命令)のOPCodeは1つのフォーマットしかないことです。0xCDimm(immは中間値)です。このため、INT命令ではレジスタもメモリも使用することができません。もちろん、いろいろな方法があります。私は、自己修正コードという、早くて小さな解決策を選びました。

基本的には、INT OPCodeの2バイト目を変更すればよいのです。これは常に2バイト(1バイト目は0xCD、2バイト目は呼び出す割り込み番号)であることを知っていれば、非常に簡単な解決方法です。

//! generate interrupt call void geninterrupt (int n) { #ifdef _MSC_VER _asm { mov al, byte ptr [n] mov byte ptr [genint+1], al jmp genint genint: int 0 // above code modifies the 0 to int number to generate } #endif }

i86_install_ir () - 割り込みハンドラをIDTにインストールする

これは少し厄介ですが、それほど難しいことではありません。構造体のbaseLobaseHiメンバは割り込みルーチン (IR) のハイビットとロービットを含んでいることを思い出してください。つまり、IR関数のアドレスを取得し、そのハイビットとロービットを格納するだけでよいのです。これは、関数ポインタによって行われます。

パラメータとして関数ポインタを渡します。このルーチンはポインタが指す関数のアドレスを取得し、ロービットとハイビットをマスクして_idt [i] の構造体に格納します(i は IDT のディスクリプタオフセット(割り込み番号))。

//! installs a new interrupt handler int i86_install_ir (uint32_t i, uint16_t flags, uint16_t sel, I86_IRQ_HANDLER irq) { if (i>I86_MAX_INTERRUPTS) return 0; if (!irq) return 0; //! get base address of interrupt handler uint64_t uiBase = (uint64_t)&(*irq); //! store base address into idt _idt[i].baseLo = uiBase & 0xffff; _idt[i].baseHi = (uiBase >> 16) & 0xffff; _idt[i].reserved = 0; _idt[i].flags = flags; _idt[i].sel = sel; return 0; }
これには少し意味があります。割り込みが発生したとき、プロセッサが情報をスタックにプッシュしてくれたのを覚えていますか?この情報は、このルーチンが呼び出されたときに、パラメータ・リストに表示されます。ただし、エラーコードをプッシュする割り込みもあれば、しない割り込みもあるので、注意が必要です。

i86_idt_initialize () - IDTインタフェースの初期化

さて、すべてをまとめましょう。次のコードはIDTRをセットアップし、すべての割り込みをキャッチするデフォルトの割り込みハンドラをセットし(これはカーネルに必要な割り込みだけを定義するためです)、最後に上記のメソッドを使ってIDTをインストールするものです。

IDTのセットアップに使用するビット・フラグはidt.hで定義されており、コードを読みやすく、修正しやすくするために提供されています。

//! initialize idt int i86_idt_initialize (uint16_t codeSel) { //! set up idtr for processor _idtr.limit = sizeof (struct idt_descriptor) * I86_MAX_INTERRUPTS -1; _idtr.base = (uint32_t)&_idt[0]; //! null out the idt memset ((void*)&_idt[0], 0, sizeof (idt_descriptor) * I86_MAX_INTERRUPTS-1); //! register default handlers for (int i=0; i<I86_MAX_INTERRUPTS; i++) i86_install_ir (i, I86_IDT_DESC_PRESENT | I86_IDT_DESC_BIT32, codeSel, (I86_IRQ_HANDLER)i86_default_handler); //! install our idt idt_install (); return 0; }

デモのまとめ

このデモは少し複雑です。少なくとも、醜い必要なものは手に入れました。INT 命令を発行すると、デフォルトのハンドラが呼び出されるのがわかると思います。もし、自分で割り込みハンドラをインストールしたら、エラーコードのあるものとないもの の両方を試してみてください。割り込みが発生するのがわかると思います。geninterrupt() や INT 命令を呼び出すと、いつでも正しい割り込みハンドラ (または割り込みハンドラが定義されていない場合はデフォルトのハンドラ) が実行されるのがわかると思います。

このチュートリアルがもっと複雑にならないように、まだハードウェア割り込みを扱わないことにしました。次回のチュートリアルでは、カーネルのシステムタイマとして使用する8253 Programmable Interval Timer (PIT)のコードと、ハードウェア割り込みに必要な8259A Programmable Interrupt Controller のコードを開発するのと同様に、これをカバーする予定です。

デモをよく研究し、すべてがどのように動くかを研究してください。i86_install_ir()を使って独自の割り込みハンドラを登録し、割り込みを発生させるようにします。これを行うために必要なことは、以下の通りです。

//! our uber 1337 interrupt handler. handles int 5 request void int_handler_5 () { _asm add esp, 12 _asm pushad // do whatever... _asm popad _asm iretd } //! registers our interrupt handler i86_install_ir (5, I86_IDT_DESC_PRESENT | I86_IDT_DESC_BIT32, 0x8, (I86_IRQ_HANDLER)int_handler_5); //! generates int 5 instruction. You can also use inline assembly, of course geninterrupt (5);
割り込みハンドラのパラメータリストはフォーマットが変わる可能性があるので省くことにしました。 つまり、パラメータにアクセスするには、ESPからアクセスする必要があります。後で楽をするために、パラメータを与えることにするかもしれませんが。

まとめ

今回は楽しいことがたくさんありましたね。 このチュートリアルでは、たくさんのことを学びました。このチュートリアルでは多くの重要なトピックを取り上げ、例外と割り込みの処理を取り上げ、システムで割り込みを再び有効にしました。 トリプルフォルトを見るのもこれが最後かもしれません。Woohoo!

このチュートリアルは少し複雑です。OSのプログラミングは楽しいですよね。^_^

次のチュートリアルでは、カーネルをさらに発展させます。8259A PICチュートリアルと同様に、8254プログラマブルインターバルタイマ(PIT)マイクロコントローラでタイミングを処理する予定です。その後、メモリ管理やプロセス管理を行う予定です。基本的なデバッグ用のテキストベースのコンソールを開発することもあるかもしれません。