カーネル(その2)
前回は、カーネルの呼出し、キー割り込みと VRAM の予約領域の設定の話をした。
今回の主な話は次の 3 つ
○ 拡張メモリ領域へのデータ転送
○ ページング機能の有効化
○ ページフォルトの実装
又、ソースはこちら
(言葉の定義)
以後、各処理の実行タイミングについて議論する際、対象となる処理がどのタイミングで実行されるのかを明確にするために、各プログラム(ルーチン)を起動してから呼ばれる順に再定義する。
・ファーストステージ
ブートセクタ(HDD の場合は MBR) に書かれたプログラム。主な処理は、FDD の初期設定と、以降のデータを適当なサイズ (onix では 10 セクタ) だけセカンドステージのメモリ領域に読み出す事。そして、セカンドステージへのジャンプ。
・セカンドステージ
ブートローダーのメインの処理。各種ハードウェア(HDD, キーボード, PIC)のチェックと初期設定。物理メモリサイズのチェックとブートパラメータの作成。A20 アドレスラインの初期化、そして GDT の初期化と登録。加えて、今回話をする拡張メモリ領域へのデータ転送処理を行っている。
・スタートアップルーチン(NEW)
新しくカーネルの前にセカンドステージから呼び出されるプログラム、今回話をする(仮)ページング機能の初期化と、ページング機能の有効化。そして、カーネルの呼出しを行っている。
・カーネル
onix のメインコンポーネント。
○ 拡張メモリ領域へのデータ転送
先にも言ってしまったが、この処理はこれはセカンドステージにおける処理になる。これまで、FD からのデータの読み出しは、ファーストステージで行っている int $0x13 (ah = 0x20) によってのみおこなわれていた。32bit プロテクトモードに以降した後で、フロッピーからデータを持ってこようとすると、データ読み出しにおける FDD コントローラの制御処理を自前で用意しなければならず面倒なので、できれば bios コールを使いたい。けれども、リアルモードにおいて扱えるアドレス幅は 1MB なので、FDD の全ての内容をそのままメモリ上には置けない。
リアルモードにおいて、1MB 以降のメモリ領域に対してデータの転送を行う為に int $0x15 (ah = 0x87) bios コールを利用した。これによって 16MB までの領域に対してデータの転送が可能になる。
しかし、この処理はメモリの転送処理になるので、FDD から直接拡張メモリ領域に対して、データを転送する事はできない。なので、一端メモリの通常領域に対して FDD からデータを読み込んで、読み込んだデータを拡張メモリ領域に転送するという処理を繰り返す。
ソースでは、second.S の _block_copy が拡張メモリ領域に対してデータ転送処理を行う。
_block_copy:
pusha
movw $0x9000, %ax
movw %ax, %es
xor %eax, %eax
movl $_ext_copy_gdt, %eax
movl %eax, %esi
addl $0x10, %eax
movw $0xffff, (%eax)
movl 0x24(%esp), %edx
movl %edx, 0x02(%eax)
movw $0x93, 0x05(%eax)
addl $0x08, %eax
movw $0xffff, (%eax)
movl 0x28(%esp), %edx
movl %edx, 0x02(%eax)
movw $0x93, 0x05(%eax)
xorl %ecx, %ecx
movl 0x2c(%esp), %ecx
movl $0x00008700, %eax
int $0x15
popa
ret
bios は gdt を利用して拡張メモリ領域に対してデータの転送処理を行っている。利用するディスクリプタは 2 個目と 3 個目で、各ディスクリプタのフォーマットは次ページに書かれている。
http://www.delorie.com/djgpp/doc/rbinter/id/35/15.html
各セグメントのアクセス権限は、上記のサイトで意味ありげに書かれている 0x93 という値を利用してみる(恐らく推奨値)。
尚、gdt は、後に lgdt で登録している領域と分けている理由は特に無いが、メモリ領域を多目的に使いまわさなければならない程、メモリに貧しい時代には生きておらず、それよりも多目的に使うことによって生じるバグを恐れた為である。
_block_copy の呼出し側の関数 load_images() [calc.c] では、セカンドステージ以降のフロッピーデータを物理アドレス 0x93000 番地から始まるバッファ領域に置いて、0x100000 番地以降の領域へ転送している。
メモリの通常領域のバッファ領域の直後にデータを置かない理由は、通常領域の後方には bios の予約領域が存在するので、それらの地雷を避けるために 0x100000 番地以降に転送している。
○ ページング機能の有効化
アーキテクチャのアドレス変換のうち、一般に論理アドレスと呼ばれるものからリニアアドレスへの変換処理については、前回の gdt の初期化及び登録によって行われるようになった。今回は、リニアアドレスを物理アドレスに変換する仕組みについて、実装したものをみてゆく。
ページング機能の有効化は、cr0 レジスタの最上位ビットをアサートする事によって行われる。ブート時には、これはネゲートされている。単にこれをアサートしただけではシステムは止まる。これを有効にする為に、ページディレクトリテーブル及びページテーブル (以下、特に断らない限り、これらを区別なく "ページテーブル" と呼ぶ) の初期化を行う。この処理を行っているのが、startup.S である。
詳しい実装を見る前に、仕組みをおさらいする。
ハードウェアのページング機能が有効な場合におけるリニアアドレスから物理アドレスへの変換は、2階層のページテーブルを参照する。
具体的なアドレス変換の方法は、指定されたリニアアドレスの上位 10 bit が cr3 レジスタが参照するページディレクトリのオフセットになり、リニアアドレスの 12-21 bit が先に解決したページディレクトリのエントリが参照するページテーブルのオフセットになり、下位 12 bit が先に解決したページディレクトリが参照するページのオフセットになる。
ページテーブルの各エントリのビットフィールドは、次のページに詳しく書かれている。
http://caspar.hazymoon.jp/OpenBSD/annex/intel_paging.html
ページテーブルを実装する上での注意 (はまりやすいポイント) として次の 2 つが存在する。
・ページテーブルの場所
・ページングを有効にする場所
まずは "ページテーブルの場所" について話をする。
ページテーブルのエントリ数は各 1K 個で、各エントリのアドレスフィールドは先のリンクのサイトに書かれているようになっている。アドレスフィールドが 20bit になっているのは、ページテーブルのサイズが固定であり、各エントリが 4byte と決められている為、20bit で 32bit アドレス空間内でページテーブルを識別できる為である。つまりページテーブルは、 4K のアライメントで存在しなければならず、適当な場所にページテーブルが存在すると、ページディレクトリからページテーブルを見付けられなくなってしまう。
startup.S では、.org によって、下位 12bit が 0 になる適当な領域にページテーブルを置き、以後しばらくの間この領域をカーネルの予約領域として不可侵に扱わなければならない。
次に、"ページングを有効にする場所" について話をする。
ページテーブルを先に話したようなきちんとした場所に置き、cr3 レジスタにその物理アドレスを登録した場合、あとはページング機能を有効にするだけなのだが。これまた適当な場所 (設定したページテーブルにおいて、現在のアドレスポインタを解決した時に現在の命令が格納されている物理メモリにマッピングされない場所) に立っている場合、ページング機能を有効化した瞬間、迷子になる。
onix では (linux同様に) 、リニアアドレスの 0x00000000 番地から始まる 8MB と、0xC0000000 番地から始まる 8MB を物理アドレスの 0x00000000 番地から始まる 8MB にマッピングさせる。これは、この後に呼び出すカーネルをリニアアドレスの 0xC0000000 番地に置く為である。なので、ページディレクトリは、0番 と 1番、そして 768番 と 769番を設定し、各 2 つのエントリが同じページテーブルを参照し、そのページテーブルが物理アドレス 0x00000000 番地から 0x00800000 までの 8MB をマッピングするように設定する。
では、実際にページング機能を有効化させる処理 (startup.S) を見ていく。
Makefile でのプログラムの配置によって、スタートアップルーチンが置かれる物理アドレスは 0x90C00 番地になる。そして、second.S によってジャンプするアドレスも 0x90C00 番地になる。しかし startup.S では、リンカスクリプトでプログラムの .text セクションのアドレスを 0xC0000000 番地に指定している。これは、ページング機能を有効にしても、解決される物理アドレスは startup.S のプログラムが存在する場所になる為のものであるが、ページング機能が有効になるまで、このプログラムが参照する場所は嘘のアドレスになる。なので startup.S では、ページテーブルを参照する際、0xC0000000 を引いた値を求めるアドレスとしている (これによって、実際の物理アドレスを参照できる)。
又、ページテーブルの作成処理において登場する stosl という命令により、%edi が指す場所に %eax の値を格納する処理を行っている。そして、cr3 レジスタにページディレクトリを格納し、cr0 の最上位ビットをアサートする。
○ ページフォルトの実装
割り込みベクタ 0x0e のハードウェア割り込み処理を stage3.S で実装。
movl $0xb0000000, %eax
movl $0x10, (%eax)
という処理を実行した時、ページフォルトに登録した割り込みサービスルーチンが実行され、続きの処理が実行される事を確認できた。
又、割り込みサービスルーチンにおいて処理を停止させた時、cr2 レジスタにページフォルトが発生した時に読み書きされたリニアアドレスが入っている事を確認。まともなカーネルでは、ページテーブルを更新し、ここに物理アドレスをマッピングさせ、Present フラグを立てる。
現段階では、テストメッセージを吐いて処理を呼出元に返している。
- Category(s)
- 学習経過
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/ohyama/30ab30fc30cd30eb-305d306e2/tbping