キーボードで割り込んでみる

前回は,ハードウェア割り込みについて学んだ。
今回は,キーボードを例に実際に割り込みを実現するためのプログラムを書いてみよう。

具体的には,キーボードが使用されると,IRQ1 割り込み信号が PIC へ届きベクタに変換され CPU へ伝達される。そして,割り込みディスクリプタテーブルに登録されている関数が実行される。

これを実現させるには次のような手順を踏む:

  • PIC 設定
  • IDT(インターラプトディスクリプタテーブル)作成
  • Interrupt Handler(割り込みハンドラ)登録
  • IDT を CPU に登録

今回は,コードを先に乗せる。

interrupt.h
#ifndef INTERRUPT_H
#define INTERRUPT_H

// --- IDT ------------------------------------------
#define IDT_ENTRIES     256

typedef struct idt_entry {
  word offset_l;
  word selector; 
  byte reserved;
  byte attribute;
  word offset_h;
} __attribute__((__packed__)) idt_entry_t;

typedef struct idt_ptr {
  word limit;
  dword base;
} __attribute__((__packed__)) idt_ptr_t;

// --- prottype --------------------------------------
void init_interrupts();

#endif
interrupt.c
#include "types.h"
#include "i386.h"
#include "mem.h"
#include "interrupt.h"

idt_entry_t	idt[IDT_ENTRIES];
idt_ptr_t	idt_ptr;

extern void INTERRUPT_KEYBOARD, INTERRUPT_NOTHING;

void init_interrupts() {
  int i;
  
  disable();  
  
  // reprogram PCI
  // Initialize Commond Word
  // ICW1 : initialization for master & slave
  outb(0x20, 0x11);
  outb(0xA0, 0x11);
  // ICW2 : specify interrupt numbers
  outb(0x21, 0x20);      // PIC1 = 0x20-0x27
  outb(0xA1, 0x28);      // PIC2 = 0x28-0x2F
  // ICW3 : set pin wired to master/slaves
  outb(0x21, 0x4);
  outb(0xA1, 0x2);
  // ICW4 : set additional option
  outb(0x21, 0x1);
  outb(0xA1, 0x1);
  // Mask interrupts
  outb(0x21, 0xFD);
  outb(0xA1, 0xFF);

  // set nothing
  for(i = 0; i < IDT_ENTRIES; i++) {
    idt[i].offset_l  = (dword)&INTERRUPT_NOTHING;
    idt[i].offset_h  = (dword)&INTERRUPT_NOTHING >> 16;
    idt[i].selector  = KERNEL_CODE;
    idt[i].reserved  = 0;
    idt[i].attribute = INT_GATE;
  }

  // set keybord
  idt[0x21].offset_l  = (dword)&INTERRUPT_KEYBOARD;
  idt[0x21].offset_h  = (dword)&INTERRUPT_KEYBOARD >> 16;
  idt[0x21].selector  = KERNEL_CODE;
  idt[0x21].reserved  = 0;
  idt[0x21].attribute = INT_GATE;

  // Specify the limit (size) and the offset of the IDT
  idt_ptr.limit = sizeof(idt_entry_t) * IDT_ENTRIES - 1;
  idt_ptr.base  = (dword)&idt[0];

  // Load the IDT descriptor and enable interrupts
  __asm__ __volatile__("lidt idt_ptr");
  enable();
}

PIC や CPU の仕様を知らないとさっぱり意味の分からないコードだが,前述の少い手順をプログラミングしただけなので,コード自体はそれほど難しくはない。
それでは,順を追って見ていこう。

PIC 設定

コメント「// reprogram PCI」以下で PCI の設定を行なっている。
PCI の設定はさらに四つの段階に分れている:

  • ICW1 PIC 初期化
  • ICW2 初期番号設定
  • ICW3 マスター/スレーブ接続 IRQ ピン番号設定
  • ICW4 追加オプション設定

I/Oポートの説明で述べた通り,マザーボードに繋がれたハードウェアは固有のアドレスを持っている。PIC1(master)は 0x20 と 0x21 という二つのアドレスを,PIC2(slave)は 0xa0と 0xa1 という二つのアドレスを持つ。
PICは二つあるので,同じような記述を二回続けることになる。

  • ICW1 はPICの設定を始める事をPICに伝える。
  • ICW2 はIRQからベクタへの変換の設定で,各PICの先頭IRQをどのベクタに変換するかを設定する。それ以降のIRQは先頭から連続したベクタに変換される。つまり,プログラマは自由にIRQをベクタに変換できる。ただし,0x20より小さいベクタは,例外が固定的に決っているか Intel が予約済みなので,ハードウェア割り込みで使えるのは 0x20 以降になる。具体的には,今回の設定では,IRQ0 はベクタ 0x20,IRQ1 は 0x21 に変換される。
  • ICW3 はPIC同士が接続されているピンの番号を設定する。PIC2(slave)には,素直にPICが接続されているピンの番号である 0x2 を登録する。注意しなければならないのは,PIC1(master)には繋がれているピンの位置を 0x4 として登録している事である。これは面白いことに,IRQ0ピンから数えてIRQ2ピンが何番目にあるかをビット位置で表し登録する必要があるためである。0x4 は次のような二進数で表現できて 00000100 これは左から三番目のビットが 1 になっている。
  • ICW4 はいくつかのオプションを追加する。ここでは 8086 モードのオプションを加えた。それ以外のオプションについては割愛する。私たちには他にやらねばならない事が沢山あるのだから。

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

