レガシーガジェット研究所

気になったことのあれこれ。

Linux Kernel ~ 割り込みと例外 例外の種類と割り込みディスクリプタ ~

概要

「詳解Linux Kernel」を参考にVersion 2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelx86とする。

今回は例外の種類及び割り込みディスクリプタの構造などについて見ていく。

例外

x86では約20種類の例外を発生させる。それぞれに例外ハンドラが用意されており、例外ハンドラは通常例外を発生させたプロセスにUNIXシグナルを送信する。

例外番号 例外名 説明 例外ハンドラ シグナル
0 除算エラー(Devide Error) フォルト。0で整数除算すると発生する。 divide_error() SIGFPE
1 デバッグ例外(Debug) トラップ又はフォルト。eflagレジスタのTFフラグを設定すると発生する。 debug() SIGTRAP
2 未使用 マスク不可割り込み用に予約。(NMIピンを使用) nmi()
3 ブレークポイント(Break Point) トラップ。int3命令(ブレークポイント)によって発生する。通常はデバッガが埋め込む。 int3() SIGTRAP
4 オーバフロー(Overflow) トラップ。into命令(オーバーフローを検出する)を実行した時にeflagレジスタのOF(overflow)フラグが設定されている場合に発生する。 overflow() SIGSEGV
5 範囲超過(Bounds Check) フォルト。bound命令(アドレス範囲を調べる)をアドレスの有効範囲外にあるオペランドと共に実行すると発生する。 bounds() SIGSEGV
6 無効オペコード(Invalid Opcode) フォルト。CPUが無効なオペコードを検出する。 invalid_op() SIGILL
7 バイス使用不可(Device Not Available) フォルトESCAPE命令やMMX命令、又はSSE/SSE2命令を実行した時にcr0レジスタのTSフラグが設定されている場合に発生する。 device_not_available()
8 ダブルフォルト(Double Fault) アボート。CPUが例外ハンドラを呼び出す時に別の例外を検出すると通常は順次処理されるが、何らかの理由により処理できない場合に呼び出される。 doublefault_fn()
9 コアプロセッサセグメントオーバラン(Coprocessor Segment Overrun) アボート。外部算術演算コアプロセッサで問題が起こった場合に発生する。 coprocessor_segment_overrun() SIGFPE
10 無効TSS(Invalid TSS) フォルト。無効なタスク状態セグメント(TSS)を持つプロセスに切り替えようとした場合に発生する。 invalid_TSS() SIGSEGV
11 セグメント不在(Segment Not Present) フォルト。メモリ中に存在しないセグメントを参照した場合に発生する。 segment_not_present() SIGBUS
12 スタックセグメントフォルト(Stack Segment Fault) フォルト。命令がスタックセグメントの範囲を越えようとした場合、又はSSレジスタがさすセグメントがメモリに存在しない場合に発生する。 stack_segment() SIGBUS
13 一般保護例外(General Protection) フォルト。x86のプロテクトモードの保護規則に違反した場合に発生する。 general_protection() SIGSEGV
14 ページフォルト(Page Fault) フォルト。参照されたページがメモリに存在しない場合、もしくは対応sルウページテーブルのエントリがNULLか、又はページング保護機構に違反した場合に発生する。 page_fault() SIGSEGV
15 予約済み Interlが予約している。
16 浮動小数点エラー(Floating Point Error) フォルト。CPUに組み込まれている浮動小数点回路がエラーを検出した場合に発生。算術オーパフローやゼロ除算など。 coprocessor_error() SIGFPE
17 アラインメントチェック(Alignment Check) フォルト。オペランドのアドレスが正しくアラインされていない場合に発生する。 alignment_check() SIGBUS
18 マシンチェック(Machine Check) アボート。マシンチェック機構がCPUやバスのエラーを検出した場合に発生する。 machine_chekc()
19 SIMD浮動小数点例外(SIMD Floating Point Execption) フォルト。CPUに組み込まれているSSEやSSE2回路が浮動小数点演算でエラーを検出した場合に発生する。 simd_coprocessor_error() SIGPE

20 ~ 31の値はIntelが将来のために予約している。

割り込みディスクリプタテーブル

