0から作るソフトウェア開発

日々勉強中。。。

0から作るOS開発

環境準備

環境設定

ブートローダ

カーネルローダ

GRUB

カーネル

ドライバーその他

0から作るOS開発 割り込みその1 割り込みとIDTとGDT

前回までの内容

これまでで、 ことがわかりました。それではカーネルを0から開発していきましょう!

割り込み

割り込みはハードウェアから特定の処理を要求されたときに、一旦ソフトウェアの処理を一時中断させて、

要求された処理を行うことをいいます。例えば、キーボードコントローラはキーボードのキーが押された時に

CPUに割り込み信号を出します。割り込みを受けたCPUはキーボードの割り込み処理を行い

どのキーが押されたのかをキーボードコントローラから読み出す処理を行うなどをします

このように割り込み処理を行うことでCPUは常にキーボードを監視する必要がなくなり、

その他の処理をしつつ、割り込み信号が来た時だけ割り込み処理を行うことで、効率的な処理ができます

割り込みには大きくわけて3つの種類があります

があります。

例外(Exception)

このサイトでは、CPU内部で発生する割り込み信号のことを例外として扱います

例外はCPUが異常を検出したときに発生します。例えば、数値計算で0で割った時や、

ベージングで割り当てられたページが無いときなどに異常を検出して例外処理を行います

主にソフトウェアが何か処理をした時に異常を検知します

割り込み要求(IRQ:InterRupt Request)

割り込み要求(IRQ)はCPUの周辺にあるハードウェアから要求があったときに発生する信号です

CPUのピンとハードウェアは電気的にワイヤーで接続されていて、ハードウェアは割り込み要求(IRQ)を

出すときにワイヤーに電気信号を出すことで、CPUに要求を伝えます。ワイヤーの信号が変化した

ことをCPUが検知して、要求にあった割り込みハンドラを呼び出します。この信号はソフトウェアの動作に

関係なく、いつ信号が送られてくるのかはわかりません。

CPUはPIC(Programmable Interrupt Controller)とワイヤーでIRQラインが結ばれています

PICはフロッピーディスクコントローラやDMAコントローラなどの割り込み要求を一括してCPUに伝えます

CPUに接続されたIRQラインとPIC

ソフトウェア割り込み

例外と違い、ソフトウェアが意図的に発生させることができる割り込みです

ブートローダその9ブートローダその10で でてきたINT命令がまさにソフトウェア割り込みを発生させる命令です

ソフトウェア割り込みは主にユーザプログラムがシステムコールを呼出すときに使用します

割り込みベクタと割り込みハンドラ

割り込みが発生したときにどのハンドラを呼び出せばいいのかを記述したものを割り込みベクタと言います

割り込みベクタには割り込みハンドラのアドレスが順番に並んでいるようなテーブルのイメージで、

番号がつけられています。割り込みが発生したときに、その割り込みに対応した番号のハンドラが

呼び出されることで割り込み処理が行われます

割り込みベクタと例外

割り込みベクタのうち例外の部分はCPU仕様によって決まっています

例外をピックアップしたリストが下記となります

例外の割り込みベクタ
ベクタ番号 ニーモニック 説明 原因
0 #DE 除算エラー
Divided by 0
0で割り算を行った
DIV命令、IDIV命令
1 #DB シングルステップ(デバッグ)
Single Step(Debug)
デバッガによるコード、データの参照
2 - マスク不可割り込み
NMI(Non-Maskable Interrupt)
マスク不可能な外部割り込み
3 #BP ブレークポイント(デバッグ)
Break Point(Debug)
INT 3命令
4 #OF オーバフロー
Overflow
INTO命令
5 #BR バウンド範囲外
Bounds Check
BOUND命令
6 #UD 無効なオペコード(未定義なオペコード)
Undifined Operation Code Instruction
UD2命令または無効なオペコード
7 #NM デバイス使用不可能(数値演算コプロセッサ無し)
No Coprocessor
浮動小数点命令、WAIT/FWAIT命令
8 #DF ダブルフォルト
Double Fault
例外、NMI、INTRを生成できる命令
9 #MF コプセッサセグメントオーバーラン
Coprocessor Segment Overrun
浮動小数点命令
Intel386プロセッサ以降のプロセッサでは生成されません
10 #TS 無効なTSS(Task State Segments)
Invalid TSS
タスクスイッチ、TSSアクセス
11 #NP セグメントが不在
Segment Not Present
セグメントレジスタのロード、システムセグメントへのアクセス
12 #SS スタックセグメントフォルト
Stack Segment Fault
スタック操作、SSレジスタのロード
13 #GP 一般保護例外
General Protection Fault(GPF)
メモリ参照、保護チェック
14 #PF ページフォルト
Page Fault
メモリ参照
15 - 予約 -
16 #MF 浮動小数点エラー(数値演算フォルト)
Coprocessor Error
浮動小数点命令、WAIT/FWAIT命令
17 #AC アライメントチェック
Alignment Check
メモリ参照
Intel486プロセッサ以降のプロセッサで発生します
18 #MC マシンチェック
Machine Check
エラーコード、ソースがモデルに依存
インテルPentinumプロセッサ以降
19 #XF SIMD浮動小数点例外
SIMD FPU Exception
SIMD浮動小数点命令
20-31 - 予約 -
32-255 - マスク可能割り込み
IRQやソフトウェア割り込みの割り込みベクタとして使用します
INTRピンによる外部割り込みまたはINT命令