x86IDT エントリは次のような構造になっている。今回は,idt_entryという構造体で作成した。

struct idt_entry bit 用途
offset_l 0-15 割り込み時,実効したい関数のアドレス(割り込みハンドラ)の下位 16bit
selector 16-31 セグメントセレクタ
reserved 32-39 Intel に予約されている。常に 0 を入れておけばいい…と思う
attribute 40-47 割り込みにはいくつかの種類があり,ここに種類を識別するための値を入れる
offset_h 48-63 割り込み時実効したい関数のアドレス(割り込みハンドラ)の上位16bit

idt_entry_t idt[IDT_ENTRIES]で 256 個のエントリを持つIDTを作成し,次のように初期化している。

   for(i = 0; i < IDT_ENTRIES; i++) {
    idt[i].offset_l  = (dword)&INTERRUPT_NOTHING;
    idt[i].offset_h  = (dword)&INTERRUPT_NOTHING >> 16;
    idt[i].selector  = KERNEL_CODE;
    idt[i].reserved  = 0;
    idt[i].attribute = INT_GATE;
  }

forの中身は,IDTエントリに情報を登録する基本的な操作で,IDTエントリを十分な情報で埋める。
ここで,関数INTERRUPT_NOTHING(コードは後述)は何もしない関数,KERNEL_CODEはカーネルのセグメントセレクタ,INI_GATEはハードウェア割り込みを表す値である。つまり,どんな割り込みがあっても何もしない関数が実行されることになる。

Interrupt Handler(割り込みハンドラ)登録

割り込み時に実行される関数は割り込みハンドラと呼ばれる。
割り込みが発生すれば,あるプログラムが実行中であったとしても,その間に割り込んで割り込みハンドラが実行される。つまり,OS は割り込み処理が終れば何もなかったかのように,元の実行中だったプログラムへ制御を返さなければならない。そのため CPU のレジスタは割り込み処理実行前に退避させ,割り込み処理実行後にもとに戻してやる必要がある。
つまり,これが割り込みハンドラの最小のコードであり,IDTの初期化で登録した何もしない割り込みハンドラである。

.global INTERRUPT_NOTHING
INTERRUPT_NOTHING:
	/* save registers */
	pushad	
	push	ds
	push	es
	push	fs
	push	gs

	/* restore registers */
	pop	gs
	pop	fs
	pop	es
	pop	ds
	popad
	iret

割り込みハンドラは特殊な関数でリターン命令がretではなくiretになっている。そのためGCCではC言語を用いて割り込みハンドラを作ることが出来無い。(一般的に,アセンブラで書いた割り込みハンドラ内からC言語の関数をコールする)
例えば,次の用な割り込みハンドラは間違いである:

void interrupt_handler() {
 hoge();
 asm("iret");
}

さて,キーボードの割り込みハンドラを書いてみるとしよう。上記のコードをベースに画面に文字「A」を表示する割り込みハンドラを作成した。

.global INTERRUPT_KEYBOARD
INTERRUPT_KEYBOARD:
	/* save registers */
	pushad	
	push	ds
	push	es
	push	fs
	push	gs

	mov	al, 'A'
	mov	[0xb8000], al
	
	/* EOI for PIC1 */
	mov	al, 0x20
	out	0x20, al
	/* restore registers */
	pop	gs
	pop	fs
	pop	es
	pop	ds
	popad
	iret

ここで,「/* EOI for PIC1 */」以下の命令はPICへ割り込み処理が終了したことを伝える命令である。これをしないとPICはいつまでたっても二回目以降の割り込みを受けつけなくなる。
キーボードは PIC1(master)へ繋がっているので一つの PIC へ命令を出すだけでいいが,PIC2(slave)へ繋がれているハードウェアが割り込み処理を終了する場合,両方の PIC へ命令を出す必要があることを覚えておこう。

さて,「// set keybord 」以下でこの割り込みハンドラを IDT へ登録している。
キーボードの割り込み信号はIRQ1で,先ほど設定した PIC によりベクタ 0x21 に変換される。よって IDT[0x21] のエントリに登録している。

ちなみに,これらの割り込みハンドラは前回までに作成したhead.Sへ書きたした。

IDT を CPU に登録

CPU はベクタを受けとれば IDT を参照することになる。そのためには CPU は IDT の先頭アドレスとサイズを知っていなくてはならない。
それを CPU に教えるためその情報を含む IDTR (Interrupt Descriptor Tbale Register) という構造体を作成し,CPU にこのアドレスを登録する。

struct idt_ptr bit 用途
limit 0-15 IDT のサイズ(byte-1)
base 16-47 IDT の先頭アドレス
lidt 命令

lidt という命令を使ってこの IDTR を登録する。最後に enable() で割り込みを有効にする。

  __asm__ __volatile__("lidt idt_ptr");
  enable();

今回新しく作成した関数など

新規 types.h
#ifndef TYPES_H
#define TYPES_H
typedef unsigned char  byte;
typedef unsigned short word;
typedef unsigned long  dword;
#endif
i386.h 追記
static __inline__ void enable() {
  __asm__ __volatile__ ("sti");
}

static __inline__ void disable() {
  __asm__ __volatile__ ("cli");
}
mem.h 追記
/* Segment Selectors  */
#define KERNEL_CODE	0x08
#define KERNEL_DATA	0x10

/* Descriptor Types */
#define INT_GATE	0x8E

おしまい

うまくいけば,キーボードを操作すると,真っ黒の画面に文字「A」が表示される。