OS自作入門6日目のまとめ
自分のためのまとめなので、他の人には意味不かも。
分割コンパイルと割り込み処理
分割
ソースファイルの分割には以下の利点と欠点がある。
利点
- きちんと分類してファイル名をつければ、目的とする箇所を見つけやすくなる。
- Makefileをうまく書いておけばmake時間の短縮になる。
- それぞれのソースが短くなるので、読みやすく扱いやすい。
欠点
- ファイル数が増える。
- 分類に失敗すると管理が大変になる。改造箇所を見つけるのに苦労する。
Makefile
Makefileには複数の処理をひとつの記述にまとめることが出来る。以下の様に。
bootpack.gas : bootpack.c Makefile
$(CC1) -o bootpack.gas bootpack.c
graphic.gas : graphic.c Makefile
$(CC1) -o graphic.gas graphic.c
dsctbl.gas : dsctbl.c Makefile
$(CC1) -o dsctbl.gas dsctbl.c
bootpack.nas : bootpack.gas Makefile
$(GAS2NASK) bootpack.gas bootpack.nas
graphic.nas : graphic.gas Makefile
$(GAS2NASK) graphic.gas graphic.nas
dsctbl.nas : dsctbl.gas Makefile
$(GAS2NASK) dsctbl.gas dsctbl.nas
複数の同じような処理が書かれているとMakefileが長くなる。これを以下のように書き換えることが出来る。
%.gas : %.c Makefile
$(CC1) -o $*.gas $*.c
%.nas : %.gas Makefile
$(GAS2NASK) $*.gas $*.nas
非常に短くなる。
ヘッダファイル(header file)
ファイルの分割によって、それまではひとつのファイルに書かれていた宣言や定義を複数のファイルに
書かなくてはならなくった。
それらをひとつのファイルにまとめて、そのファイルを読み込むことによってそれまで書かれていたものを省くことが出来る。
このヘッダファイルを読み込むということは、ソースファイル直接書き込むのと同じ効果になる。
/*bootpack.h*/
/* asmhead.nas */
struct BOOTINFO { /* 0x0ff0-0x0fff */
char cyls; /* ブートセクタはどこまでディスクを読んだのか */
char leds; /* ブート時のキーボードのLEDの状態 */
char vmode; /* ビデオモード 何ビットカラーか */
char reserve;
short scrnx, scrny; /* 画面解像度 */
char *vram;
};
#define ADR_BOOTINFO 0x00000ff0
/* naskfunc.nas */
void io_hlt(void);
void io_cli(void);
void io_out8(int port, int data);
int io_load_eflags(void);
void io_store_eflags(int eflags);
void load_gdtr(int limit, int addr);
void load_idtr(int limit, int addr);
/* graphic.c */
void init_palette(void);
void set_palette(int start, int end, unsigned char *rgb);
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1);
...
このように。そしてこのファイルを
#include "bootpack.h"
として読み込む。普通は
#include <stdio.h>
のようにして読み込むのだが、""で囲むと、読み込むファイルが同じ階層にあることになる。
LGDT
5日目で理解できなかったLGDTあたりの説明がここで行われていた。
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LGDT [ESP+6]
RET
この関数は、指定されたリミットと番地を、GDTRという48ビットのレジスタに代入するためのもの。
GDTRは48ビットの特殊なレジスタなのでMOV命令では代入できない。
そのため代入にはメモリの番地を指定するしかなく、番地を指定するとそこから48ビット(6byte)分読み込んでGDTRレジスタに
代入してくれる。
そのための命令がLGDT。
このレジスタの下位16ビットはリミットをあらわしていて、
これはGDTの有効バイト数-1のこと。のこりの上位32ビットがGDTのおいてある場所を示している。
32ビットは4GB表現できるので、現在のメモリならこれでok。
具体的な説明は以下。
[ESP+4]にリミット値が、[ESP+8]に番地が入っている。
それぞれ0x0000ffffと0x00270000で、これを1バイトづつ区切って表すと
FF FF 00 00 00 00 27 00
となる。下位のビットが番地の小さいバイトに入るので、
この図のように値をずらしてから48ビット分読み込めばちょうどうまくいく。
IDTRもほぼ同様の処理を行っている。
segment discriptor
/* dsctbl.c */
struct SEGMENT_DESCRIPTOR {
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
}
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
if (limit > 0xfffff) {
ar |= 0x8000; /* G_bit = 1 */
limit /= 0x1000;
}
sd->limit_low = limit & 0xffff;
sd->base_low = base & 0xffff;
sd->base_mid = (base >> 16) & 0xff;
sd->access_right = ar & 0xff;
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
sd->base_high = (base >> 24) & 0xff;
return;
}
セグメントには
- 大きさ
- 開始番地
- 管理用属性(書き込み禁止、実行禁止、システム専用など)
これらの情報が必要となる。その情報をまとめて書き込むのがset_segmdesc関数である。
セグメントディスクリプタにはそれぞれの情報がhigh,low,middleなどに分割されているのだが、
シフトなどを用いてうまいこと分割して格納している。
セグメントの番地
番地は32ビットで表現される。この番地はセグメントのベース番地と呼ばれることもある。
baseはhigh,mid,lowに分かれているが、32ビットである。(short,char,charで4バイト)
こうすることで286との互換性を保つことができるらしい。
リミット
セグメントが何バイトかをあらわす。short,charで3バイト。上位4ビットは属性の続きを書き込むので、
実際に使えるのは20ビット。
2^20=2^10*2^10=1024*1024=1k*1k=1M
20ビットだと2の20乗で1MBまでしか表現できないことになるが、セグメントの属性にあるGビットというフラグを
たてることでリミットをバイト単位ではなくページ単位で解釈できるようになる。
ページは4KBなので、1Mページあれば
4KB*1M=4GB
となって、全メモリを扱える。GビットのGはgranularityのこと。
セグメントの属性
12ビット。アクセス権とも呼ばれる。
sd->access_right = ar & 0xff;
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
ちょっとよくわからんけど、まず下位8ビットだけを取り出して代入。そのあとにlimit_highの上位4ビットに代入。
してるんだと思う。|はOR演算子だから以下の様になる。
実行モード
実行中のプログラムがおいてあるセグメントのアクセス権によって実行モードが別れる。
- 0x9a:システムモード(特権モード)
- 0xfa:アプリケーションモード(ユーザモード)
リングプロテクション
x86アーキテクチャにはリングプロテクションと呼ばれる保護構造が存在する。
リングは0~3まで存在し、4つの権限を表現できる。
下位のリングほど特権が強く、OSカーネルはリング0で動作することが多い。アプリケーションなどはリング3で動作させることが多い。
以下の様な感じ。この図はリングプロテクションの説明によく用いられる。内側に行くほど特権が強くなる。
PIC(Pogramable Interupt Controler)
割り込みを使うためにはPICの初期化を行わないといけないらしい。
PICは割り込みコントローラというもので、自分で設定可能らしいです。
CPUは割り込みをひとつづつしか処理できないので、PICに機器からの割り込みを全てまとめてPICがCPUに割り込みを出力する。
CPUとPICの関係は以下の様になっているらしい。
通常PICはひとつの装置につきひとつを割り当てるので、8個で足りなくならないようにIRQ2にもう一個PICを取り付けることで数を増やしている。
マスタとスレーブという。スレーブのPICはかならずIRQ2につながることになっている。
PICの初期化は以下の様に行う。
//bootpack.h
#define PIC0_ICW1 0x0020
#define PIC0_OCW2 0x0020
#define PIC0_IMR 0x0021
#define PIC0_ICW2 0x0021
#define PIC0_ICW3 0x0021
#define PIC0_ICW4 0x0021
#define PIC1_ICW1 0x00a0
#define PIC1_OCW2 0x00a0
#define PIC1_IMR 0x00a1
#define PIC1_ICW2 0x00a1
#define PIC1_ICW3 0x00a1
#define PIC1_ICW4 0x00a1
//int.c
void init_pic(void)
/* PICの初期化 */
{
io_out8(PIC0_IMR, 0xff ); /* 全ての割り込みを受け付けない */
io_out8(PIC1_IMR, 0xff ); /* 全ての割り込みを受け付けない */
io_out8(PIC0_ICW1, 0x11 ); /* エッジトリガモード */
io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7は、INT20-27で受ける */
io_out8(PIC0_ICW3, 1 << 2); /* PIC1はIRQ2にて接続 */
io_out8(PIC0_ICW4, 0x01 ); /* ノンバッファモード */
io_out8(PIC1_ICW1, 0x11 ); /* エッジトリガモード */
io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15は、INT28-2fで受ける */
io_out8(PIC1_ICW3, 2 ); /* PIC1はIRQ2にて接続 */
io_out8(PIC1_ICW4, 0x01 ); /* ノンバッファモード */
io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外は全て禁止 */
io_out8(PIC1_IMR, 0xff ); /* 11111111 全ての割り込みを受け付けない */
return;
}
PICへの命令はCPUから見れば外部へのデータ出力なので、OUT命令で行う。
書き込みを行うポート番号はヘッダファイルに書いてある。
ヘッダファイルを見るとポート番号は同じになっているが、書き込む順番とかをPIC側で処理してくれるので問題ない。
PICのレジスタについて
- IMR
- interrupt mask register.割り込みマスクレジスタ。8ビットになっていて、それぞれのビットがひとつづつIRQに対応している。このビットが1になっているIRQは全て無視される。割り込みに関する設定を行っているときや、何も機器がつながっていないときはこのビットを立てておく。
- ICW
- Initial Control Word.初期化制御データ。1~4が存在して、合計で4バイト。それぞれいろんな意味がある。
- ICW1,ICW4
- ここでは詳しく説明されていない。
- ICW3
- マスタについては、何番にスレーブがつながっているかどうかを規定する。ここでは2番につながることが確定しているので、00000100を入れる以外に選択肢はない。
- ICW2
- IRQをどの割り込み番号としてCPUに通知するかを決めるためのもの。今回の設定ではIRQ0-15をINT 0x20-0x2fで受け取ることにしている。INT 0x00-0x1fは割り込みに使ってはいけない。INT 0x00-0x1fはアプリケーションがOSに対して悪さをしようとしたときに、CPUが内部で自動的に発生させて、それを通じてOSに通知するためののものだからである。これに重なるようにしてはいけない。
割り込みハンドラ作成
マウスはIRQ12、キーボードはIRQ1なので、それぞれに対応する番号の割り込みハンドラINT 0x2c,INT 0x21を作る。
割り込みハンドラとは、割り込みが発生したときに呼び出される割り込みを扱う(handle)プログラムのこと。
プログラム自体はC言語で書いてもいいが、割り込み処理終了後にはC言語では記述できないIRETD命令を実行しなければいけないので、
アセンブラで書かなければいけない。以下の様に。
EXTERN _inthandler21, _inthandler2c
_asm_inthandler21:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler21
POP EAX
POPAD
POP DS
POP ES
IRETD
まず外部にプルグラムがあることを宣言してある。
宣言しておけばCALL命令で呼ぶことが出来る。やっていることは、レジスタの値をスタックに積んで、関数から帰ったときに復帰するための準備である。
割り込みハンドラ登録
ここでは以下の様にしている。
//dsctbl.c
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) ADR_IDT;
void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
gd->offset_low = offset & 0xffff;
gd->selector = selector;
gd->dw_count = (ar >> 8) & 0xff;
gd->access_right = ar & 0xff;
gd->offset_high = (offset >> 16) & 0xffff;
return;
}
/* IDTの設定 */
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
バッファ
FIFOとかFILOとかある。
FIFO
First In First Outの略。先入れ先出しのこと。レストランなどのキッチンでも使われてるのを聞いたことある。最初に入れたものから出すことを表す。
FILO
First In Last Outの略。先入れ後出しのこと。キッチンではやってはいけない。最初に入れたものは最後に取り出すことを表す。スタックはこのタイプが用いられている。
最終更新:2007年07月25日 21:26