IDT(Interrupt Descriptor Table)と呼ばれるテーブルで割り込みベクタを作っていきます

IDT(Interrupt Descriptor Table)

IDTは割り込みと割り込みハンドラを結びつけるテーブルで、8バイトのディスクリプタです

IDTは256個までの配列でそれぞれ順番に0番から255番までの割り込みに対応しています

GDTとよく似たディスクリプタの構成をしていて、 3種類のディスクリプタがあります

タスクゲートディスクリプタを使用して割り込みハンドラ処理を行うとタスクスイッチができます

(このサイトではタスクゲートディスクリプタは使用しないようにしています)

割り込み/トラップディスクリプタは割り込み/例外時に使用するディスクリプタとなります

割り込みゲートディスクリプタとトラップゲートディスクリプタはほとんど同じ動作をしますが、

1箇所だけ違う動作を行います。割り込み/例外発生時に、割り込みゲートディスクリプタでは

EFLAGSレジスタのIF(割り込みフラグ)をクリアし、割り込み処理中は他の割り込みが入らいないように

します。一方で、トラップデートディスクリプタはEFLAGSのIFフラグを変更しないので、

その他の割り込み/例外を許可します。この3種類のディスクリプタについて見てみましょう

IDTのディスクリプタ

ディスクリプタの構造です。上から順に、タスクゲートディスクリプタ、割り込みゲートディスクリプタ、

トラップゲートディスクリプタです

IDTのディスクリプタ。タスクゲートディスクリプタ、割り込みゲートディスクリプタ、トラップゲートディスクリプタの構造

IDTのディスクリプタ
ビット ビット名称 説明
0-15 Offset タスクゲート
使用しません
割り込み/トラップゲート
割り込みハンドラのアドレスの下位16ビットを入れます
16-31 (TSS)Segment Selector タスクゲート
TSSセグメントセレクタ
割り込み/トラップゲート
セグメントセレクタ
(通常割り込みはring0で処理をするので、0x08のコードディスクリプタを設定します)
31-35 Unused 使用しません
36-39 0/Unused タスクゲート
使用しません
割り込み/トラップゲート
0固定です
40-42 0/1 タスクゲート
101b
割り込みゲート
110b
トラップゲート
111b
43 D ゲートのサイズを決定します
0:16ビット
1:32ビット
32ビットプロテクティッドモードですので、通常1に設定します
44 0 0固定です
45-46 DPL
(Descriptor Plivilege Level)
ディスクリプタの特権レベルを設定します(アクセス可能なレベルを設定します)
セグメントセレクタで指定するRPLのレベルは現在実行中のレベルを意味します
このディスクリプタにアクセスする場合はセグメントセレクタのRPLとこのDPLビットが比較されます
47 セグメントプレゼントフラグ
(Segment Present Flag)
セグメントが存在するかどうかを設定します
0:セグメントは存在しない
1:セグメントは存在する
空のIDTのディスクリプタを作る場合はここに0を入れます
48-63 Offset タスクゲート
使用しません
割り込み/トラップゲート
割り込みハンドラのアドレスの上位16ビットを入れます


ここでは割り込みを使いますので、割り込みゲートディスクリプタを使用します

割り込みはカーネルの特権レベルRing0で動作しますので、DPLは0x0を入れます

IDTR(Interrupt Descriptor Table Register)

IDTRはCPUが持っている特殊なレジスタの1つです。 GDTのときにGDTを作成して、

LGDT命令でGDTのサイズとアドレスを読み込みました。LGDT命令が実行をされると

GDTのサイズとアドレスがGDTRというCPUの特殊なレジスタに読み込まれます

IDTの場合も同様にLIDT命令を使ってIDTRにIDTのサイズとアドレスを格納します

IDTRにIDTのサイズとアドレスが格納されれば、割り込みが発生した時にCPUが

IDTを参照し、対応する割り込みハンドラの処理を行ってくれます

IDTをC言語で構築する

IDTをC言語で作っていきます

まずはIDTのゲートディスクリプタを定義します

ゲートディスクリプタの定義


