割り込みの仕組みについて

新年明けましておめでとうございます!









さて、「はじめて読む486」を2,3週間前に購入し、東京旅行の移動時間や

学校の課題の時間の合間を縫ってなんとか読み進めていきました。



とりあえず「7章 割り込み」まではなんとかたどり着いたのですが、

やはり何度やっても割り込みがうまくいかない^^;



何か見落としている抜け穴があるかもしれないので、

自分が身に付けた知識の整理がてらに、まとめてみようと思います。



以下、前提知識としてセグメントの理解はほぼ必須かと思われます。


○割り込みとは

といっても、根本的なところは省略します。今回の本質ではありませんので。

要するに、ハードウェアなど入力を定期的に監視する方針だと処理上で問題が起こるので、

入力があったときに割り込んでくれ、という仕組みです。



割込みにはこんな種類があります。




ハードウェア割り込み

周辺機器からの信号によって発生

トラップ

INT、INTO命令の実行によって発生(ソフトウェア割り込み)

フォールト

保護機能、MMU(メモリ管理ユニット)のチェックによって発生

アボート

処理を続けられないエラーが起きた場合に発生


とまあこんな感じです。下の三つをまとめて例外割込みといいます。



また、割り込みはその種類によって0〜255の一意な番号で、

割り込みを処理する割り込みハンドラ対応付けられます。

これを割り込み番号とか割り込みベクタだとかいいます。



※486本では「割り込みハンドラ」は「割り込み処理ルーチン」となっていますが、

今は此方の方が一般的なので、以下すべて「割り込みハンドラ」とします。




さらに、CPUが割り込み信号を受け取っても、EFLAGSレジスタの第9ビット目、

IFビットがセット(IF=1)されていないと、割り込みは発生しません。

IFビットを立てるにはSTI命令(Set Interrupt Flag)、クリアするにはCLI命令(Clear Interrupt Flag)

使います。





○IDT

リアルモードでの割り込みの仕組みは省略し、プロテクトモードにおける割り込みのみに話を絞ります。

前述の通り、割り込み番号は割り込みハンドラと一意対応させられています。

これは、プログラマ割り込みディスクリプタテーブル(IDT)を使って設定します。



割り込みディスクリプタテーブルは、ゲートディスクリプタの配列になっています。

(というか、メモリ上に連続配置されてるだけです…配列自体がそうなんですが)

ゲートディスクリプタの主な役割は、割り込みハンドラのアドレスを保持することです。



ゲートディスクリプタの構造は次のように、構造体で表すことができます。



struct {

  short offset_low;

  short selector;

  char count;

  char type;

  short offset_high;

};



セレクタ(16bitで、selectorに格納)

 保持している割り込みハンドラの存在するセグメント番号。



・オフセット(32bitで、上位16bitはoffset_high、下位16bitはoffset_lowに格納)

 保持している割り込みハンドラの、「セレクタが示すセグメント」の先頭からのアドレス。



・種類(8bitで、typeに格納)

 とりあえず0x8E。あまり深く考えなくてよい。(特権レベル0で32bitの割り込みゲート)



countはコピーカウントを表すそうですが、とりあえず現段階ではあまり考える必要はないとのこと。



まあこの説明じゃわかりにくいのも当然なので…

gd.png

こんな感じです。割り込み番号0x01番に対応する割り込みハンドラは、

セレクタ0x08、オフセット0x250ならば…

割り込みハンドラはGDTの0x08番目のディスクリプタに入ってるベース番地から0x250

ところにあります。





○割り込みの仕組み

割り込み通知がCPUに来たとき、CPUは割り込み番号に対応する割り込みハンドラを

呼び出そうとします。そのためにはIDTを読み込まなければならないので、

CPUはメモリ上に置かれたIDTの場所を知らなければいけません。



IDTの先頭アドレスとサイズ(リミット)を記録するレジスタが、IDTRレジスタです。



これは、48bitのシステムレジスタで、上位16bitがIDTのリミット値、

つまり、IDTに格納されているディスクリプタの数-1で、

下位32bitがIDTの先頭アドレスです。



48bitのレジスタなので、MOV命令では代入できません。

そのため、専用のLIDT命令を使います。



メモリ上にリミット値16bitと先頭アドレス32bitを連続して配置させ、リミット値16bitの

先頭アドレスをLIDT命令のオペランドに指定します。



lidt.png

これで、CPUは割り込みが発生したときにIDTRからアドレスを読みだしてIDTを読み込みます。



IDTを読み込んだ後ですが…



割り込み番号に対応するディスクリプタを読みだします。

そのディスクリプタのオフセットをeipレジスタセレクタをcsレジスタにセットします。



load.png

なぜなら、csレジスタに値をセットすると、その値をセレクタとして読みだしたセグメントを実行し、

eipレジスタは、現在実行中のセグメントの中で、次に実行すべき命令のオフセットアドレスを

表すからです。



要するに、このようにセットすることでCPUは勝手に割り込みハンドラを実行するのです。







さて、割り込みハンドラはRET命令ではなく、IRET命令でリターンしなければならない、と言われます。

これがその理由です。



割り込みハンドラはCPUが勝手にcallします。

普通のcall命令は、eipをpushして、指定されたメモリアドレスにjmp、を実行します。

ところが、割り込みハンドラのcallでは、eipの他にcsも保存しなければなりません。

そのため、eipとcsをpushしてjmpするのです。



普通のRET命令はeipをpopすることで元の命令に戻ります。

割り込みハンドラからリターンするときは、別のセグメントに移動しているので、

csも元通りにしなければなりません。そのため、csとeipをpopするIRET命令を使います。





<まとめ>



interrupt.png



と理解しております…。



しかし割り込みが発生しない…。