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

日々勉強中。。。

0から作るOS開発

環境準備

環境設定

ブートローダ

カーネルローダ

GRUB

カーネル

ドライバーその他

0から作るOS開発 カーネルローダその2 プロテクティッドモードとGDT

前回までの内容

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

前回までで、カーネルローダに移行することができました

今回は主に32ビットで動作するプロテクティッドモードについて見ていきます

最初は32ビットのレジスタについて説明します

32ビットレジスタ

今までは16ビットのレジスタのみを紹介してきました(EFLAGSレジスタは32ビットでした)

32ビットのプロテクティッドモードにする前に、32ビットのレジスタについて紹介します

16ビットの汎用レジスタAX、BX、CX、DX、BP、SI、DI、SPは拡張されて(Extended)

それぞくれEAX、EBX、ECX、EDX、EBP、ESI、EDI、ESPの名前で32ビットのレジスタとして

使用できます。また、命令ポインタIPもEIPとして32ビットのレジスタとして使用できます

(もちろんAX、AHやALなども同時に使用できます)

16ビットの汎用レジスタAX、BX、CX、DX、BP、SI、DI、SPは拡張されてEAX、EBX、ECX、EDX、EBP、ESI、EDI、ESPの名前で32ビットのレジスタとして使用できる

それでは本題のプロテクティッドモードについて見ていきたいと思います

プロテクティッドモード

プロテクティッドモード(Protected Mode)は仮想メモリが扱えますので、

その名前の通り”保護モード”と言えます。

プロテクティッドモードの特徴として

などがあります。特にリアルモードで使用してきましたセグメントレジスタとその使いかたが

劇的に変わってしますので、注意が必要です

今回はプロテクティッドモードへの移行する手順について見ていきます

プロテクティッドモードへの移行

プロテクティッドモードへ移行し、4GBのメモリ空間を利用するには次の手続きを踏む必要があります

この3段階の設定を行う必要があります

プロテクティッドモード用のメモリアクセス設定はGDT(Global Descriptor Table)を使った

メモリアクセスを行う準備をします。次のプロテクティッドモードをONするにはCPUの

特殊なレジスタであるコントロールレジスタCR0の設定を行います

そして、4GBののメモリ空間を利用するにはCPUのアドレスバスを使用出来るようにキーボードコントローラから設定します

またまた、いきなり難易度が急上昇しました。難しい語句がいきなりでてきて何のことやらさっぱりです。。。

かなり難しいので、順番に詳細を見ていきたいと思います。まずはプロテクティッドモード用のメモリアクセス設定を行う方法です

プロテクティッド用のメモリアクセス設定 GDT(Global Descriptor Table)

リアルモードではコードセグメントCS、データセグメントDS、FS、ES、スタックセグメントSSと各セグメントに対応したオフセットを

使ってメモリアクセスしていました。プロテクティッドモードでは上記のセグメントレジスタとGDTを使ってメモリにアクセスします

ではGDTについてみていきましょう

GDT(Global Descriptor Table)

GDTを日本語で言うと”大域的記述子表”といったところでしょうか。日本語にするとますます難しい。。。ですが

Tableというその名の通りテーブルで、GDTは64ビットを1つのデータとした配列(テーブル)となります

そして1つの要素である64ビットのデータはセグメントディスクリプタ(Segment Descriptor)といい、

セグメントの開始位置、セグメントのサイズアクセス権限の設定などを細かく設定できます

このセグメントディスクリプタは全部で8192個まで持つことができます

プロテクティッドモードでは仮想メモリとしてセグメンテーションが扱えるように

GDTが作られています。逆にGDTを巧みに駆使することでセグメンテーションを行います

LinuxなどのOSは仮想メモリとしてセグメンテーションではなく管理が比較的楽なページングを使っていますが、パソコンについては

CPUの仕様上セグメンテーションの設定を行う必要があります。必要ありますが、Linuxでは4GBのメモリ空間全部を

1つのセグメントとしてみなしてGDTの設定をしています。このホームページでもその方針でいきます