/*
==================================================================================

	Description : Gate Descriptor of IDT

==================================================================================
*/
typedef struct
{
	unsigned short	baseLo;
	unsigned short	selector;
	unsigned char	reserved;
	unsigned char	flags;
	unsigned short	baseHi;
} __attribute__ ((packed)) GATE_DESCRIPTOR;



インテルのCPUはリトルエンディアンですので、構造体の最初に宣言した変数が

一番アドレスが小さくなります。baseLoはゲートディスクリプタの図のOffset0-15ビットにあたります

selectorはSegment Selector16-31ビットで、reservedは32-39の未使用の領域と

割り込み/トラップゲートの0固定に割り当てています。flagsは40-47ビットをごちゃっといっしょくたに

しています。baseHiはOffset48-63ビットにあたります

最後に”__attribute__ ((packed))”が入っています。これはshort型とshort型の間にchar型が

入ってしまっているので、構造体の変数が変に配置されないようにするための、GCCのアトリビュートキーワードです

例えば、attribute機能で構造体をpackしない場合は(この例ではGDTのセグメントディスクリプタで例を

作っています。下の方にスクロールすればGDTの構造を再度載せていいますので、ご参考にしてください。

IDTではよい例が思いつきませんでした。。。)

GDTのセグメントディスクリプタをattribute機能でpackしなかった場合の構造体のアライメント

上記の図の用にCPUは特殊なGDTRという64ビット(8バイト)のレジスタに連続した8バイトのデータを

格納しますが、コンパイラはデータの境界を考えてchar型の後に空白(0x00など)を挿入(パディング)

してしまいます。これでは、図の赤枠の部分のデータをCPUが読み込んでしまいますので、プログラムで

意図した通りに動いてはくれません。そこでGCCのattribute機能でpackedします

attribute機能でpakcedするとデータを上手いこと詰めてくれます(CPUによっては無理やりpackedすると

非効率になってしまう場合があります)

GDTのセグメントディスクリプタをattribute機能でpackした場合の構造体のアライメント

attribute機能でpackedすることで、連続したデータになりますので、プログラムはCPUが期待する

データを用意することができます(このような場合、本当はpackedするのではなくてC言語的には

unsinged long longで定義するほうが正しいです)

ゲートディスクリプタからIDTを作る

IDTのゲートディスクリプタは合計256個まで作成することができます


#define	NUM_IDT	256

/*
==================================================================================

	Description : IDT

==================================================================================
*/
GATE_DESCRIPTOR	idt[ NUM_IDT ];



256個作成した例ですが、メモリがもったいないというときは数を減らせます

IDTRの定義

IDTRは46ビットのレジスタで、その構造は下記図となります

IDTRの構造

LIDT命令を行うことで、0-15ビットのIDT LimitにはIDTのサイズが、

16-46ビットのIDT Base AddressにはIDTのアドレスが格納されます


/*
==================================================================================

	Description : IDTR

==================================================================================
*/
typedef struct
{
	unsigned short		size;
	GATE_DESCRIPTOR*	base;
} __attribute__ ((packed)) IDTR;

IDTR	idtr;



32ビットコードを作成するコンパイラではポインタが32ビットのサイズとなります

ここで定義したidtrにIDTのサイズとアドレスを入れていきます

ゲートディスクリプタをセットアップする

ゲートディスクリプタを割り込みゲートディスクリプタとしてセットアップしていきます

セットアップするときにいくつかのフラグをセットする必要がありますので、予め#defineしておきます

ゲートディスクリプタのフラグ定義


/*
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/

	Description : Definition for IDT Gate Descriptor Flags
	
	flags
	bit number	description
	0...4		interruptgate		: 01110b = 32bit descriptor
						: 00110b = 16bit descriptor
			task gate 		: must be 00101b
			trap gate		: 01111b = 32bit descriptor
	5...6		descriptor privedlge	: ring0 = 00b
			level			: ring1 = 01b
						: ring2 = 10b
						: ring3 = 11b
	7		present bit		: Segment is present

_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
*/
#define	DEF_IDT_FLAGS_INTGATE_16BIT		0x06
#define	DEF_IDT_FLAGS_TSKGATE			0x05
#define	DEF_IDT_FLAGS_CALL_GATE			0x0C
#define	DEF_IDT_FLAGS_INTGATE_32BIT		0x0E
#define	DEF_IDT_FLAGS_TRPGATE			0x0F
#define	DEF_IDT_FLAGS_DPL_LV0			0x00
#define	DEF_IDT_FLAGS_DPL_LV1			0x20
#define	DEF_IDT_FLAGS_DPL_LV2			0x40
#define	DEF_IDT_FLAGS_DPL_LV3			0x60
#define	DEF_IDT_FLAGS_PRESENT			0x80

#define	DEF_IDT_INT_SELECTOR			0x08