割り込みディスクリプタテーブル(IDT: Interrupt Descriptor Table)というテーブルは割り込みや例外のベクタとハンドラのアドレスの対応表である。

IDT自体のフォーマットはGDTやLDTとほぼ同じで、エントリは8バイト、エントリの最大数は256個なので、2048バイト(8 * 256)必要となる。Linuxでは例外に32エントリ、割り込みに224エントリあてられる。

IDTはメモリの任意の場所に配置することが可能で、そのアドレスと上限サイズはCPUのidtr(Interrupt Descritor Table Register)が保持している。割り込みを使用する前にlidt(Load Interrupt Descriptor Table)アセンブリ命令で初期化する必要がある。

IDTは以下のように定義されている。

// arch/i386/kernel/traps.c
/*
 * The IDT has to be page-aligned to simplify the Pentium
 * F0 0F bug workaround.. We have a special link segment
 * for this.
 */
struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };

ディスクリプタには以下の3種類が存在する。

  • タスクゲート
  • 割り込みゲート
  • トラップゲート

タスクゲート(Task Gate)

プロセスのTSSを配置する。割り込みが発生した際に現在実行しているプロセスのTSSとリプレイスする。Linuxはタスクゲートを使用しない。

ビット 説明
0-15 予約済み(未使用)
16-31 置き換え対象プロセスのTSSセレクタ
32-39 現在未使用
40-43 エントリのタイプ(タスクゲートでは0101)
44 常に0。未使用
45-46 DPL(Descriptor Privilege Level)
47 当該エントリの有効無効フラグ(1:有効、0:無効)
48-63 予約済み(未使用)

割り込みゲート(Interrupt Gate)

割り込みまたは例外ハンドラの存在するセグメントの、セレクタやオフセットを配置する。当該セグメントに制御を移す間は、プロセッサはEFLAGレジスタのIFフラグを落としマスク可能割り込みを禁止する。

ビット 説明
0-15 bits 当該ゲートがコールされた時に呼び出されるカーネル関数へのアドレスのオフセット(0bit目~15bit目)
16-31 GDT内にあるセグメントディスクリプタへのセグメントセレクタ
32-36 予約済み(未使用)
37-39 常に0(未使用)
40-43 エントリのタイプ(割り込みゲートでは1110)
44 常に0(未使用)
45-46 DPL(Descriptor Privilege Level)
47 当該エントリの有効無効フラグ(1:有効、0:無効)
48-63 当該ゲートがコールされた時に呼び出されるカーネル関数へのアドレスのオフセット(16bit目~31bit目)

トラップゲート

基本的にCPUからの例外をハンドルするために使用される。割り込みゲートとは異なりIFフラグをクリアしない。

ビット 説明
0-15 当該ゲートがコールされた時に呼び出されるカーネル関数へのアドレスのオフセット(0bit目~15bit目)
16-31 GDT内にあるセグメントディスクリプタへのセレクタ
32-36 予約済み(未使用)
37-39 常に0(未使用)
40-43 エントリのタイプ(トラップゲートは1111)
44 常に0(未使用)
45-46 DPL(Descriptor Privilege Level)
47 当該エントリの有効無効フラグ(1:有効、0:無効)
48-63 当該ゲートがコールされた時に呼び出されるカーネル関数へのアドレスのオフセット(16bit目~31bit目)

基本的にLinuxでは割り込み処理に割り込みゲート、例外処理にトラップゲートを使用している。

ハードウェア割り込みと例外の処理