GDTとセグメントレジスタの役割

GDTはセグメントディスクリプタのテーブルで、各セグメントディスクリプタはメモリ上のセグメントに対応しています

その対応したセグメントの開始位置、サイズ、アクセス権限の設定をGDTのセグメントディスクリプタで行うことができます

GDTのセグメントディスクリプタは各セグメントの位置、大きさ、アクセス権限を設定する

リアルモードでは各セグメントの開始位置はCS、DS、ES、GS、FS、SSで決定していましたが、

このようにプロテクティッドモードではGDTのセグメントディスクリプタで決定するため

リアルモードで使用していたCS、DS、ES、FS、GS、SSは違う役割となります

プロテクティッドモードのセグメントレジスタCS、DS、ES、FS、GS、SSの役割はGDTの何番目の

セグメントディスクリプタを使用するかを決めます

図で見て行きましょう

プロテクティッドモードのセグメントレジスタはGDTのセグメントディスクリプタをオフセットで指定する

このようにセグメントレジスタはGDTのセグメントディスクリプタの何番目を使用するのかを決めます

何番目かを指定するときにはセグメントディスクリプタのオフセット番号を指定します

例えば図のCSは1番目(最初を0番目として)を指しています

セグメントディスクリプタのサイズは64ビットで8バイトですので、1番目のセグメントディスクリプタの

開始位置は8バイト目となります。ですので、CSは0x08を入れると1番目のセグメントディスクリプタを指すことになります

同様にDSには0x10が入っています。2番目のセグメントディスクリプタの開始位置は16バイト目ですので

DSに0x10(16)を入れることで2番目のセグメントディスクリプタを指すことになります

このようにセグメントレジスタの値はGDTのセグメントディスクリプタを選択する役割となりますので、

セグメントセレクタと呼ばれます

論理アドレスとリニアアドレス

リアルモードではセグメントレジスタの値:オフセットの論理アドレス(Logical Address)でメモリにアクセスしました

論理アドレスを計算したアドレスがリニアアドレス(Linear Address)でした。同様にプロテクティッドモードでも

[セグメントセレクタの値:オフセット]が論理アドレスとなります

ただしセグメントセレクタの値はリアルモードと同様16ビットとなりますが、オフセットは32ビットとなります

論理アドレスのオフセットとセグメントセレクタが選択するセグメントディスクリプタのベースアドレスからリニアアドレスを計算する

このように、例えばプログラムでアドレス0x00001000の場所にアクセスする場合

となります。リニアアドレスに変換する作業はCPUが勝手に計算してくれます

ここで注意していただきたいのですが、プログラム中ではオフセットであるアドレスだけを主に書いています

論理アドレスはセグメントレジスタの値とアドレス(オフセット)の組み合わせですので、

プログラム中に明示的にセグメントレジスタを指定しなければ、CPUはどのセグメントセレクタを

使っていいのかわかりません。ですが、主に使うセレクタはCS(コード)、DS(データ)、SS(スタック)

だけですので、プログラムでは特にどのセグメントセレクタを使うのかを書く必要はありません

例えばプログラムを実行するのなら決まってCSですし、データの読み書きはDSを使います

POP、PUSH命令であれば自動的にSSのセレクタを使うので、プログラムにはアドレス(オフセット)だけを書きます

ES、FS、GSのセグメントセレクタを意図的に使いわける場合は明示的に指定する必要があります

(またはES、FS、GSレジスタを使う命令を使うときには意識して使う必要があります)

セグメントレジスタのセグメントセレクタ

プロテクティッドモードはメモリを保護するモードでメモリにアクセスする権限を設定できます

アクセス権限にはレベルがあり、特権レベルと呼ばれます。特権レベルは0から3までのレベルをとります

とくにカーネルでは特権レベルはリングと呼ばれますので、リングはリング0からリング3まであります

リングはリング0が最も特権レベルが高く、リング3が最も低い特権レベルとなります