割り込みゲートディスクリプタのセットアップ

割り込みゲートディスクリプタをセットアップする関数例です


/*
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	Funtion     :setupInterruptGate
	Input       :int int_num 
		  < interrupt number >
		  void (interrupt_handller) ( )
		  < address of interrupt handller >
	Output      :void
	Return      :void

	Description :set up interrupt gate
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
*/
void setupInterruptGate( int int_num, void *interrupt_handler )
{
	setupGateDescriptor( int_num,
		( int )interrupt_handler,
		DEF_IDT_INT_SELECTOR,
		DEF_IDT_FLAGS_PRESENT | DEF_IDT_FLAGS_INTGATE_32BIT );
}



flagsにDPLの特権レベルを設定する必要ありますが、ここではリング0ですので、

明示的には書いておりません。(書いておいた方がよいかと思います)

次に、ゲートディスクリプタをセットアップする関数を作ります


/*
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	Funtion     :setupGateDescriptor
	Input       :int int_num 
		  < interrupt number >
		  int base
		  < base address ( for interrupt and trap gate )>
		  unsigned short selector
		  < segment selector >
		  unsigned char flags 
		  < flags of IDT >
	Output      :void
	Return      :void

	Description :set up gate descriptor
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
*/
void
setupGateDescriptor( int int_num,
	int base,
	unsigned short selector,
	unsigned char flags )
{
	idt[ int_num ].baseLo	= ( unsigned short )(   base & 0x0000FFFF );
	idt[ int_num ].selector	= selector;
	idt[ int_num ].reserved	= 0x00;
	idt[ int_num ].flags	= flags;
	idt[ int_num ].baseHi	= ( unsigned short )( ( base & 0xFFFF0000 ) >> 16 );
}



関数setupInterruptGateから呼び出されてIDTのゲートディスクリプタを設定します

ここで


	idt[ int_num ].baseLo	= ( unsigned short )(   base & 0x0000FFFF );
	idt[ int_num ].baseHi	= ( unsigned short )( ( base & 0xFFFF0000 ) >> 16 );



この2つの処理は32ビット割り込みハンドラのアドレスを上位16ビット、下位16ビットに分けて

それぞれ、baseLo、baseHiに入れています


	idt[ int_num ].selector	= selector;
	idt[ int_num ].reserved	= 0x00;
	idt[ int_num ].flags	= flags;



selector、flagsはそのまま入れます。ここではselectorは0x08、

flagsは10001110bを入れています。reservedは0x00固定です

IDTRに読み込ませるデータを設定する

IDTRに読み込ませるデータを作っていきます。IDTRにはIDTのサイズとアドレスが必要です


	idtr.size	= NUM_IDT * sizeof( GATE_DESCRIPTOR );
	idtr.base	= ( GATE_DESCRIPTOR *)idt;



IDTのゲートディスクリプタのサイズ×ゲートディスクリプタの数がIDTのサイズとなります

IDTのアドレスは変数idtの先頭アドレスをbaseに格納します

これでようやく好きな割り込み番号に対応するゲートディスクリプタを設定できるようになりました

次はIDTをLIDT命令で読み込みます

IDTをLIDT命令で読み込む

LIDT命令はANSI Cで規定されている規格の範囲外の命令です

ANSI Cから外れてしまっているので、無理やりC言語に埋め込んでしまいます

このような場合インラインアセンブラとしてC言語に埋め込みます


/*
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	Funtion     :load_idt
	Input       :void
	Output      :void
	Return      :void

	Description :load idt
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
*/
#define	load_idt( ) ({ __asm__ __volatile__ ( "lidt idtr" ); })



C言語にアセンブラの命令を埋め込むには__asm__ __volatile__を使います

__asm__ __volatile__("アセンブラ言語");とC言語の中で記述すれば

そこに”アセンブラ言語”で書かれた命令をC言語に埋め込んでくれます

但し、注意していただきたいのですが、ここで記述するアセンブラ言語はGASで

デフォルトとなっていますAT& T記法で記述する必要があります

ブートローダで使ったnasmのIntel記法の方が分かりやすいのですが、今回はGCCを使用しています

GCCのアセンブラGASがインラインアセンブラはAT& T記法を採用していますので、仕方ありません。。。

仕方ありませんが、慣れてくるとなかなか使い勝手がよくなってくきますので、ぜひやってみてください

lidt命令を実行する関数ができたら、IDTのサイズとアドレスを格納して関数を呼び出します


	idtr.size	= NUM_IDT * sizeof( GATE_DESCRIPTOR );
	idtr.base	= ( GATE_DESCRIPTOR *)idt;
	
	load_idt( );



これでIDTのロードができました

GDTをC言語で構築する

IDTができましたので、GDTもC言語で作ってみます

GDTはカーネルローダその2ででてきました

