Operating Systems Development Series | |
Errors, Exceptions, Interruptions
注意:
このチュートリアルはソフトウェア割込み処理について述べており、ハードウェア割込み処理については述べていません。もし、ハードウェア割り込みをお探しなら、8259A
PICチュートリアルをご覧ください。ハードウェア割り込みのソフトウェア側での処理については、こちらで説明しています。
はじめにお帰りなさい。:)前回のチュートリアルで、私たちはシステムの基本的な設計をカバーしました。そう、ここまではかなり基本的で簡単だったでしょう? いつになったらシステムレベルのコードに戻るのだろう、と思っているかもしれません。さて....*お帰りなさい:) このチュートリアルでは、非常に重要な概念を扱います。エラー処理エラー処理には、単に問題を処理するだけでなく、問題を捕捉することも含まれます。例外 処理には 割り込みが必要なので、割り込み処理も扱います。 割り込みはアーキテクチャに依存します。このため、私たちは、1337という巨大な、しかし(現時点では)非常に空のハードウェア抽象化レイヤーを通して割り込みを管理するためのインタフェースを開発し、私たち自身のトラップゲートをインストールするためにカーネルとインタフェースをとる予定です。これはプロセッサ例外エラーをキャッチし、現在そして永久にトリプルフォルトを防ぐために使われるとともに、完全にハードウェア独立であることができます。 面白そうでしょ?では、ここからが本題です。
...いろいろありますね、では始めましょう。 エラー、エラー、エラーさて、現実を直視しましょう。完璧な人間なんていません。コンピュータの場合、これはさらに真実です。私たちはカーネルランドという素晴らしい世界で仕事をしているので、単純なエラーがソフトウェアやハードウェアに予測できない問題を引き起こす可能性があるため、状況はさらに悪化します。読者の多くは、すでに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)から復帰するために使用されます。 例えば、ここではソフトウェア命令で割り込みを発生させます。 これらの命令は、ソフトウェア割込みを生成し、ソフトウェアで割込みルーチン(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バイトで、以下のような形式です。
では、IVTについて見ていきましょう。最初の数個の割り込みは予約されており、そのままです。
ハードではありません。これらの割り込みは、それぞれ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のディスクリプタは以下のような形式をとります。どのような種類の記述子であるかによって、一部フォーマットが変わります。
それだけですか?そうです、これで全部です。) あとは、GDTと同じように、IDTを埋めて、インストールするだけです。IDTはGDTよりずっとシンプルなので、さらに簡単です :) 上のリストが完全な記述形式です。ここでは、割り込みゲートを開発することだけを考え、それだけに焦点を当てます。 割り込みディスクリプタ。例GDTと同じように、ビットレベルで例を作り、すべてがどのように動くかを正確に説明するのに役立ちます。まず、割込みディスクリプタの例を見てみましょう。これはアセンブリで表示されるので、よりよく全体を見ることができます。 そう、これがディスクリプタのすべてなんだ。そんなに難しくはないでしょう? 上の表とどのような関係があるのか、各ビットを分解して見てみましょう。 これが私たちのディスクリプタですが、バイナリ形式です。ほとんどの部分はすべて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ビットずつ分解してみましょう。 さて...今、私たちはビット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レジスタは次のような形式になっています。
十分シンプルでしょう?IDTのベースアドレスはこのレジスタに格納されています。 プロセッサはこのレジスタを使って、IDTがどこにあるかを判断しています。 このレジスタにはリミットとベース・アドレスの両方が格納されているため、このフォーマットを知っておくことは非常に重要です。このため、単にIDTのベースアドレスを与えても動作しません。この問題は、通常、次のようなフォーマットで新しい構造体を作成することで解決されます。
あれ、このレジスタはどうやってアクセスするんだっけ?そうなんです。 LIDT命令 - IDTをロードするこの命令は、IDTの新しいアドレスをIDTRレジスタに格納するために使用されます。この命令は、電流保護レベル(CPL)が0(Ring0)の場合のみ使用できます。使い方はとても簡単です。それがすべてです。idt_baseがIDTのベースアドレスであれば、そのアドレスがIDTRにコピーされます。 SIDT命令 - IDTを格納するこの命令はIDTRの値を6バイトのメモリに格納するために使用されます。この命令は、リング0とリング3の両方のアプリケーションで使用することができます。
割り込みの仕組み詳細呼び出す割込みプロシージャを探す割り込みや例外が発生すると、プロセッサはその例外番号や割り込み番号をIDTのインデックスとして使用します。 ご存知のように、IDTは上図のような256個のディスクリプタの配列に過ぎません。IDTR.baseAddressはIDTRの上位ビットに格納されているIDTのベースアドレスで、IDTR.indexは割込み番号です。これにより、プロセッサは割り込みハンドラのディスクリプタのインデックスのベースアドレスを取得することができます。IDTR.limitに格納されているIDTのリミットサイズよりも計算値が大きい場合、プロセッサはIDTのサイズを超える呼び出しになるため、GPF(General Protection Fault)を実行します。ディスクリプタは割込み、トラップ、タスクゲートのいずれかであることを忘れないでください。インデックスが割込みゲートまたはトラップゲートを指している場合、プロ セッサは例外ハンドラまたは割込みハンドラを呼び出します。これはコールゲートをCALLするのと同じように行われます。インデックスがタスクゲートを指している場合、プロセッサはタスクゲートへのCALLと同様に、例外または割込みハンドラタスクへのタスクスイッチを実行します。 ハンドラの情報とアドレスは、この記述子内に格納されます。プロセッサがスイッチを実行するとき ハンドラの実行
割り込みハンドラが呼ばれたとき、スタックがどのようにプッシュされるのか、また、どのような例外がエラーコードをプッシュするのかを知ることは非常に重要なことです。この点については、次に説明します。 割込みハンドラの内部割込みハンドラの位置はディスクリプタに格納されているので、プロセッサはハンドラを実行することができるようになりました。ご存知のように、プロセッサはハンドラを実行するとき、いくつかの特別な情報をスタックにプッシュします。もしハンドラが同じリングレベルで実行されているなら(そうなる)、プロセッサは EFLAGS、CS、EIP、エラーコードを 現在のスタックにプッシュすることを覚えておく必要があります。これにより、もし実行可能であれば、実行を継続することができます。 これらのことをまとめると、ハンドラが呼ばれたとき、スタックは次のようにセットアップされることになります。 この情報を元にハンドラから戻り、何が原因で例外が発生したのか(エラーコードがある場合)を判断します。 割り込みハンドラ内部エラーコードの書式また、エラーコードがスタックに格納されている場合は、その情報を元にエラーの原因を特定します。エラーコードは以下のようなフォーマットになっています。
エラーコードは、外部で発生した例外(INTR、LINT0、LINT1ピン経由)、またはINT n命令ではスタックにプッシュされません。 ページフォルト例外エラーの場合、エラーコードの形式が異なります。それについては、次のセクションで見ていきます。 ハンドラからの復帰すべてのハンドラは、IRETか IRETDのどちらかの命令を使って戻らなければなりません。IRETは、保存されたEFLAGS(ハンドラの実行時にスタックにプッシュされた)を復元することと、EFLAGSのIOPLフィールドが、現在の保護レベル(CPL)が0の場合にのみ0に設定されることを除いて、RETと同じです。 IFフラグは、CPLがIOPL以下の場合のみ変更されます。ハンドラ実行時にスタックスイッチが発生した場合、IRETは中断された手順のスタックにも切り替えます。 x86の例外例外をリストアップすべての例外は、IVTまたはIDT内の最初の数回の割り込みとして定義されています。x86クラスのプロセッサから生成される例外の完全なリストを以下に示します。
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.hとidt.cpp の中には、ここで学んだことをカバーする新しいコードもあります:割り込み記述子テーブル (IDT).これはまた、私たちのハードウェア抽象化レイヤ(HAL)の始まりでもあります! ご存知のように、私はこの連載を始めるにあたって、ハードウェアの抽象化とその重要性を強調してきました。その理由は、HALの構築を続けていくうちに、すぐにおわかりいただけると思います。Hal をカーネルから完全に独立させることの利点も、ここでわかるかもしれませんね。 さて、それでは HAL の主要なインターフェースの始まりを見てみましょう。 Hal - include/hal.h - HAL のためのプラットフォーム非依存型インターフェースこれはHALとカーネルとの間のインターフェースです。これは標準のインクルードディレクトリの一部であり、その実装とは完全に分離されています。ヘッダーファイルは、その中のルーチンを定義する任意のインプリメンテーションによって使用されることを意図しているため、すべてのルーチンはexternと宣言されています。インプリメンテーションはアーキテクチャに依存しますが、インターフェースは特定のインプリメンテーションに結合されることはなく、完全にハードウェアに依存しないものとなっています。実装自体はアーキテクチャに依存しますが、我々は単純に異なるアーキテクチャのための実装を構築することができます。各インプリメンテーションはこの共通のインターフェースを使い、(hal.dllのような)ダイナミックローディングをサポートできるので、a)異なるアーキテクチャ用に構築するときにどの静的HALインプリメンテーションを使うかリンクするか、b) 異なるHALを独立して構築し、スタートアップ時にどのHALを使用するかを選択できるのです。これらはすべて同じインタフェース (Hal.h) を使っているので、異なる実装 (したがって異なるハードウェアセットアップ) を使うためにカーネルを変更する必要はありません。 今のところ2つの関数しかありません。必要ならもっと追加する予定です。 これらのルーチンのプロトタイプは、スタートアップとシャットダウンのパラメータを指定できるように変更する予定です。いずれにせよ、これらは非常に汎用的なルーチンで、インプリメンテーションのために必要であれば、ハードウェアのセットアップとシャットダウンの方法を提供することを意図しています。 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のディスクリプタの形式は何だったっけ?そう、じゃあ、いいや...。
はいはい、もうやめときますね :)しかし、真面目な話、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ビットの大きさが保証されていることも覚えておいてください。 簡単ですね。構造体内のフラグバイトを構築するために設定できるビットフラグがたくさんあります。 ヘッダーファイルを見て、それらがどのように機能するかを確認してください。基本的には、設定したいビットフラグをビットごとに OR することになります。次のセクションで、この方法を説明します。 gdtrの抽象化チュートリアルで、プロテクトモード、gdt、gdtrについて説明しました。 gdtrは、使用するGDTを指定するためのプロセッサ内部のレジスターです。48ビットのポインターで、次のような書式に従わなければなりません。
ここで新しいGDTと_gdtrを見ることができますが、これはプロセッサのGDTRレジスタをセットアップするときに参照するために使用されます。 gdt_install():gdtr に gdt をインストールする。このルーチンは非常に単純なものです。lgdt 命令を使用して GDTR を gdtr ポインタでロードしているだけです。 CS は決して変化しないので、ここで far jump を行う必要はありません。
gdt_set_descriptor():gdt に新しいディスクリプタを設定します。このルーチンはGDTに新しいディスクリプタをインストールするために使用されます。ほとんどの場合、これはそれほど難しいことではありません。
i86_gdt_initialize() - gdt を初期化する。このルーチンはすべてを一つにまとめます。GDTR 構造を設定し、いくつかのデフォルトのディスクリプタを GDT にインストールし、最後に GDT をインストールするだけです。この GDT は、ブートローダと同じものです。ベースアドレスは0、リミット(最大アドレス)は4GB(0xffffff)です。フラグはすべてgdt.hで定義されています。これらは可読性を高め、醜いマジックナンバーを取り除くために定義されています。フラグを使えば、ディスクリプタが何のためにあるのか、もっと簡単にわかるはずです!
Hal: インタラプトディスクリプタテーブルここが面白いところです!IDTインタフェースは、idt.hとidt.cppのソースファイルの中にあります。hal.h - idt_descriptorこれは割込みディスクリプタの構造です。このチュートリアルで見たディスクリプタの形式と比較してみてください。各メンバが何を表し、割込みディスクリプタのどの位置にあるかを見てみましょう。
idt.cpp - idtrgdtr 構造体と同じように、idtr 構造体も用意しました。この構造体は、idtr レジスタの構造体と全く同じであることに注意してください。さて...IDT は割り込みディスクリプタの配列に過ぎないことを思い出してください。これによって、_idtrは参照用として、プロセッサのIDTRレジスタに現在の情報を保存し、私たちが使用できるようにします。基本的に、私たちがここからしなければならないことは、IDTと_idtrをセットアップすることです; それからIDTをインストールすることです!難しいことではありません :) idt_install() - 新しい IDT をインストールします。これはIDTをIDTRにインストールするために使用されます。これは、コンパイラ間の移植性を高めるために、インラインアセンブリ言語(コンパイラに依存します)を共通のインターフェースの後ろに抽象化するために使用されるヘルパーメソッドです。
i86_default_handler() - デフォルトのインタラプトハンドラ私たちのIDTインタフェースは、独自の割り込み処理ルーチンをIDTに直接インストールする方法を提供します。256個の割り込みがあるので、256個の割り込みハンドラがあります。そのため、256個の割り込みがあり、256個の割り込みハンドラがあります。では、カーネルがまだ扱っていない割り込みが発生した場合はどうなるのでしょうか?これはそのためのものです!これは IDT インターフェースがインストールする基本的な未処理の例外ハンドラです (これについては後で説明します。) このハンドラが行うことは、デバッグモード用にビルドされている場合、エラーを表示することだけです。そして、システムを停止させます。
割り込みから復帰する... CやC++では、IRから戻るときに自動的にスタックから値をポップアウトしてRET命令を発行しています。これはまずい!このため、IRET命令で復帰する方法を独自に発行する必要があります。 geninterrupt() - 割り込みコールを生成するこれはちょっとやっかいです。これは、より多くのコンパイラの移植性を高めるために、インラインアセンブリ言語を共通のインタフェースの背後に抽象化するために提供される別のヘルパーメソッドです。しかし、それはまた、アブリタリ割り込みコールを生成するという課題を隠蔽するものでもあります。問題は、割り込み(INT命令)のOPCodeは1つのフォーマットしかないことです。0xCDimm(immは中間値)です。このため、INT命令ではレジスタもメモリも使用することができません。もちろん、いろいろな方法があります。私は、自己修正コードという、早くて小さな解決策を選びました。 基本的には、INT OPCodeの2バイト目を変更すればよいのです。これは常に2バイト(1バイト目は0xCD、2バイト目は呼び出す割り込み番号)であることを知っていれば、非常に簡単な解決方法です。
i86_install_ir () - 割り込みハンドラをIDTにインストールするこれは少し厄介ですが、それほど難しいことではありません。構造体のbaseLoとbaseHiメンバは割り込みルーチン (IR) のハイビットとロービットを含んでいることを思い出してください。つまり、IR関数のアドレスを取得し、そのハイビットとロービットを格納するだけでよいのです。これは、関数ポインタによって行われます。パラメータとして関数ポインタを渡します。このルーチンはポインタが指す関数のアドレスを取得し、ロービットとハイビットをマスクして_idt [i] の構造体に格納します(i は IDT のディスクリプタオフセット(割り込み番号))。 これには少し意味があります。割り込みが発生したとき、プロセッサが情報をスタックにプッシュしてくれたのを覚えていますか?この情報は、このルーチンが呼び出されたときに、パラメータ・リストに表示されます。ただし、エラーコードをプッシュする割り込みもあれば、しない割り込みもあるので、注意が必要です。 i86_idt_initialize () - IDTインタフェースの初期化さて、すべてをまとめましょう。次のコードはIDTRをセットアップし、すべての割り込みをキャッチするデフォルトの割り込みハンドラをセットし(これはカーネルに必要な割り込みだけを定義するためです)、最後に上記のメソッドを使ってIDTをインストールするものです。IDTのセットアップに使用するビット・フラグはidt.hで定義されており、コードを読みやすく、修正しやすくするために提供されています。
デモのまとめこのデモは少し複雑です。少なくとも、醜い必要なものは手に入れました。INT 命令を発行すると、デフォルトのハンドラが呼び出されるのがわかると思います。もし、自分で割り込みハンドラをインストールしたら、エラーコードのあるものとないもの の両方を試してみてください。割り込みが発生するのがわかると思います。geninterrupt() や INT 命令を呼び出すと、いつでも正しい割り込みハンドラ (または割り込みハンドラが定義されていない場合はデフォルトのハンドラ) が実行されるのがわかると思います。このチュートリアルがもっと複雑にならないように、まだハードウェア割り込みを扱わないことにしました。次回のチュートリアルでは、カーネルのシステムタイマとして使用する8253 Programmable Interval Timer (PIT)のコードと、ハードウェア割り込みに必要な8259A Programmable Interrupt Controller のコードを開発するのと同様に、これをカバーする予定です。 デモをよく研究し、すべてがどのように動くかを研究してください。i86_install_ir()を使って独自の割り込みハンドラを登録し、割り込みを発生させるようにします。これを行うために必要なことは、以下の通りです。 割り込みハンドラのパラメータリストはフォーマットが変わる可能性があるので省くことにしました。 つまり、パラメータにアクセスするには、ESPからアクセスする必要があります。後で楽をするために、パラメータを与えることにするかもしれませんが。 まとめ今回は楽しいことがたくさんありましたね。 このチュートリアルでは、たくさんのことを学びました。このチュートリアルでは多くの重要なトピックを取り上げ、例外と割り込みの処理を取り上げ、システムで割り込みを再び有効にしました。 トリプルフォルトを見るのもこれが最後かもしれません。Woohoo!このチュートリアルは少し複雑です。OSのプログラミングは楽しいですよね。^_^ 次のチュートリアルでは、カーネルをさらに発展させます。8259A PICチュートリアルと同様に、8254プログラマブルインターバルタイマ(PIT)マイクロコントローラでタイミングを処理する予定です。その後、メモリ管理やプロセス管理を行う予定です。基本的なデバッグ用のテキストベースのコンソールを開発することもあるかもしれません。 |