LinuxなどのOSではカーネルにリング0の特権レベルが与えられ、ユーザアプリケーションはリング3が与えられています

特にリング1、2の特権レベルは使用されておらず、リング0とリング3が使われています

セグメントにアクセスするときにリングレベルによってアクセスできるかどうかが決まります

そのセグメントにアクセスできるリングレベルのこを必要な特権レベル(RPL:Requested Privilege Level)といいます

RPLはセグメントセレクタの値で設定されます。それではセグメントセレクタのビットアサインを見ていきましょう

セグメントセレクタは3ビット目から15ビット目がIndexビット、2ビット目がTIビット、1ビット目から0ビット目がRPLビットとなります

セグメントセレクタ
ビット ビット名称 説明
0-1 RPL そのセグメントアクセスに要求される特権レベル
0が最高位のレベル、3が最低のレベル
2 TI テーブルインジケータ
0:GDT
1:LDT(Local Descriptor Table)
※LDTはローカルディスクリプタテーブルといいます。GDTと同じようにセグメントディスクリプタを持っています
3-15 Index GDTのインデックス
インデックスはGDTの何番目のディスクリプタを指しているかを示す値です
インデックスにはGDTのディスクリプタ番号×8を計算した値をいれてください


セグメントレジスタにセグメントセレクタの値を設定するにま今までどおりMOV命令を使用します(例外もあります)

例えばリング0でGDTの1番目のセグメントディスクリプタが示すコードセグメントを使用するときは


	MOV CS, 0x08



とします。またリング3で同じくコードセグメントを使用するときは

	MOV CS, 0x0B



としますが、特権レベルを切り替えるときはセグメントにアクセス権があるかどうかの注意が必要となります

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

セグメントディスクリプタをセグメントセレクタで指定することができるようになりました

次に、セグメントセレクタが指定するGDT、LDT(セグメントセレクタの表で少しでてきました

今は関係ありませんので、こんなものがあるんだぐらいで)のセグメントディスクリプタについて見ていきます

セグメントディスクリプタは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を作っていきます

GDTの作成とロード

GDTのデータをCPUに教える命令がLGDT命令です。LGDT命令ではGDTのサイズとアドレスを格納している

アドレスを指定します。GDTのサイズとアドレスを格納しているデータを見てみましょう


gdt_toc:			; GDTとサイズとアドレスを格納しているアドレスラベル
		DW 8*4		; GDTのサイズ
		DD _gdt		; GDTのアドレス



この例ではgdt_tocにGDTのサイズとアドレスを格納しています。GDTのセグメントディスクリプタは64ビットで8バイト

ですので、今回はセグメントディスクリプタを4つ作りますので8バイト×4としました

サイズは16ビットで、アドレスは32ビットの大きさで格納しておきます

そしてgdt_tocをLGDTで読み込みます


	LGDT	[gdt_toc]	; gdt_tocのアドレスを指定して読み込む



読み込む前にGDTのテーブルを作っておきます


_gdt:					; このラベルをgdt_tocに登録する
		; Null descriptor
		DW 0x0000		; 全部値が0のNullディスクリプタを作ります
		DW 0x0000		; Intel CPUの仕様で決まっています
		DW 0x0000		
		DW 0x0000		
		
		; Code descriptor				
		DB 0xFF			; コードセグメントのサイズ(Segment Limit)を入れます
		DB 0xFF			; サイズは0xFFFFFを入れます
		DW 0x0000		; コードセグメントのベースアドレスを入れます。0x00000000を入れます
		DB 0x00			; Base Address Mid
		DB 10011010b		; Type、S、DPL、Pの値入れます
		DB 11001111b		; Segment Limit Hi、AVL、0、D/B、Gの値を入れます
		DB 0			; Base Address Hi

		; Data descriptor
		DB 0xFF			; データセグメントのサイズ(Segment Limit)を入れます
		DB 0xFF			; サイズは0xFFFFFを入れます
		DW 0x0000		; データセグメントのベースアドレスを入れます。0x00000000を入れます
		DB 0x00			; Base Address Mid
		DB 10010010b		; Type、S、DPL、Pの値入れます
		DB 11001111b		; Segment Limit Hi、AVL、0、D/B、Gの値を入れます
		DB 0			; Base Address Hi	
		
		; TEMPORARY
		DW 0x0000	
		DW 0x0000		
		DW 0x0000		
		DW 0x0000		