GDTのビットアサインをもう一度参考までに載せます

セグメントディスクリプタは64ビットの大きさで、セグメントの特権レベル、サイズ、ベースアドレスなどを設定します

セグメントディスクリプタ
ビット ビット名称 説明
0-15 Segment Limit Low そのセグメントのサイズを入れます。セグメントリミットは全部で20ビットのサイズですが
ここは全20ビットのうち下位16ビット(0番目のビットから15番目のビット)を入れます
16-31 Base Address Low そのセグメントの開始アドレスを入れます。ベースアドレスは全部で32ビットのサイズですが
ここは全32ビットのうち下位16ビット(0番目のビットから15番目のビット)を入れます
32-39 Base Address Mid そのセグメントの開始アドレスを入れます。ベースアドレスは全部で32ビットのサイズですが
ここは全32ビットのうち中位8ビット(16番目のビットから23番目のビット)を入れます
40-43 Type セグメントのタイプを設定します。Sビットが1の場合ビット43でコードセグメントかデータセグメントか設定できます。

ビット40 アクセスビット
	0:未アクセス
	1:アクセス済み
	仮想記憶で使用します。そのセグメントにアクセスしたかどうかをセットします
ビット41 リード/ライトビット(データセグメントの場合)
	0:読み取り専用
	1:読み取り/書き込み可能
	リード・ライトのアクセス設定ができます
ビット41 リード/実行ビット(コードセグメントの場合)
	0:実行専用
	1:読み取り/実行可能
	リード・実行のアクセス設定ができます
ビット42 伸長方向ビット(データセグメントの場合)
	0:アップ方向
	1:ダウン方向
	セグメントがメモリの上、下どちらの方向に伸びているかを設定します。
	通常はアップ方向です。
ビット42 コンフォーミングビット(コードセグメントの場合)
	0:非コンフォーミング
	1:コンフォーミング許可
	コンフォーミング許可(1)の場合には、低い特権レベルのプログラムから
	このセグメントのコードを実行することができます。
	非コンフォーミング(0)の場合は、逆に低い特権レベルからはこの
	セグメントのコードを実行することができませんので、カーネルのコードは
	この設定を行います。
ビット43 セグメント
	0:データセグメント
	1:コードセグメント
	セグメントがデータセグメントなのかコードセグメントなのかを設定します

44 S ディスクリプタタイプフラグです
0:システムセグメント用
1:コードセグメント、データセグメント用
45-46 DPL Descriptor Plivilege Levelです。セグメントの特権レベルを設定します
0は最高位の特権レベル。リング0。0から3の値で指定します
セグメントセレクタで指定するRPLのレベルは現在実行中のレベルを意味します
セグメントにアクセスする場合はセグメントセレクタのRPLとこのDPLビットが比較されます
47 P Segment Present Flagです。セグメントがメモリ内に存在しているかどうかを設定します
0:セグメントがメモリ内に無い
1:セグメントがメモリ内に有る
通常はメモリ内にセグメントがありますので、1をセットします
48-51 Segment Limit Hi そのセグメントのサイズを入れます。セグメントリミットは全部で20ビットのサイズですが
ここは全20ビットのうち上位4ビット(16番目のビットから19番目のビット)を入れます
52 AVL Availableフラグです。このビットについてはOS(カーネル)が自由に使用してよいビットとなります
使用目的は自由です
54 D/B デフォルトのオペレーションサイズを設定します
0:16ビット用のコードセグメント、データセグメントに使用します
1:32ビット用のコードセグメント、データセグメントに使用します
55 G Granularity Flag(グラニュラリティフラグ)です。セグメントリミットの単位を設定します
0:セグメントリミットの値をバイト単位として解釈します
1:セグメントリミットの値を×4Kバイト単位として解釈します
通常は1に設定します
56-63 Base Address Hi そのセグメントの開始アドレスを入れます。ベースアドレスは全部で32ビットのサイズですが
ここは全32ビットのうち上位8ビット(24番目のビットから31番目のビット)を入れます


GDTをC言語で作っていきます

まずはGDTのセグメントディスクリプタを定義します

セグメントディスクリプタの定義

				
/*
==================================================================================

	Description : Segment Descriptor of GDT

==================================================================================
*/
typedef struct
{
	unsigned short	limitLo;
	unsigned short	baseLo;
	unsigned char	baseMid;
	unsigned short	flags;
	unsigned char	baseHi;
}  __attribute__ ((packed)) SEGMENT_DESCRIPTOR;



このように定義しました。IDTのゲートディスクリプタの定義と似ていますので要領は同じです

セグメントディスクリプタからGDTを作る


#define	NUM_GDT	3

/*
==================================================================================

	Description : GDT

==================================================================================
*/
SEGMENT_DESCRIPTOR	gdt[ NUM_GDT ];



