|
0から作るOS開発 カーネルローダその1 メモリマップ
|
前回までの内容
これまでで、
- FAT12ファイルシステムにはブートセクタ、FAT領域、ルートディレクトリ領域、ファイル領域で構成される
- ブートセクタにはFAT12に関する情報が記録されている
- FAT領域にはクラスタが格納されている
クラスタはファイルがどのセクタに記録されているかの情報が記憶されている
- ルートディレクトリ領域には32バイトのエントリがあり、
エントリはA:ドライブの直下にあるファイル情報を記録している
- ファイル領域にはファイルのデータが格納されている
- ファイルを読み込む手順は
1.ルートディレクトリのエントリからファイル名を探し出す
2.見つかったエントリからファイルの開始クラスタ番号を取得する
3.FAT領域の取得したクラスタ番号のクラスタを調べる
4.調べたクラスタ番号からファイル格納セクタを計算し読み込む
ということがわかりました。それではブートローダの開発を進めていきましょう!
前回までで、ファイルをメモリに読み込むことができるようになりました
読み込みができるようになったので、読み込み先のアドレスを決めるためにメモリマップを見てきます
メモリマップ
パソコンのメモリは好き勝手に使ってよい場所と、そうでない場所があります
どのアドレスを使っていけばよいのかを見て行きましょう
このメモリマップは”リアルモード”で使用できるメモリのマップとなります
リアルモードでアクセスできるメモリは約1MBで0x0010FFEFまででした
この図では0x00100000までのメモリが何に使われているのかを示しています
リアルモードでRAMとして自由に使えるエリアは未使用のエリアとなります
ですので、0x00000500から0x000A0000にOS(カーネル)をロードすれば良いのでしょうか
そこにはいくつか問題があります
OS(カーネル)ロードの問題点
リアルモードでOS(カーネル)をロードするときの問題点として以下が考えられます
- 0x00000500から0x000A0000を目一杯使ってここにカーネルをロードするとしても、
そのサイズは最大で638kバイト
- 上記からカーネルのサイズは最大で638kバイトに制限されてしまう
- リアルモードでは0x0010FFEFまでしかアクセスできないので、0x00100000以降の
広大なメモリ領域にカーネルを置くことができない
そこで、約4Gバイト(0xFFFFFFFF)のメモリまでアクセスできる32ビット動作の
プロテクティッドモードに移行してからカーネルをロードします
32ビットプロテクティッドモードの利点としては
- 0x00100000以降の広大なメモリ領域を使うことができる
- 4Gバイトまでのメモリを扱うことができる
- 仮想メモリが使用することができる
となります。ですので、カーネルをロードする前に32ビットのプロテクティッドモードに移行してからロードします
しかし、そこでも問題点が浮上してきます
プロテクティッドモードに移行するときの問題点としては
- ブートローダのサイズが512バイトで、32ビット動作時の32ビット命令を入れる必要があり、サイズ的に難しい
32ビットモードでは扱うデータのサイズが基本的に32ビット(4バイト)となります
ですので、指定するアドレスも4バイト、命令も4バイト、使うメモリもレジスタも4バイトとなり
コードサイズが劇的に増えてしまうので、512バイトでは収まりきらなくなってきます
そこで、一般的なOSをロードする手法として、ブートローダを2段階構成にします
1段階目はここまで紹介してきました512バイトのブートローダで、2段階目のローダをロードします
2段階目は32ビットモードへの移行とカーネルをロードする方針をとります
このホームページでは2段階目のブートローダを
カーネルローダとして位置づけます
カーネルローダをロードする
ではどこに2段階目のローダをロードするかを見て行きましょう
メモリの未使用領域のどこに読み込んでもいいのですが、決めないと作れないので決めておきます
ここではただの例ですので、自由に決めていただいてなんら問題ありません
一応下記例を載せておりますので、ご参考になればと思います
この例ではブートローダの1段階目で読み込むFAT領域を0x00007E00に、
ルートディレクトリ領域を0x0000A200に、ブートローダ(2段階目)を0x00000500に読み込む例としております
今回のブートローダではこのようにメモリを使うことを想定して作ってまいります
そして、2段階目のブートローダでいよいよカーネルをロードしていきます
カーネルは
0x00100000にロードします
一般的なOSではカーネルをこの場所にロードしているようですので、その慣例通りにしてみます
それではカーネルローダを作っていきましょう
はじめてのカーネルローダ
まずは簡単なソースを作って、1段階目から2段階目のブートローダ(カーネルローダ)を読み込めるかどうかテストします
まずはカーネルローダを作ってみます。この例では”Starting.asm”というファイル名にしました
(名前はなんでもいいです)
メッセージを表示するだけのプログラムとなります
[BITS 16]
ORG 0x500
JMP MAIN2
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; Preprocessor directives
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
%include "Print.inc"
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; Data Section
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
msgphello DB 0x0D, 0x0A, "Hello", 0x0D, 0x0A, 0x00
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; Starting Kernel Procedure
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
MAIN2:
; 汎用レジスタ初期化
XOR AX, AX
XOR BX, BX
XOR CX, CX
XOR DX, DX
; データセグメント初期化
MOV DS, AX
MOV ES, AX
; メッセージ表示
MOV SI, msgphello
CALL DisplayMessage
HLT
ここでカーネルローダは0x00000500にロードすることを想定していますので、
ベースアドレスを0x00000500としました
[BITS 16]
ORG 0x500
JMP MAIN2
あたらしい、NASMの擬似命令がでてきました
これは”Print.inc”ファイルをインクルードする命令です
%include "Print.inc"
別途"Print.inc"というファイル名を作ります
"Print.inc"には1段階目のブートローダででてきたDisplayMessage関数を書いております
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; Display Message
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
%ifndef __Print_INC_INCLUDED__
%define __Print_INC_INCLUDED__
[BITS 16]
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; DisplayMessage
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
DisplayMessage:
PUSH AX
PUSH BX
StartDispMsg:
LODSB
OR AL, AL
JZ .DONE
MOV AH, 0x0E
MOV BH, 0x00
MOV BL, 0x07
INT 0x10
JMP StartDispMsg
.DONE:
POP BX
POP AX
RET
%endif
このようにアセンブラ言語を別のファイルに書いてモジュールとして管理できます
カーネルローダをアセンブルします
nasm -o STARTING.IMG Starting.asm
すると”STARTING.IMG”(ファイル名はなんでもいいです)ができますので、
"STARTING.IMG"をAドライブにコピーします
cp STARTING.IMG /cygdrive/a/
CygwinでAドライブを指定する場合は"/cygdrive/a/"と記述します
1段階目のブートローダからカーネルローダへ
では1段階目のブートローダのソースに処理を追加してみます
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; Load Image
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
LOAD_IMAGE:
MOV AX, WORD [cluster]
POP BX
CALL ClusterLBA
XOR CX, CX
MOV CL, BYTE [BPB_SecPerClus]
CALL ReadSector
ADD BX, 0x0200
PUSH BX
; ここから次のクラスタを調べる
MOV AX, WORD [cluster]
MOV CX, AX
MOV DX, AX
SHR DX, 0x0001
ADD CX, DX
MOV BX, WORD[BX_FAT_ADDR]
ADD BX, CX
MOV DX, WORD [BX]
TEST AX, 0x0001
JNZ ODD_CLUSTER
EVEN_CLUSTER:
AND DX, 0x0FFF
JMP LOCAL_DONE
ODD_CLUSTER:
SHR DX, 0x0004
LOCAL_DONE:
MOV WORD [cluster], DX
CMP DX, 0x0FF0
JB LOAD_IMAGE
ALL_DONE:
POP BX
PUSH WORD [ES_IMAGE_ADDR] ; ここを追加します。ES_IMAGE_ADDRは0x0050
PUSH WORD 0x0000 ; ここを追加します
RETF ; ここを追加します
HLT ; 停止
ここでRETF命令を使用しています。RETF命令は関数からのリターン命令です
通常のRET命令は同じコードセグメント内にあるアドレスに戻りますが、
カーネルローダを遠くに置く場合、コードセグメントの開始位置を変える必要があります
(例えば0x00100000に置く場合コードセグメントCSは0x0001にしたほうが都合がいいと思います)
そのため、コードセグメントを変更しつつ、ジャンプしたい場合に使用しています
(今回の例では遠く離れたアドレスではないので、通常のジャンプでもよいかと思います)
RETF命令はスタックの一番上に積んであるデータをPOPしてきて、それをRETF命令後のIPとして使用します
また、スタックの一番上から2番目のデータをPOPしてきてコードセグメントCSの値として使用します
ですので、RETF命令の前にCSの値と、IPの値をPUSHしてからRETFしております
この例ではCSの値として0x0050、IPの値として0x0000をPUSHしています
これでRETF命令を実行すれば0x0050:0x0000(0x00000500)にジャンプします
これでカーネルローダへ移行することができました
次は32ビットのプロテクティッドモードについて見ていきます