GDTでセットアップする必要がある必須のセグメントディスクリプタとして3つのディスクリプタがあります

(例ではテンポラリを作っていますが特にいりません)

この3つのセグメントディスクリプタについてみていきます

Nullディスクリプタ

Intelの仕様では0番目のセグメントディスクリプタとして値が全部0のNullディスクリプタを作る必要があります


		; Null descriptor
		DW 0x0000		; 全部値が0のNullディスクリプタを作ります
		DW 0x0000		; Intel CPUの仕様で決まっています
		DW 0x0000		
		DW 0x0000



値を0で全部埋めます。セグメントディスクリプタは64ビットで8バイトであることに注意してください

コードセグメントディスクリプタ

特にプログラムを置くセグメントの設定をするディスクリプタとなります


		; Code descriptor				
		DB 0xFF			; コードセグメントのサイズ(Segment Limit)を入れます
		DB 0xFF			; サイズは0xFFFFFを入れます
		DW 0x0000		; コードセグメントのベースアドレスを入れます。0x00000000を入れます
		DB 0x00			; Base Address Mid
		DB 10011010b		; Type、S、DPL、Pの値入れます
		DB 11001111b		; Segment Limit Hi、AVL、0、D/B、Gの値を入れます
		DB 0			; Base Address Hi



GDTは難しいので一つずつ見ていきたいと思います

		
		DB 0xFF			; コードセグメントのサイズ(Segment Limit)を入れます
		DB 0xFF			; サイズは0xFFFFFを入れます



最初の0ビット目から15ビット目はSegment Limit Lowです

Segment Limitはセグメントのサイズを決めます。セグメントのサイズは55ビット目のGビットも関係します

通常32ビット仮想記憶は4GBまで扱えますので、Gビットを1にしてSegment Limitの単位を4Kバイトに

します。この例ではSegment Limitのサイズは0xFFFFにしていますので、

0xFFFFF×4Kバイト=0xFFFFF000がセグメントのサイズとなります

LinuxなどのOSでは主にページングを使用しますが、Intel CPUの場合は使用上セグメンテーションの

設定が必須となっています。そこのでセグメンテーションを意識することなく使用するために、

仮想メモリ全体を1つのセグメントとして設定しています。この例でも同様にしていて、

ベースアドレス0x00000000でサイズ0xFFFFF000の大きな1つのセグメントとして設定を行います

(この例ではDBを2つ置いてますがDWを1つのほうが見やすいかと思います)

		
		DW 0x0000		; コードセグメントのベースアドレスを入れます。0x00000000を入れます
		DB 0x00			; Base Address Mid



16ビット目から31ビット目はBase Address Lowで32ビット目から39ビット目はBase Address Mideです

ベースアドレスは0x00000000にしたいので両方0x00で埋めます

		
		DB 10011010b		; Type、S、DPL、Pの値入れます



次は40ビット目から47ビット目までの設定を行なっています

		
		DB 11001111b		; Segment Limit Hi、AVL、0、D/B、Gの値を入れます



次は48ビット目から55ビット目までの設定を行なっています

		
		DB 0			; Base Address Hi



ベースアドレス0x00000000の最上位バイト0x00を入れます

ここまでがコードセグメントディスクリプタの設定となります

データディスクリプタ

データディスクリプタはデータを読み書きするセグメントを設定します

