自作OS、始まります! #7
割り込みをしたい
前回、ついにローダーが完成していよいよOSの自作が始まったわけで、とりあえずまずはキーボードで入力するところからかなと。
幸いにも文字を描画する部分については#5でもう作っているので、今回やるべきことは割り込みを受け付けるためのGDT/IDT初期化、PIC初期化(今時はAPICが望ましいですがとりあえずしばらくはPICを使います。)、キーボード割り込み処理ハンドラの作成あたりでしょうか。
GDT初期化
とはいっても作っているのは64bitのOS、セグメントは使わないので申し訳程度の初期化です。
今回は「フルスクラッチで作る!x86_64自作OS」を参考にほとんど同じ内容を実装しただけなのでそちらを参照してください。
IDT初期化
IDT(Interrupt Descriptor Table)とはある割込みに対してどの関数を呼び出すか、を記述する表のようなものです。
まずは必要な構造体と初期化関数です。
typedef struct { uint16_t offset_low, selector; uint8_t ist, type_attr; uint16_t offset_middle; uint32_t offset_high, zero; } INTERRUPT_DESCRIPTOR; INTERRUPT_DESCRIPTOR descriptors[256]; void init_idt() { for (unsigned int i = 0; i < 256; ++i) { set_interrupt_handler(i, default_handler); } unsigned long long idtr[2]; idtr[0] = (reinterpret_cast<unsigned long long>(descriptors) << 16) | (sizeof(descriptors) - 1); idtr[1] = (reinterpret_cast<unsigned long long>(descriptors) >> 48); load_idt(idtr); } .global load_idt load_idt: lidt [rcx] ret
IDTの実装はINTERRUPT_DESCRIPTOR構造体が256エントリ存在する配列で、それぞれに対して呼び出したいハンドラ関数を設定しなければなりません。
とはいえ、まず使いたいのはキーボードの割り込みであるため、一旦全て同じ関数(default_handler:hltするだけ)を設定し、後から必要なハンドラを個別に設定していくことにします。
また、IDTの設定には通常の命令ではなくlidtという特殊な命令を使う必要があり、これはCやC++では書けないためアセンブリで書く必要があります。(load_idt関数)
次に、ハンドラを設定するためのset_interrupt_handler関数です。
void set_interrupt_handler(unsigned int idt_no, __attribute__((ms_abi)) void (*handler)()) { descriptors[idt_no].offset_low = reinterpret_cast<unsigned long long>(handler); descriptors[idt_no].selector = 0x0008; descriptors[idt_no].type_attr = 14; descriptors[idt_no].type_attr |= 0b10000000; descriptors[idt_no].offset_middle = reinterpret_cast<unsigned long long>(handler) >> 16; descriptors[idt_no].offset_high = reinterpret_cast<unsigned long long>(handler) >> 32; }
idt_noは割込み番号で、例えばキーボードの割り込みなら0x21を使用しています。先ほどの構造体を見ればわかりますが、設定方法が結構複雑です・・・
offset_○○には設定したいハンドラのアドレス64bitを上位32bit、真ん中16bit、下位16bitに分けて入れます。selectorはハンドラの所属しているセグメントの番号を格納します。(ただし下位3bitを0にする必要があるため、実際には番号を3bit左シフトしたものです。今回で言えば1 << 3で8となっています。)
type_attrはtypeとattributeを合わせて持っている変数で、14が割り込みまたは例外であることを示すディスクリプタタイプです。そして8bit目のpフラグと呼ばれる部分がディスクリプタが存在しているかどうか(Present)を表すフラグで、type_attr |= 0b10000000とすることでビットを1にしています。
書き忘れていましたが、引数のvoid (*handler)()は引数無しの関数ポインタです。ここに設定したいハンドラを記述した関数を渡すことで設定します。
PIC初期化
PICはProgrammable Interrupt Controllerの略でCPUに割り込み発生を通知してくれます。 今更PICについて書くことはないそうなので、自分も特にないです。やってることも30日OSと同様に初期化しているだけです。
void init_pic() { io_write_8(PIC0_ICW1_ADDR, 0x11); io_write_8(PIC0_ICW2_ADDR, 0x20); io_write_8(PIC0_ICW3_ADDR, 0x04); io_write_8(PIC0_ICW4_ADDR, 0x01); io_write_8(PIC0_OCW1_ADDR, 0xff); io_write_8(PIC1_ICW1_ADDR, 0x11); io_write_8(PIC1_ICW2_ADDR, 0x28); io_write_8(PIC1_ICW3_ADDR, 0x02); io_write_8(PIC1_ICW4_ADDR, 0x01); io_write_8(PIC1_OCW1_ADDR, 0xff); } void set_pic(unsigned int idt_no) { if (idt_no < 0x28) { unsigned char idt_set_bit = 1U << (idt_no - 0x20); if (!(io_read_8(PIC0_IMR_ADDR) & idt_set_bit)) return; io_write_8(PIC0_OCW1_ADDR, io_read_8(PIC0_IMR_ADDR) - idt_set_bit); } else { unsigned char idt_set_bit = 1 << (idt_no - 0x28); if(!(io_read_8(PIC1_IMR_ADDR) & idt_set_bit)) return; io_write_8(PIC1_OCW1_ADDR, io_read_8(PIC1_IMR_ADDR) - idt_set_bit); } }
set_picは指定したIRQのビットを0にする関数で、一応マスターとスレーブに対応できているはずです。(マウスの割り込みを試すまで正しいかはわからないのですが・・・)
io_write_8はout命令を用いて指定したアドレスに8ビットのデータを書き込む関数でアセンブリによる実装です。ついでにio_read_8は逆にin命令で8ビットのデータを読み込む関数です。
.global io_write_8 io_write_8: mov eax, edx mov edx, ecx out dx, al ret .global io_read_8 io_read_8: mov edx, ecx xor rax, rax in al, dx ret
KBC初期化
KBCはキーボードコントローラーのことで、KBCがPICに割り込みを通知、PICがCPUに割り込みの通知を通知することで割り込みが発生します。このKBCにレジスタ経由でアクセスすることで、キーボードが押されているかどうかや、どのキーが押されているかといった情報を取得することができます。
.global int21_handler int21_handler: push rdi mov rdi, 0x21 call handler_wrapper pop rdi iretq .global handler_wrapper handler_wrapper: push rax push rcx push rdx push rbx push rbp push rsi call do_default_handler pop rsi pop rbp pop rbx pop rdx pop rcx pop rax ret
int21_handlerがキーボードの割り込み時に呼び出したいハンドラです。handler_wrapperを見るとよくわかるのですが、割り込み発生時はレジスタをスタックにpushして値を保存しておく必要があります。int21_handler内に全部羅列してもいいのですが、それだと今後ハンドラが増えていったときに冗長なため、まず対応したハンドラを呼び出した時に割込み番号だけ引数に設定し、handler_wrapperを呼び出す形にしました。これなら今後ハンドラを増やすことになっても番号を変更するだけで済みますからね。また、割り込みから戻る際には通常のret命令ではなくiretq命令を使用する必要があります。handler_wrapperはハンドラとして設定することはないためret命令で終わっていますが、int21_handlerはiretq命令で終了します。
そして、上記のハンドラから呼び出すdo_default_handler関数は、割り込み番号を引数のレジスタ(rdi)にセットしているのでそれを使って対応した関数を呼び出すようにします。
extern "C" { void do_default_handler(unsigned int handler_no) { switch (handler_no) { case 0x21: do_kbc_handler(); break; default: break; } } }
現状割り込みを発生させるのはキーボードのみなので0x21の場合だけ書いておきましょう。extern "C"でC言語として実装していますが、これがないとアセンブリから関数を呼び出すのがはるかに難しくなります。というのもC++の関数の場合マングルを考えたうえで呼び出す関数のシンボル名を記述する必要があるからです。やってる内容は特にC++の機能が必要ではないのでCで書いてしまえばいいでしょう。もし必要ならC++の関数を呼び出すCの関数でもいいと思います。
最後はキーボードのハンドラです。
void do_kbc_handler() { if (!(io_read_8(KBC_STATUS_ADDR) & KBC_STATUS_OBF)) { io_write_8(PIC0_OCW2_ADDR, 0x61); return; } unsigned char keycode = io_read_8(KBC_DATA_ADDR); if (keycode & KBC_DATA_PRESSED) { io_write_8(PIC0_OCW2_ADDR, 0x61); return; } g_chara = keycode_map[keycode]; io_write_8(PIC0_OCW2_ADDR, 0x61); return; }
keycode_mapはキーボードを押した時に来るキーコード(例えば'A'なら0x1eなど)を実際の文字に変換するための配列です。keycode_map[0x1e]→'A'というように値を返してくれます。ただ、このキーコードはそれぞれのキーボードによって微妙に変わる可能性があるので、その場合は自分でキーコード見ながら配列を実装する必要があります・・・(私はしました・・・)
また、今回は簡単に実装したかったのでグローバル変数に格納してしまっていますが、これは非常によくないです。次回、バッファリングに対応させる予定です。それと割り込みを禁止させる部分も追加しないとですね・・・
それぞれの初期化をまとめる
上記4つの初期化をカーネルロード後に呼び出すためにinit_kernel関数に入れておきます。気をつけることとしては初期化の順番くらいでしょうか。
void init_kernel() { init_gdt(); init_idt(); init_pic(); init_kbc(); }
思いのほか実装しなければならない部分が多かったのですが、これで割り込みに対応できたはずです。
いざ、割り込み!
今回はグローバル変数に入力された値を格納しているだけなので、無限ループで割り込みが来たら変数の値を出力するという形で試してみます。
while (1) { io_stihlt(); putc(frame_buffer, g_chara); g_chara = 0; }
io_stihlt()はsti命令を実行した後hlt命令を行うアセンブリ関数です。sti命令は割り込み許可を行う命令で、逆に割り込みを禁止する命令はcli命令です。(今回は使っていませんが、次回使う予定です。)
sti命令で割り込みを許可した後であればhlt命令で停止してしまっても割り込みによってまた動いてくれるのでCPUの節約になります。
後はお祈りしながらmake runするだけです。
まだ大文字と小文字を区別できていないので全て大文字になってしまっていますが、キーボードの割り込み処理には対応できています!
次回以降の目標
次回は割り込み禁止部分の設定、大文字小文字の対応(シフトキーの対応)、バッファリングの対応などでしょうか。それが終わったら・・・はまたそのうち考えます。