カーネルの初期化が完了しておりCPUがプロテクトモードで動作していると仮定する。csとeipが次に実行する命令の論理アドレスを保持している。次に実行対象である命令を実行する前にCPUは割り込み又は例外が発生していないかを確認し発生していれば以下の処理を行う。(IDTに格納されているのはトラップゲート又は割り込みゲートであることを前提とする)

  • 割り込み又は例外に対応するベクタを取得。
  • idtrレジスタが保持しているIDTのベースアドレスから先ほど取得したベクタをインデックスに対応するエントリ(base_address + (8(entry size) * vector))を取得。
  • gdtrレジスタからGDTのベースアドレスを取得し、GDT内から先ほど取得したIDTエントリが指すセグメントディスクリプタを取得。当該ディスクリプタが割り込み又は例外ハンドラが存在するセグメントのベースアドレスを指す。
  • csレジスタの下位2bitから現行の特権レベル(CPL: Current Privilege Level)を取得し、GDTディスクリプタ内のDPL(Descriptor Privilege Level)と比較する。CPL < DPLの場合には一般保護例外を発生させる。トラップゲートの場合は前述のチェックに加えて、CPLIDTディスクリプタ内のDPLと比較しDPL < CPLの場合一般保護例外を発生させる。
  • 制御回路は新しい特権レベル用のスタックを使用する必要がため、CPLとDPLを比較し特権レベルの変更の有無を確認する。
    • tr(Task Register)レジスタからTSS(Task State Segment)にアクセス。
    • ssとespに新しい特権レベル用のスタックセグメントとスタックポインタを設定。
    • 古い特権レベル用のスタックの論理アドレスを保持するssとespを新しいスタックに退避する。
  • フォルトが発生した場合は例外を発生させた命令の論理アドレスをcsとeipに読み込む。(Page Faultなど)
  • スタックにeflag、cs、eipレジスタの内容を退避する。
  • 例外がハードウェアエラーコードを保持していれば、当該値をスタックに退避する。
  • 取得したIDTエントリが保持するセグメントセレクタとゲートディスクリプタのオフセットフィールドを、それぞれcsとeipに読み込む。

割り込み又は例外処理の完了後はiret命令を使用し、以下の流れで割り込んだプロセスに制御を戻す。

  • スタックに退避されているcs、eip、eflagsレジスタの値を読み込む。スタックにエラーコードが退避されている場合は取り除く。
  • ハンドラのCPLがcsレジスタの下位2bitを等しいかをチェックし、等しければiret命令は実行を終了する。異なる場合は以下の処理に続く。
  • スタックからssとespを読み込むことで古い特権レベルのスタックに戻る。
  • ds、es、fs、gs、セグメントレジスタをチェックし、CPLの値よりも小さいDPLを保持するディスクリプタを参照するセグメントセレクタをクリアする。

例外及び割り込みのネスト

割り込みが発生した際に最初に実行されるのはCPUレジスタの内容をカーネルスタックに退避すること、そして最後に実行されるのはそれらをリストアすることである。だが割り込みは入れ子状態になることもあり、割り込みを抜けた先が割り込みであるといったことが起こる。加えて割り込みの際には退避される情報がカーネルスタックに積まれるためカレントプロセスと密接に結びついている。

大部分の例外はCPUがユーザモードの時に発生するが「ページフォルト」はカーネルモードの場合でも発生する。当該例外はアクセスしたアドレス空間が物理RAMにマッピングされていない場合に発生する。当該例外を処理している間カーネルは他のプロセスを実行することもある。「ページフォルト」ハンドラはさらなる例外を発生させないので。例外によるネストは最大でも2つとなる。(1つ目はシステムコール、2つ目がページフォルト)

I/Oデバイスが発生させる割り込みはカレントプロセスのデータを参照しないような処理のため、どのプロセスで実行されるかがわからない。

割り込みハンドラが別の割り込みハンドラに割り込むことはあるが、逆に例外ハンドラが割り込みハンドラに割り込むことはない。なぜならカーネルモードでの唯一の例外は「ページフォルト」であり、割り込みハンドラ内には当該例外を起こすような処理な行わないからだ。よってプロセスが切り替わることもない。

カーネルが割り込みのネストを許可するのには以下のような理由がある。

  • PICやデバイスドライバスループットを向上させる目的。デバイスコントローラがIRQラインに信号を発した際、PICが信号を外部割り込みに変換しCPUに送信する。しかしPICがCPUからのACKを受け取るまではPICもデバイスコントローラも処理を停止させた状態となる。他の割り込みの最中にカーネルが他の割り込みを受け付けることで、すぐにACKを返すことができる。
  • 優先度レベルのない割り込みモデルを実現できる。どの割り込みハンドラの処理でも遅延可能で、ハードウェアデバイスに優先度を設定しておく必要がないため、カーネルを単純化し移植性を高めることができる。