セグメントディスクリプタをセットアップする

セグメントディスクリプタをセットアップして各セグメントを作っていきます

セットアップするときにいくつかのフラグをセットする必要がありますので、予め#defineしておきます

グローバルディスクリプタのフラグ定義


/*
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/

	Description :definition for Global descriptor
	
	flags
	bit number  description
	0...15		segment limit low	
	16...31		base address low
	32...39		base address mid
	40		access bit		: unaccessed = 0b
						: accessed = 1b
	41		read/write bit		: read only = 0b
						: read/write = 1b
	42		expansion direction	: up direction = 0b
						: down direction = 1b
	43		segment	type		: data segment = 0b
						: code segment = 1b
	44		descriptor flag		: for system segment = 0b
						: for code, data segment = 1b
	45...46		descriptor privedlge	: ring0 = 00b
			level			: ring1 = 01b
						: ring2 = 10b
						: ring3 = 11b
	47		present bit		: Segment is present
	48...51		segment limit hi
	52		AVL			: free to use by system program
	53		reserved
	54		d/b			: for 16bit = 0b
						: for 32bit = 1b
	55		G			: unit of segment is byte
						: unit of segment is 4K
	56...63		base address hi

_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
*/
#define	NULL_DESCRIPTOR			0
#define	CODE_DESCRIPTOR			1
#define	DATA_DESCRIPTOR			2
#define	TEMP_DESCRIPTOR			3
#define	TASK_CODE_DESCRIPTOR		3
#define	TASK_DATA_DESCRIPTOR		4
#define	KTSS_DESCRIPTOR			5

/* Null Descriptor		*/
#define	DEF_GDT_NULL_LIMIT		0x0000
#define	DEF_GDT_NULL_BASELO		0x0000
#define	DEF_GDT_NULL_BASEMID		0x00
#define	DEF_GDT_NULL_FLAGS		0x0000
#define	DEF_GDT_NULL_BASEHI		0x00

/* Code Descriptor		*/
#define	DEF_GDT_CODE_LIMIT		0xFFFF
#define	DEF_GDT_CODE_BASELO		0x0000
#define	DEF_GDT_CODE_BASEMID		0x00
#define	DEF_GDT_CODE_FLAGS_BL		0x9A
#define	DEF_GDT_CODE_FLAGS_BH		0xCF
#define	DEF_GDT_CODE_FLAGS		0xCF9A
#define	DEF_GDT_CODE_BASEHI		0x00

/* Data Descriptor		*/
#define	DEF_GDT_DATA_LIMIT		0xFFFF
#define	DEF_GDT_DATA_BASELO		0x0000
#define	DEF_GDT_DATA_BASEMID		0x00
#define	DEF_GDT_DATA_FLAGS		0xCF92
#define	DEF_GDT_DATA_FLAGS_BL		0x92
#define	DEF_GDT_DATA_FLAGS_BH		0xCF
#define	DEF_GDT_DATA_BASEHI		0x00



セグメントディスクリプタのセットアップ

ゲートディスクリプタのフラグ定義を使ってGDTをセットアップしていきます


	/* set up null descriptor */
	gdt[ NULL_DESCRIPTOR			].limitLo	= DEF_GDT_NULL_LIMIT;
	gdt[ NULL_DESCRIPTOR			].baseLo	= DEF_GDT_NULL_BASELO;
	gdt[ NULL_DESCRIPTOR			].baseMid	= DEF_GDT_NULL_BASEMID;
	gdt[ NULL_DESCRIPTOR			].flags		= DEF_GDT_NULL_FLAGS;
	gdt[ NULL_DESCRIPTOR			].baseHi	= DEF_GDT_NULL_BASEHI;
	/* set up code descriptor */
	gdt[ CODE_DESCRIPTOR			].limitLo	= DEF_GDT_CODE_LIMIT;
	gdt[ CODE_DESCRIPTOR			].baseLo	= DEF_GDT_CODE_BASELO;
	gdt[ CODE_DESCRIPTOR			].baseMid	= DEF_GDT_CODE_BASEMID;
	gdt[ CODE_DESCRIPTOR			].flags		= DEF_GDT_CODE_FLAGS;
	gdt[ CODE_DESCRIPTOR			].baseHi	= DEF_GDT_CODE_BASEHI;

	/* set up data descriptor */
	gdt[ DATA_DESCRIPTOR			].limitLo	= DEF_GDT_DATA_LIMIT;
	gdt[ DATA_DESCRIPTOR			].baseLo	= DEF_GDT_DATA_BASELO;
	gdt[ DATA_DESCRIPTOR			].baseMid	= DEF_GDT_DATA_BASEMID;
	gdt[ DATA_DESCRIPTOR			].flags		= DEF_GDT_DATA_FLAGS;
	gdt[ DATA_DESCRIPTOR			].baseHi	= DEF_GDT_DATA_BASEHI;



