OS自作入門 六日目

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
ツールボックス

下から選んでください:

新しいページを作成する
ヘルプ / FAQ もご覧ください。