マルチコアプロセッサでは複数のプロセスが走るため、プロセスの切り替えによって別のCPUに移動することがある。

割り込みディスクリプタテーブルの初期化

カーネルが割り込みを許可する前に以下の2点を行っておく必要がある。

  • idtrレジスタIDTの先頭アドレスを設定
  • IDT内の全エントリの初期化

ユーザプロセスはint命令にベクタ番号を指定して割り込み信号を発生させることができるので、IDTの初期化時に特定の割り込みやトラップのディスクリプタのDPLを0に設定しておく必要がある。数は少ないがユーザプロセスが直接生成できるものもあるため、対象のディスクリプタのDPLのみ3を設定する。

ゲートの種類とゲート登録用関数

  • 割り込みゲート

Intelの割り込みゲートでユーザプロセスからはアクセスできないものとなっている(DPL=0)。Linuxの割り込みハンドラは全て割り込みゲートで実装されている。

割り込みゲートを登録する関数であるset_intr_gate()の定義を以下に示す。内部で呼ばれている_set_gate()を見ると、第一引数はIDTの何番目のエントリか、第二引数はエントリタイプ、第三引数はDPL、第四引数は関数のアドレス、第五引数はセグメントの指定となっているのがわかる。

// arch/i386/kernel/traps.c
/*
 * This needs to use 'idt_table' rather than 'idt', and
 * thus use the _nonmapped_ version of the IDT, as the
 * Pentium F0 0F bugfix can have resulted in the mapped
 * IDT being write-protected.
 */
void set_intr_gate(unsigned int n, void *addr)
{
    _set_gate(idt_table+n,14,0,addr,__KERNEL_CS);
}
  • システムゲート

Intelの言うトラップゲートでユーザプロセスからもアクセスが可能(DPL=3)。ベクタ番号4, 5, 128はシステムゲートで実装されているためinto、bound、int 0x80の3つの命令はユーザプロセスも使用が可能となる。

システムゲートを登録する関数であるset_system_gate()の定義を以下に示す。

// arch/i386/kernel/traps.c
static void __init set_system_gate(unsigned int n, void *addr)
{
    _set_gate(idt_table+n,15,3,addr,__KERNEL_CS);
}
  • システム割り込みゲート

Intelで言う割り込みゲートでユーザプロセスからもアクセスが可能(DPL=3)。ベクタ番号3のハンドラは当該ゲートで実装されているため、int3命令はユーザプロセスからも利用可能。

// arch/i386/kernel/traps.c
/*
 * This routine sets up an interrupt gate at directory privilege level 3.
 */
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
    _set_gate(idt_table+n, 14, 3, addr, __KERNEL_CS);
}
  • トラップゲート

Intelのいうトラップゲートでユーザプロセスがアクセス可能でないもの(DPL=0)。Linuxではほとんどの例外ハンドラをこのトラップゲートで実装している。

// arch/i386/kernel/traps.c
static void __init set_trap_gate(unsigned int n, void *addr)
{
    _set_gate(idt_table+n,15,0,addr,__KERNEL_CS);
}
  • タスクゲート

Intelの言うタスクゲートでユーザプロセスからはアクセス不可能(DPL=0)。Linuxではダブルフォルト例外ハンドラは当該ゲートで実装されている。

// arch/i386/kernel/traps.c
static void __init set_task_gate(unsigned int n, unsigned int gdt_entry)
{
    _set_gate(idt_table+n,5,0,0,(gdt_entry<<3));
}

上記の関数内部で呼ばれている_set_gate()の定義は以下。

// arch/i386/kernel/traps.c
#define _set_gate(gate_addr,type,dpl,addr,seg) \
do { \
  int __d0, __d1; \
  __asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
   "movw %4,%%dx\n\t" \
   "movl %%eax,%0\n\t" \
   "movl %%edx,%1" \
   :"=m" (*((long *) (gate_addr))), \
    "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
   :"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
    "3" ((char *) (addr)),"2" ((seg) << 16)); \
} while (0)

上記では各引数の値をeax及びedxにセットし、それぞれ32bitずつのデータをゲートディスクリプタのアドレスを起点に+0及び+1の場所に保存している。

参考文献