IDTと同様に各変数に値をセットする関数を作ってもいいかと思います

GDTRの定義

GDTもIDTと同様にIDTRのようなレジスタを持っています。GDTRといい46ビットのレジスタです

LGDT命令を行うことで、0-15ビットのGDT LimitにはGDTのサイズが、

16-46ビットのGDT Base AddressにはGDTのアドレスが格納されます


/*
==================================================================================

	Description : GDTR

==================================================================================
*/
typedef struct
{
	unsigned short		size;
	SEGMENT_DESCRIPTOR*	base;
} __attribute__ ((packed)) GDTR;

GDTR	gdtr;



ここで定義したgdtrにGDTのサイズとアドレスを入れていきます

GDTRに読み込ませるデータを設定する

GDTRに読み込ませるデータを作っていきます。GDTRにはGDTのサイズとアドレスが必要です


	gdtr.size	= NUM_GDT * sizeof( SEGMENT_DESCRIPTOR );
	gdtr.base	= ( SEGMENT_DESCRIPTOR *)gdt;



GDTをLGDT命令で読み込む

LGDT命令はLIDT命令と同じように使用します


/*
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	Funtion     :load_gdt
	Input       :void
	Output      :void
	Return      :void

	Description :load gdt
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
*/
#define	load_gdt( ) ({ __asm__ __volatile__ ( "lgdt gdtr" ); })



lgdt命令を実行する関数ができたら、GDTのサイズとアドレスを格納して関数を呼び出します


	gdtr.size	= NUM_GDT * sizeof( SEGMENT_DESCRIPTOR );
	gdtr.base	= ( SEGMENT_DESCRIPTOR *)gdt;
	
	load_gdt( );



これでGDTのロードができました

GRUBからカーネルをロードする場合について

ここまでは、このサイトで作成したブートローダーからカーネルをロードした

場合についてのGDTの設定についてとなります。


マルチブート仕様 に基づいて作られたブートローダーからカーネルを

起動した場合は、GDTの再設定を行う必要があります。GRUB起動時に

GRUBはGDTをセットアップしますが、マルチブート仕様を見るとブートローダーは

特に決まったGDTを設定するわけではありません。ですので、

GRUBからカーネルを起動する でカーネルを起動した後はどんなGDTが

設定してあるかはカーネル自身はわかりませんので、自分で独自に作った

カーネル専用のGDTを再設定する必要があります。また、GDTの設定と同時に

セグメントセレクター(CS、DS、ES、FS、GS、SS)も設定する必要があります。

セグメントセレクターの動作については カーネルローダその2 

プロテクティッドモードとGDT
を参照してください。


ということですので、GRUBから起動したときのGDTのロードと

セグメントセレクターの設定について見ていきます。この設定については

このサイトで作成したブートローダーから起動した場合でも動作自体は

変わりませんので、あらかじめプログラムしておくことをおすすめ致します。


それでは、GDTのロードとコードセグメントの設定について見ていきます。

最初に全体を見てみます。


/*
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	Funtion     :load_gdt
	Input       :void
	Output      :void
	Return      :void

	Description :load gdt
_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
*/
PRIVATE INLINE VOID load_gdt( VOID )
{
    __asm__ __volatile__ ( "lgdt gdtr" );
    __asm__ __volatile__ ( "mov %ax, 0x10" );
    __asm__ __volatile__ ( "mov %ds, %ax" );
    __asm__ __volatile__ ( "mov %es, %ax" );
    __asm__ __volatile__ ( "mov %fs, %ax" );
    __asm__ __volatile__ ( "mov %gs, %ax" );
    __asm__ __volatile__ ( "mov %ss, %ax" );
    __asm__ __volatile__ ( "jmp 0x08:_flush_seg" );
    __asm__ __volatile__ ( "_flush_seg:" );
}



まずは、普通にGDTをロードします。


    __asm__ __volatile__ ( "lgdt gdtr" );



次からはAXレジスターに0x10を入れます。0x10という値は、

データセグメントを指すセレクターの値です。このサイトでは、

Nullディスクリプタのセレクターが0x00(0番目のディスクリプター)、

コードセグメントディスクリプターのセレクターが0x08(1番目の

ディスクリプター)、データセグメントのディスクリプターのセレクターが

0x10(2番目のディスクリプター)としていますので、データセグメントの

セレクターに0x10を入れるための準備としてAXレジスターに0x10を

格納します。


    __asm__ __volatile__ ( "mov %ax, 0x10" );



そして、0x10(AXレジスター)の値を順次コードセグメントセレクターに