こちらも同様に4GBのメモリ空間を1つの大きなセグメントとして扱います


		DB 0xFF			; データセグメントのサイズ(Segment Limit)を入れます
		DB 0xFF			; サイズは0xFFFFFを入れます
		DW 0x0000		; データセグメントのベースアドレスを入れます。0x00000000を入れます
		DB 0x00			; Base Address Mid
		DB 10010010b		; Type、S、DPL、Pの値入れます
		DB 11001111b		; Segment Limit Hi、AVL、0、D/B、Gの値を入れます
		DB 0			; Base Address Hi	



データセグメントディスクリプタはコードセグメントディスクリプタとほとんど同じ設定となります

違う箇所はコードセグメントとして設定している箇所だけです


		DB 10010010b		; Type、S、DPL、Pの値入れます



43ビット目のセグメントの設定で0を入れ、データセグメントとして設定します。このビットのみコードセグメントと違います

TEMPORARYディスクリプタについて

昔に作ったプログラムですので、特に意味も無くテスト用に作っています。特に設ける必要はありません。。。

このディスクリプタを作らない場合はGLDT命令でロードする内容を変更する必要があります


gdt_toc:			; GDTとサイズとアドレスを格納しているアドレスラベル
		DW 8*3		; GDTのサイズ
		DD _gdt		; GDTのアドレス



ディスクリプタがNull、コード、データの3つになりますので、上記の用に変更してください

GDTの他にもLDT(Local Descriptor Table)や割り込みを使うときに使用する

IDT(Interrupt Descriptor Table)などがありますが、後ほどご紹介いたします

GDT設定のまとめ

Nullディスクリプタ、コードディスクリプタ、データディスクリプタを用意します

GDTのサイズ、アドレスを設定してLGDT命令でGDTをロードします

ではアセンブラ言語のまとめを見てみましょう

	
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; Set up GDT
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
_setup_gdt:
	CLI				; 割り込みを禁止します
	PUSHA				; AX、CX、DX、BX、SP、BP、SI、DIレジスタをPUSHします
	LGDT	[gdt_toc]		; GDTをロードします
	STI				; 割り込みを有効にします
	POPA				; DI、SI、BP、SP、BX、DX、CX、AXにPOPします
	RET


;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; Global Descriptor Table
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
gdt_toc:
		DW 4*8
		DD _gdt	

_gdt:					; このラベルをgdt_tocに登録する
		; Null descriptor
		DW 0x0000		; 全部値が0のNullディスクリプタを作ります
		DW 0x0000		; Intel CPUの仕様で決まっています
		DW 0x0000		
		DW 0x0000		
		
		; Code descriptor				
		DB 0xFF			; コードセグメントのサイズ(Segment Limit)を入れます
		DB 0xFF			; サイズは0xFFFFFを入れます
		DW 0x0000		; コードセグメントのベースアドレスを入れます。0x00000000を入れます
		DB 0x00			; Base Address Mid
		DB 10011010b		; Type、S、DPL、Pの値入れます
		DB 11001111b		; Segment Limit Hi、AVL、0、D/B、Gの値を入れます
		DB 0			; Base Address Hi

		; Data descriptor
		DB 0xFF			; データセグメントのサイズ(Segment Limit)を入れます
		DB 0xFF			; サイズは0xFFFFFを入れます
		DW 0x0000		; データセグメントのベースアドレスを入れます。0x00000000を入れます
		DB 0x00			; Base Address Mid
		DB 10010010b		; Type、S、DPL、Pの値入れます
		DB 11001111b		; Segment Limit Hi、AVL、0、D/B、Gの値を入れます
		DB 0			; Base Address Hi	
		
		; TEMPORARY
		DW 0x0000	
		DW 0x0000		
		DW 0x0000		
		DW 0x0000		



関数_setup_gdtを呼出すことで、GDTの設定を行います

_setup_gdtではLGDT命令でGDTをロードしたときに例外が発生しないようCLIで割り込みを禁止しています

GDTは8192個持つことができますが、8192個もテーブルを持ってしまうと

8バイト×8192個=65536バイト(64Kバイト)もメモリを消費してしまいますので

なるべく少ないテーブルにした方がいいかと思います

inserted by FC2 system