入れていきます。


    __asm__ __volatile__ ( "mov %ds, %ax" );
    __asm__ __volatile__ ( "mov %es, %ax" );
    __asm__ __volatile__ ( "mov %fs, %ax" );
    __asm__ __volatile__ ( "mov %gs, %ax" );
    __asm__ __volatile__ ( "mov %ss, %ax" );



次が少し複雑なのですが、コードセグメントCSに0x08を入れています。


    __asm__ __volatile__ ( "jmp 0x08:_flush_seg" );
    __asm__ __volatile__ ( "_flush_seg:" );



ジャンプ命令 を使用してコードセグメントに0x08を入れています。

ここで何故ジャンプ命令を使わなければいけないのか?という疑問が

出てくると思います。データセグメントのようにMOV命令を使用して、CSに

0x08を入れるといいのではないかと思いますが、そこには問題があります。

Intelのプロセッサーは非常に賢く、現在の命令を実行しながら、次の

命令や、その次の次の命令をあらかじめ読み込んでどんな命令かを

解析したりします。これをパイプライン処理といいます。命令の実行するまでには

命令の取り出し、命令の解析、命令の実行などの段階があります。

これを1命令につき順次行っていては効率が悪いので、例えば、

命令の解析を行っている最中に、同時に次の命令を取り出すことで

効率をあげています。(もともとはRISCの考え方でしたがIntelも

CISCにこの考え方を導入しました。現在では何十の命令を先取りしています)。


ここでよく考えてみると例えば、次のようにMOV命令でコードセグメントの

セレクターを設定する場合を考えてみます。


    __asm__ __volatile__ ( "mov %ax, 0x08" );
    __asm__ __volatile__ ( "mov %cs, %ax" );
    
    //次の命令が続いています。
    //この例では関数ですので、つぎにret命令が続くと想定します。
    //(実際には__inline__してますので、多分違います。。。)

    __asm__ __volatile__ ( "ret" );
    //上記のret命令は説明のために追加しています。



このような場合、まさにCSに値0x08を設定している最中に、ret命令の

取り出し、解釈が行われます。これはこれでいいのですが、困ったことに、

このret命令の解釈やアドレス計算が行われている前後で違うCSの値

を使用してしまうことに問題があります。また、どこかのタイミングでプロセッサー

内で設定する前のGDTと今回設定したGDTが変わってしまうことにも問題

となります。(実際にIntelがどのようにプロセッサーを実装しているかは

わかりません。プロセッサーの理論上の話となります)。そうなるとパイプライン

内でアドレスアクセスに不整合が生じて想定どおりに命令が実行できない

ことはなんとなく分かっていただけるかと思います。(また実際にはret命令の

時点ではなくて、CPUはあらかじめ沢山の命令を事前に取り込んでいますので

取り込んだ命令のどこかで不整合が発生してしまいます。)


そこで、セグメントセレクターCSの値を変更する際にはパイプラインの

不整合が生じないように一度パイプラインを綺麗さっぱり忘れさせることが

必要となります。パイプラインを綺麗にするにはここで見て来ましたように

条件分岐命令(ここではジャンプ命令でした)を使用します。条件分岐命令

ではパイプライン処理のなかで命令をまさに”実行”する段階でないとアドレスが

決まりませんので、CPUはパイプラインに先取りしている条件以降の命令を

全部破棄します。(True条件以降のほうの命令を先取りするのか、False条件

以降のほうを先取りしているのかはプロセッサーの実装方法によってことなります。

また、命令を廃棄した後にまた新しく命令を先取りしていきます。そのときには

処理速度が遅くなってしまいますので、その間にもう1つのパイプライン処理系に

切り替えて効率化を図るプロセッサーもあります。Intelが開発したハイパー

スレッディング方式となります。)


しかし、パイプラインをクリアーしただけでは問題が発生します。条件分岐命令が

実行された後はまたパイプラインに命令が取り込まれますので、ここで同様の

不整合が発生してしまいます。そこで、パイプラインをクリアーするのと同時に

CSの値も変更する必要があります。このためにジャンプ命令を使ってファーコールを

行います。”ファーコール”?とは”ファー=遠い”と”コール=呼び出し”という意味と

なります。何が”遠い”のかというと違うセグメントにある命令を呼出すことを

”遠い=ファー”ということになります。同一セグメントだと”ニアコール”となります。

この場合は”ニア=近い”で同じセグメント内の呼び出しですので、”近い”と

呼びます。このようにCPUはファーコールを行うときにはCSの値を書き換えます

ので、条件分岐でパイプラインをクリアーするのと同時にCSの値を書き換える

ことができます。



    __asm__ __volatile__ ( "jmp 0x08:_flush_seg" );
    __asm__ __volatile__ ( "_flush_seg:" );



という1命令(_flush_segは命令ではありません)にこんな意図が隠されて

います。



今回はここまでです。次回はPICとIRQについて説明いたします

inserted by FC2 system