トップページ

MONAにおけるメモリ管理(その2)

MONAにおけるメモリ管理においてIA-32プロセッサが どのようにしてメモリを見ているのか、そして、MONA自身がどの様なメモリの管理の 仕方をしているのかを簡単に説明した。ここではIA-32プロセッサでのメモリの設定 方法とMONA自身のソースコードを挙げることでより深いメモリに関する理解を得ることを 目的にしている。

Table Of Contents


セグメントの設定

メモリ管理で初めにやらなければならないことはセグメントの設定です。つまり、 いくつセグメントを定義するのか、それぞれのセグメントはどこからどこまでの 領域を含むのかということを設定しなければなりません。これらの設定はメモリ上の グローバルデスクリプタテーブル(Global Descriptor Table : GDT)と呼ばれる領域で 指定できます。この領域はメモリ上のどこにでもとることが出来るため、セグメントの 設定はおおよそ次のような手順をとります。

  1. グローバルデスクリプタテーブル用のメモリ領域を確保する
  2. 先ほど確保した領域にセグメントの設定を書き込む
  3. 1.で確保した領域がグローバルデスクリプタテーブルであることをCPUに知らせる

セグメントの設定はいつでも出来ますが、CPUの動作モードがリアルモードから プロテクトモードに移行する前には必ず設定しなければなりません。そうでなければ CPUが強制的に再起動をさせられます。そして、グローバルデスクリプタテーブル用の メモリ領域はRAM上のどこにとってもかまいませんが、出来るならば1Mbyte以降のメモリ 領域にとったほうが互換性の面から見ても安全です。最後に、グローバルデスクリプタ テーブルのありかをCPUに教えることですが、これはLGDT命令を用います。この命令は グローバルデスクリプタテーブルの開始アドレスを長さを格納したメモリ領域の 先頭アドレスを指定するものですが、メモリアドレスは全て物理アドレスで指定します。 セグメンテーションもページングも開始していないこの段階では当然のことです。 つぎに、グローバルデスクリプタテーブルについて説明します。

グローバルデスクリプタテーブル

グローバルデスクリプタテーブルはセグメントデスクリプタと呼ばれる64ビット、 8バイトのエントリの配列になっています。このセグメントデスクリプタが一つ一つの セグメントの開始アドレス、長さ、アクセス権限などを定めています。 グローバルデスクリプタテーブルの0番目(一番初め)のエントリは常に無効な エントリとして予約されています

0..15 (16bits)
セグメントリミットの0..15
16..31 (16bits)
ベースアドレスの 0..15
32..39 (8bits)
ベースアドレスの16..23
40..43 (4bits)
セグメントタイプ
44..44 (1bit)
デスクリプタタイプ。
0ならシステムデスクリプタ。1ならコードまたはデータ
45..46 (2bits)
デスクリプタの特権レベル
47..47 (1bit)
セグメントが存在するかどうか
48..51 (4bits)
セグメントリミットの16..19
52..52 (1bit)
システムソフトウエアが使用できるビット
53..53 (1bit)
0にしておく
54..54 (1bit)
デフォルトのオペレーション・サイズ
0なら16ビットセグメント、1なら32ビットセグメント
55..55 (1bit)
G、グラニュラリティ
0ならセグメントリミット1つ当たり1バイトの大きさになる。つまり、 0xfffffで1メガバイトの大きさになる。
1ならセグメントリミット1つ当たり4キロバイトの大きさになる。つまり、 0xfffffで4ギガバイトの大きさになる。
56..63 (8bits)
ベースアドレスの24..31

次に重要なフィールドについて簡単に説明します。

ベースアドレス
ペースアドレスはセグメントが始まるアドレスを論理アドレスで指定します。 あと、ベースアドレスは16バイトのアライメントに合わせたほうが処理が速くなります。
セグメントリミット
対象のセグメントの大きさを指定します。具体的にはセグメント内で使用されるオフセットの 値を制限し、オフセットの値がリミット値を超えると一般保護例外を飛ばします。後述の グラニュラリティフィールドの値が0ならばオフセットの下位20ビットの値で制限し、1で あればオフセットの下位12ビットを無視して上位20ビットの値で制限します。 例えばリミットの大きさが0x12345であった場合、グラニュラリティが0であればオフセットの 値は0x00012345までが有効で、0x00012346から後の値は無効になります。グラニュラリティが 1の場合は0x12345FFFまでの値が有効でそこから後、つまり、0x12346000から後の値が 無効になります。
グラニュラリティ
セグメントリミットをオフセットのどの部分で掛けるかを指定します。0ならばオフセットの 下位20ビットの値で制限を設け、1ならばオフセットの下位12ビットは無視して上位20ビットの 値で制限を設けます。つまり、このフィールドの値が0であればセグメント内の最大の オフセットは0x000FFFFFまでであり、1MByteの大きさのセグメントを指定できます。また、 フィールドの値が1であれば最大のオフセットは0xFFFFFFFFであり、4GBybteの大きさの セグメントを指定できます。
デスクリプタの特権レベル
このセグメントがどの特権レベルのためのセグメントであるかを指定します。特権レベルは 4ビットの数字で、数字として値が小さいほどより高い特権レベルを表します。通常 カーネルの特権レベルは0で、ユーザープロセスの特権レベルは3になります。 つまり、カーネルのためのセグメントならば0にしておき、ユーザープロセスのための セグメントであれば3に設定します。
デスクリプタタイプ
ここで定義したデスクリプタがOSやユーザープロセスのためのデスクリプタであるのか、 CPUが動作の管理のために使用するシステムデスクリプタであるのかを指定します。 システムデスクリプタにはローカルデスクリプタテーブル(Local Descriptor Table: LDT) やタスクステートセグメント(Task State Segment : TSS)のための領域のデスクリプタが あります。カーネルやユーザープロセスのためのデータやコードを格納するセグメントを 定義するときはこのフィールドは1に設定し、システムデスクリプタを定義するときは 0に設定します。
デフォルトのオペレーションサイズ
このフィールドの意味はセグメントのタイプによって多少変わってきますが、 重要なことは32ビットというキーワードが出てくれば1にし、16ビットという キーワードが出てくれば0に設定します。例えば、後述するセグメントタイプが コードを格納するセグメントであれば、デフォルトのデータタイプが32ビット長か、 16ビット長かを指定できます。同様にスタック用のセグメントを指定しているときは スタックに格納するデータが32ビット長か、16ビット長かを指定できます。
セグメントタイプ
セグメントタイプでここで定義されているセグメントがどの様な目的のために 使用されるのかを指定します。セグメントには大きく分けてCPUが用いるセグメントと カーネル・ユーザープロセスが用いるセグメントの2つに分けられます。この2つの指定は デスクリプタタイプのフィールドで設定し、細かな設定をこのセグメントタイプフィールドで 行います。詳しい設定は次の「セグメントタイプ」を参照してください。

セグメントタイプ

セグメントデスクリプタのセグメントタイプフィールドでそのセグメントがどのように 用いられるのかを設定します。このフィールドの意味はセグメントがカーネル・ユーザー プロセスのためのセグメントであるのか、CPUが自分自身の管理のために用いるセグメント かで、意味が大きく変わっています。まず、カーネル・ユーザープロセスのための設定を 説明します。

最上位ビットが1である場合
ビット3(最上位) 値は1
ビット2 コンフォーミングかそうでないか
ビット1 読み取り可能か
ビット0(最下位) アクセス済みか
最上位ビットが0である場合
ビット3(最上位) 値は0
ビット2 エキスパンドダウンかそうでないか
ビット1 書き込み可能か
ビット0(最下位) アクセス済みか

タイプを設定するビットフィールドは4ビット長ですが、このうちの最上位ビットは コードを格納しているのか、データを格納しているのかを指定します。コードを格納してる 場合、ビット2はこのコードセグメントがコンフォーミングかそうでないかを指定できます。 非コンフォーミングモードであればコードセグメントの実行はDPLに等しい特権レベルでしか 実行できませんが、コンフォーミングモードであればDPLで示される特権レベル 以下 のプロセスから実行することが出来ます。ビット1はコードセグメントが 読み取り可能かどうかを表しており、ビット0はこのセグメントがアクセスされたかどうかを 示します。

また、データを格納している場合、ビット2はエキスパンドダウンかそうでないかを指定します。 非エキスパンドダウンモードであればオフセットとして有効な範囲は0x00000000から セグメントリミットで示される値までです。しかし、エキスパンドダウンモードであれば、 有効な範囲はセグメントリミットで示される値から0xFFFFFFFFまでです。これは、 セグメントリミットの値を変更することで、セグメントの領域をアドレスの下位のほうに 動的に伸ばすことが出来ます。IA-32のスタックはアドレスの下方に伸びるのでスタックの 大きさを動的に調節することが出来ます。

一方、システムデスクリプタのタイプフィールドは次のような意味を持ちます。 この中でよく使用するものは32ビットTSSぐらいでしょうか。

システムデスクリプタのタイプフィールド
0000 予約済み
0001 16ビットTSS
0010 LDT
0011 16ビットTSS(ビジー)
0100 16ビットコールゲート
0101 タスクゲート
0110 16ビット割り込みゲート
0111 16ビットトラップゲート
1000 予約済み
1001 32ビットTSS
1010 予約済み
1011 32ビットTSS(ビジー)
1100 32ビットコールゲート
1101 予約済み
1110 32ビット割り込みゲート
1111 32ビットトラップゲート

セグメントの指定

セグメントを指定するためにはそのセグメントに対応する16ビットのセグメントセレクタを セグメントレジスタにロードする必要があります。このセレクタでどのセグメントを 指定するのかは、グローバルデスクリプタテーブルもしくはローカルデスクリプタテーブル に格納してあるセグメントデスクリプタのインデックスを指定します。セグメント セレクタの構造は次のようになっています。

0..1 (2bits)
要求特権レベル
2..2 (1bit)
テーブル・インディケーター
3..15(13bits)
インデックス 

それぞれのフィールドの意味は次のとおりです。

要求特権レベル
このセレクタでどのレベルの特権レベルを用いるのかを指定します。この値は 現在の特権レベル(CPL)と変わっていてもいいのですが、話がややこしくなるので、 カーネルモードで動いているときは00とし、ユーザーモードで動いている場合は 11としたほうがいいでしょう。
テーブル・インディケーター
次に述べるインデックスの値がグローバルデスクリプタテーブルのものなのか、 ローカルデスクリプタテーブルのものなのかを指定します。このフィールドの値が 0ならばグローバルデスクリプタテーブルを、1ならばローカルデスクリプタテーブルを 探します。
インデックス
グローバルデスクリプタテーブルもしくはローカルデスクリプタテーブルの なかで目的のセグメントのデスクリプタが保存されているインデックスを指定します。 インデックスの値に8を掛けてデスクリプタテーブルのベースアドレスに加算すれば 目的のセグメントのデスクリプタが設定してあるメモリアドレスがわかる算段です。

以上がセグメントセレクタについての説明です。セグメントセレクタを用いることで グローバルデスクリプタテーブルにある任意のデスクリプタをロードすることが出来、 セグメントを用いることが出来ます。たとえば、インデックス1にあるデスクリプタの セグメントを使用したければ0x80(特権レベル0)をセレクタとしてロードすればいいわけです。 なお、0x83とすると特権レベルが3になりユーザーモードでの使用となります。

グローバルデスクリプタテーブルの設定

グローバルデスクリプタテーブルを指定する方法はLGDT命令を使用します。 この命令は48ビットのメモリ領域をオペランドにとり、グローバル デスクリプタテーブルをロードします。命令のオペランドの内容は次のとおりです。

0..15 (16bits)
テーブルリミット
16..47 (32bits)
テーブルベースアドレス

上で示したビット配置にしてそれを命令のオペランドに取るわけです。 初めが16ビットデータなのでC言語のインラインアセンブラを用いて命令を 書くときは#pragmaを使わなければならないでしょう。テーブルリミットは テーブルが存在するオフセットの最大値です。つまり、「テーブルの大きさ−1」が リミットになります。テーブルのベースアドレスは論理アドレスでテーブルの 開始アドレスを指定します。

MONAでの設定方法

MONAでグローバルデスクリプタテーブルを設定するところは2箇所あります。 初めはsecondboot.asmでリアルモードからプロテクトモードへ移行する直前で、 もうひとつはGDTUtil.cppでカーネル用のメモリ領域を確保した直後で設定しています。 それぞれアセンブリコードとC++コードでどのようにして設定しているのかを見てみます。

まず、secondboot.asmでの設定部分を以下に示します。このコードではプロテクトモードに 移行する直前でグローバルデスクリプタテーブルを設定していることがわかります。 また、それぞれのセグメントデスクリプタはgdt0, gdt8, gdt10, gdt18で指定しています。

;-------------------------------------------------------------------------------
; To Protect mode
;-------------------------------------------------------------------------------
RealToProtect:
;;; Real to Protect
        mov     ax, cs          ; we jump from firstboot
        mov     ds, ax          ; so ds is changed
        lgdt    [gdtr]          ; load gdtr
        cli                     ; disable interrupt
        mov     eax, cr0        ; real
        or      eax, 1          ; to
        mov     cr0, eax        ; protect
        jmp     flush_q1

;-------------------------------------------------------------------------------
; GDT definition: It is temporary.
;-------------------------------------------------------------------------------
gdtr:
        dw gdt_end - gdt0 - 1   ; gdt limit
        dd gdt0 + 0x100 * 16    ; start adderess

gdt0:                           ; segment 00
        dw 0                    ; segment limitL
        dw 0                    ; segment baseL
        db 0                    ; segment baseM
        db 0                    ; segment type
        db 0                    ; segment limitH
        db 0                    ; segment baseH

gdt08:                          ; segment 08(code segment)
        dw 0xffff               ; segment limitL
        dw 0                    ; segment baseL
        db 0                    ; segment baseM
        db 0x9a                 ; Type Code
        db 0xff                 ; segment limitH
        db 0                    ; segment baseH

gdt10:                          ; segment 10(data segment)
        dw 0xffff               ; segment limitL
        dw 0                    ; segment baseL
        db 0                    ; segment baseM
        db 0x92                 ; Type Data
        db 0xff                 ; segment limitH
        db 0                    ; segment baseH

gdt18:                          ; segment 18(stack segment)
        dw 0                    ; segment limitL
        dw 0                    ; segment baseL
        db 0                    ; segment baseM
        db 0x96                 ; Type Stack
        db 0xC0                 ; segment limitH
        db 0                    ; segment baseH

gdt_end:                        ; end of gdt

それぞれのデスクリプタがどのような意味を持っているのかは自分で解析してください。 ただ、これを書いた人が横着なのかどうかはわかりませんが、コードセグメントと データセグメントの53ビット目が1になっています。ここは本来0とすべき ビットなのですが・・・。また、スタックの指定がエキスパンドダウンになっています。 エキスパンドダウンでリミットが0ということは論理アドレス空間全てを含むという意味です。

次にGDTUtil.cppでの設定を見てみます。C++コードで書かれているので 理解できる人も多きのではないでしょうか。デスクリプタの設定はsetSegDesc関数で 行っています。

void GDTUtil::setup() {

    g_gdt = (SegDesc*)malloc(sizeof(SegDesc) * GDT_ENTRY_NUM);
    checkMemoryAllocate(g_gdt, "GDT Memory allocate");

    /* NULL */
    setSegDesc(&g_gdt[0], 0, 0, 0);
    g_gdt[0].limitH = 0;

    /* allcate TSS */
    g_tss = (TSS*)malloc(sizeof(TSS));

    /* SYS CS 0-4GB */
    setSegDesc(&g_gdt[1], 0, 0xFFFFF              , SEGMENT_PRESENT | SEGMENT_DPL0 | 0x10 | 0x0A);

    /* 中略 */

    /* USER SS 0-4GB */
    setSegDesc(&g_gdt[7], 0, 0xFFFFF              , SEGMENT_PRESENT | SEGMENT_DPL3 | 0x10 | 0x02);

    /* lgdt */
    GDTR gdtr;
    gdtr.base  = (dword)g_gdt;
    gdtr.limit = sizeof(SegDesc) * GDT_ENTRY_NUM - 1;
    lgdt(&gdtr);

    /* setup TSS */
    setupTSS(0x20);

    return;
}

この関数の中では次のようなセグメントが定義されています。

  1. カーネル用コードセグメント
  2. カーネル用データセグメント
  3. カーネル用スタックセグメント
  4. タスクステートセグメント
  5. ユーザー用コードセグメント
  6. ユーザー用データセグメント
  7. ユーザー用スタックセグメント

コード・データ・スタックのセグメントはどれもベースアドレスが0でリミットが0xfffff かつ、グラニュラリティが1に設定されているので、論理アドレス空間全てを含みます。 スタックセグメントの設定ですが、この部分ではエキスパンドダウンの設定ではなく 通常の設定になっています。higeponも途中で気が変わったのでしょうか?あと、 TSSデスクリプタの設定でリミットが0x67になっています。これはTSSの大きさが67バイト であることに由来すると思いますが、setSegDesc関数では勝手にグラニュラリティを1に 設定するのでリミット1でも大丈夫だと思います。たぶんね・・・

MONAではセグメンテーションによるメモリ保護きのうは殆ど使っていないので セグメントに関する処理はここぐらいです。あとはタスクの切り替えの部分でしょうか? セグメントに関する説明はこの辺にして次はページングに関して説明します。


ページングの設定

ページングに必要なメモリ領域

ページングとは論理アドレスを物理アドレスに変換する機構です。しかし、 論理アドレス一つ一つに対して物理アドレスを対応させることはメモリサイズから 考えて無理があります。そこで、論理アドレス空間と物理アドレス空間をページと呼ばれる 小さな空間に細かく分割して、ページどうしの対応をとることでアドレス変換を 可能にしています。ひとつのページの大きさはIA-32プロセッサの場合は4Kバイトであり、 論理アドレス空間は4G / 4K = 1M = 2^10個のページに分かれることになります。 このページ一つ一つに対して物理アドレス空間のページの開始アドレスを対応付ける ことが出来れば変換は可能になります。この手の変換で一番早い方法はページに0から 番号をつけ、その番号をインデックスとする配列をつくり、その一つ一つのエントリに 対応する物理アドレスのページの開始アドレスを保存しておけばいいわけです。 ページを指定するためには20ビットの情報が必要ですから、 4バイトあれば十分でしょう。 したがって、論理アドレス全体を物理アドレスに マップするためには4MByteの領域が必要になります。

ページングを行うために必要なメモリは上述の議論で4MByteとなりました。これは ひとつの論理アドレス空間を物理アドレス空間に変換するために必要なメモリです。 しかし、プロセスごとに独立したメモリ空間を設けたければプロセスごとに4MByteの 変換表が必要になります。ところで、今私がこの文書を書いている環境では30個ほどの プロセスが走っています。システムが複雑になるにつれて裏で走るプロセスは多くなり、 まして、マイクロカーネルを実装するMONAでは多くのバックグラウンドプロセスを 必要とします。何が言いたいのかというと、30個ほどのプロセスが入れば変換表の 為のメモリ領域は120MByteになるということです。しかも、それぞれのプロセスが 使用するメモリはそれほど多くないので、変換表の殆どのテーブルはアドレス変換の ために使用されないのです。つまり、変換表の大きさとして4MByteを見積もったことには かなり無駄があるということです。

ページディレクトリ・ページテーブル

変換表が大きくなる原因はプロセスが普段使用しないページも変換しようと するからです。しかし、一般にどのアドレスが使用されるかということは 事前にはわかりません。したがって、変換するページを前もって制限することで 変換表の大きさを小さくすることは出来ません。そこで、多段階の変換が持ち上がって くるわけです。

ページに20ビットの番号がついていたわけですが、これを上位10ビットと下位10ビットの 2つに分けます。このように分割した上で変換は次のようになります。

  1. プロセッサはページ番号の上位10ビットを見ます。そして変換表(その1)から 例の10ビットの値をインデックスとするエントリを見ます。
  2. そこには変換表(その2) のアドレス(正確にはその変換表が格納されているページのページ番号)が書かれており、 その変換表(その2)から今度はページ番号の下位10ビットの値をインデックスとする エントリを得ます。
  3. このエントリには目的の物理アドレスに対応するページ番号が書かれています。

一つ一つの変換表にはそれそれ2^10=1024個のエントリが格納されるので大きさは4Kbyteと なります。さっきの4Mbyteと比べればホコリみないなものです。それから、 初めに参照される変換表をページディレクトリといい、ページディレクトリから参照される 変換表をページテーブルといいます。これらのテーブルはメモリ上のどこにでも存在 出来ますが、必ず4Kbyteのアライメントに沿わなければなりません。つまり、ページ ディレクトリやページテーブルはそれ自身がひとつのページをなしているといえます。 また、アドレス変換の大元となるページディレクトリの先頭アドレスはCR3と呼ばれる コントロールレジスタに保存されています。また、ページングの開始はCR0と呼ばれる コントロールレジスタの最上位ビットを立てることで為されます。

ページエントリ

ページディレクトリやページテーブルに格納されている配列の情報の一つ一つを ページエントリといい、32ビットの大きさを持っています。ページエントリはそれぞれ 目的の物理ページのベースアドレスを上位20ビットを保持しています。また、 ページディレクトリのエントリはそれぞれページテーブルを格納している物理ページの ベースアドレスを格納しています。

0..0 (1bit)
存在フラグ
1..1 (1bit)
読み取り・書き込みフラグ
2..2 (1bit)
ユーザー・スーパバイザフラグ 
3..3 (1bit)
ライトスルーフラグ
4..4 (1bit)
キャッシュディスエーブルフラグ
5..5 (1bit)
アクセス済みフラグ
6..6 (1bit)
ダーティフラグ
7..7 (1bit)
ページサイズフラグ
8..8 (1bit)
グローバルフラグ
9..11 (3bit)
自由に使用可能
12..31 (20bit)
ページのベースアドレス
存在フラグ
存在フラグは論理ページに対応する物理ページが存在すれば1、存在しなければ 0が入ります。このフラグの値を確定するのはOSの仕事でCPUは一切関知しません。 このフラグが0のエントリにアクセスしたときにページフォルトが発生します。
読み取り・書き込みフラグ
このフラグは対応するページが読み込み専用であるとき0になり、書き込み可能で あるとき1になります。
ユーザ・スーパバイザフラグ
このフラグはスーパバイザモードのみアクセスできるとき0になり、ユーザーモードで アクセスできるときに1になります。
ライトスルーフラグ
0のときライトバックキャッシングを行い、1のときライトスルーキャッシングを 行います。
キャッシュディスエーブルフラグ
0のときページテーブルやディレクトリがキャッシュの対象になり、1のとき キャッシュが禁止されます。
アクセス済みフラグ
対象のページにアクセス(読み取りまたは書き込み)するとプロセッサが 自動的にこのフラグを立てます。 このフラグはカーネルが明示的にクリアしない限りプロセッサによってクリア されることはありません。
ダーティフラグ
対象のページに書き込むとプロセッサが自動的にこのフラグを立てます。 このフラグはカーネルが明示的にクリアしない限りプロセッサによってクリア されることはありません。また、ページディレクトリのエントリでこのフラグが 立つことはありません。
ページサイズフラグ
このフラグがクリアされているとページの大きさは4キロバイトとなり、 セットされていると2メガバイトとなります。よほどのことがない限り 0にしておくほうがいいでしょう。
グローバルフラグ
頻繁に使用されるページがTLBからフラッシュされるのを防ぎます。 よほどのことがない限り0にしておくほうがいいでしょう。
自由に使用可能
カーネルが自由に使用できる領域です。よくアクセスされるページに印をつけて スワッピングの対象からはずすなどの利用方法があります。
ページのベースアドレス
ページディレクトリのエントリではページテーブルのベースアドレスが 格納されており、ページテーブルのエントリでは目的の物理ページのベースアドレスが 格納されています。

ページフォルト

プロセッサが論理アドレスにアクセスしたとき、そのページから物理アドレスの ページにアクセスできなかったときにページフォルト例外を発します。 ページフォルト例外を発する条件は次のとおりです。

ページフォルトがどのような状態で発生したのかは例外ハンドラに送られる 32ビットのエラーコードに示されています。エラーコードは次のように なってします。

0..0 (1bit)
0のときページフォルトの原因は存在フラグが立っていなかったことを 表し、1のときページレベルでの保護違反があったことを意味します。 ページレベルでの保護違反には書き込み違反や特権違反などがあります。
1..1 (1bit)
0のときメモリを読み込もうとしてページフォルトが発生したことを表し、 1のときメモリに書き込もうとしてページフォルトが発生したことを表します。
2..2 (1bit)
このビットはページフォルトが起きたときプロセッサがどのような保護モードで 動いていたのかを表します。0のときプロセッサはスーパバイザモード(特権レベル 0, 1, 2)で動いていたことを表し、1のときはユーザーモード(特権レベル3)で 動いていたことを表します。
3..3 (1bit)
このビットがたっていると予約されているビットに1が設定されていることを 表します。
4..31 (28bits)
予約済み

ページフォルトが発生するとプロセスの処理が一時中断し、カーネルに処理が移ります。 カーネルはページフォルトが起きた原因を解析して問題を解決、例えば 論理ページに物理ページを割り当てる、して処理を元のプロセスに戻します。 元のプロセスで再び同じーページにアクセスし、問題が解決していれば 何事もなかったかのように処理を続行します。しかし、問題解決が 不可能とであればプロセスを中断させることになります。

MONAでの設定方法 -PageManager.cpp-

これまで説明したページング機構のことを踏まえうえで、 MONAがどのようにしてページングを行い、管理しているのかを 説明します。

まず、MONAのページングはPageManagerというクラスが担当しています。 このクラスの仕事はプロセスが生成されたときにそのプロセス用の ページディレクトリを割り当てます。プロセスごとに ページディレクトリを割り当てることで、各プロセスが独立した メモリ空間を持つことが出来ます。そして、ページフォルト例外を 初めて受け取るのもこのクラスです。もっとも例外を受けてから このクラスにたどり着くまでには他にも実行するコードがあるのですが、 C++のコードがクラス内で実行するところはPageManagerクラスが 初めてです。このほかにも全ての物理メモリの状況を把握して 必要なときにメモリを割り当てたり解放したりします。

コンストラクタ・ページングのセットアップ

では早速クラスのコンストラクタを見てみます。

PageManager::PageManager(dword totalMemorySize)
{
    dword pageNumber = (totalMemorySize + ARCH_PAGE_SIZE - 1) / ARCH_PAGE_SIZE;

    memoryMap_ = new BitMap(pageNumber);
    checkMemoryAllocate(memoryMap_, "PageManager memoryMap");

    /*
     * page table pool
     *
     * page table should be at 0-8MB. so use kernel memory allocation.
     */
    byte* temp = (byte*)malloc(PAGE_TABLE_POOL_SIZE);
    checkMemoryAllocate(temp, "PageManager page table pool");

    /* 4KB align */
    pageTablePoolAddress_ = (PhysicalAddress)(((int)temp + 4095) & 0xFFFFF000);

    /* page table pool bitmap */
    int poolNum = ((int)temp + PAGE_TABLE_POOL_SIZE - pageTablePoolAddress_) / ARCH_PAGE_SIZE;
    pageTablePool_ = new BitMap(poolNum);
    checkMemoryAllocate(pageTablePool_, "PageManger page table pool bitmap");
}

コンストラクタでは初めに物理メモリ上に存在できるページの数を数えて、 memorMap_というビットマップ管理クラスでそれぞれが使用済みかそうでないかを 判断できるようにしています。次に、ページディレクトリやテーブルを確保する 領域を一括して確保しています。これはページディレクトリやページテーブルは それ自体ページを為しており、必ず4キロバイトのアライメントに沿わなければならない 為です。一つ一つのページをアライメントに沿うように確保するためにはそれぞれ データ用の4キロバイト+アライメント調整用の4キロバイト確保しなければなりません。 しかし、一括して確保する場合一括したデータ量+アライメント調整用の4キロバイト ですむためかなりメモリが節約できます。確保した領域はプールしておいて allocateTable関数で確保できるようになっています。

コンストラクタが呼び出された時点ではページディレクトリ・テーブルの設定は されていません。これらの設定は次のsetup関数が行っています。この関数では おもに次のことを行っています。

初めにカーネルが使用する領域を設定する部分から見ます。

void PageManager::setup(PhysicalAddress vram)
{
    PageEntry* table1 = allocatePageTable();
    PageEntry* table2 = allocatePageTable();
    g_page_directory  = allocatePageTable();

    /* allocate page to physical address 0-4MB */
    for (int i = 0; i < ARCH_PAGE_TABLE_NUM; i++)
    {
        memoryMap_->mark(i);
        setAttribute(&(table1[i]), true, true, true, 4096 * i);
    }

    /* allocate page to physical address 4-8MB */
    for (int i = 0; i < ARCH_PAGE_TABLE_NUM; i++)
    {
        memoryMap_->mark(i + 1024);
        setAttribute(&(table2[i]), true, true, true, 4096 * 1024 + 4096 * i);
    }

    /* Map 0-8MB */
    memset(g_page_directory, 0, sizeof(PageEntry) * ARCH_PAGE_TABLE_NUM);
    setAttribute(&(g_page_directory[0]), true, true, true, (PhysicalAddress)table1);
    setAttribute(&(g_page_directory[1]), true, true, true, (PhysicalAddress)table2);

この関数が設定するカーネル領域は初めの0から8メガバイトの領域でこの領域がそのまま 論理アドレス空間にマップされます。領域の設定はmemoryMap_->mark関数で 物理メモリを使用済みにし、setAttribute関数でテーブルエントリの属性を設定します。 そして、最後にテーブルのアドレスをページディレクトリのエントリに格納します。 カーネルプロセスは全てこの領域が使えるようになっていますが、物理メモリを 確保しているのはこの部分だけです。これ以降で作成されるカーネルプロセスでは ページディレクトリにアドレスを登録しているだけになっています。

つぎに、VRAM領域を設定している部分を見ます。

    /* VRAM */
    vram_ = vram;

    /* find 4KB align */
    vram = ((int)vram + 4096 - 1) & 0xFFFFF000;

    /* max vram size. 1600 * 1200 * 32bpp = 7.3MB */
    int vramSizeByte = (g_vesaDetail->xResolution * g_vesaDetail->yResolution * g_vesaDetail->bitsPerPixel / 8);
    int vramMaxIndex = ((vramSizeByte + 4096 - 1) & 0xFFFFF000) / 4096;

    /* Map VRAM */
    for (int i = 0; i < vramMaxIndex; i++, vram += 4096)
    {
        PageEntry* table;
        dword directoryIndex = getDirectoryIndex(vram);
        dword tableIndex     = getTableIndex(vram);

        if (isPresent(&(g_page_directory[directoryIndex])))
        {
            table = (PageEntry*)(g_page_directory[directoryIndex] & 0xfffff000);
        } else
        {
            table = allocatePageTable();
            memset(table, 0, sizeof(PageEntry) * ARCH_PAGE_TABLE_NUM);
            setAttribute(&(g_page_directory[directoryIndex]), true, true, true, (PhysicalAddress)table);
        }
        setAttribute(&(table[tableIndex]), true, true, true, vram);
    }

VRAM領域もプロセスのアドレス空間から確保しておきます。ただVRAM領域は 物理メモリ上に確保されないのでmemoryMap_などで確保する必要はありません。

最後にページディレクトリの設定とページングの開始部分を見ます。

    setPageDirectory((PhysicalAddress)g_page_directory);

    /*
     * create kernel page directory
     * paddress == laddress
     */
    kernelDirectory_ = createKernelPageDirectory();

    startPaging();
}

setPageDirectory関数およびstartPaging関数はそれぞれインラインアセンブリを 呼び出してページングを開始しています。途中のcreateKernelPageDirectory関数は ここでは大して意味はありません。システムコールのテストで用いられるようです。 話のついでに、setPageDirectory関数およびstartPaging関数の実装を見てみましょう。

void PageManager::setPageDirectory(PhysicalAddress address)
{
    asm volatile("movl %0   , %%eax \n"
                 "movl %%eax, %%cr3 \n" : /* no output */ : "m"(address) : "eax");
}
void PageManager::startPaging()
{
    asm volatile("mov %%cr0      , %%eax \n"
                 "or  $0x80000000, %%eax \n"
                 "mov %%eax      , %%cr0 \n"
                 : /* no output */
                 : /* no input  */ : "eax");
}

setPageDirectory関数でCR3レジスタにページディレクトリのベースポインタを設定し、 startPaging関数でCR0レジスタの最上位ビットをセットしていることがわかります。

ページフォルトハンドラ

ページフォルトハンドラはCPUがページフォルト例外を検知したときに呼び出されます。 ページフォルト例外は14番目の例外となっており、MONAではihandler.asmの arch_cpufaulthandler_eの部分から例外処理が開始します。

arch_cpufaulthandler_e:
        pushAll
        changeData
        call arch_save_thread_registers
        push ebp
        mov  ebp, esp
        sub  esp, 8
        mov  eax, dword[esp + 52] ; error cd
        mov  dword[esp + 4], eax
        mov  eax, cr2             ; page fault address
        mov  dword[esp + 0], eax
        call cpufaultHandler_e
        leave
        popAll
        add esp, 0x04             ; remove error_cd
        iretd

このハンドラからC++コードのcpufaultHandler_e関数が呼び出されます。 関数に引数を渡していて、第1引数はフォルトが発生したアドレスで、これは CR2レジスタから得られます。もうひとつはページフォルトの原因を表すエラーコードで これはスタックのはるか上のほうにプッシュされています。ページフォルトとは 関係ないことですが、例外ハンドラにはエラーコードがプッシュされており、 ハンドラから抜け出すときはこのエラーコードをスタックから削除しなければ ならないことに注意してください。

次に、cpufaultHandler_e関数ですが、これはただ単にg_page_managerという PageManagerクラスのインスタンスのpageFaultHandler関数を呼び出している だけです。この関数でページフォルトに対して何らかの問題解決を 行わなければならず、この関数で問題が解決できなければCPUは異常停止します。

void cpufaultHandler_e(dword address, dword error)
{
    if (!g_page_manager->pageFaultHandler(address, error)) {

        dokodemoView();
        panic("unhandled:fault0E - page fault");
    }
}

次がいよいよPageManagerクラスで定義されているページフォルトハンドラです。 この関数はまずページディレクトリが正しいかどうかをチェックし、ページフォルトが どの領域で起きたのかを確かめて、それぞれの領域にあった処理を行っています。 そして、どの領域で起きたのかがわからなかった場合は明らかなアクセスエラーとして プロセスを強制的に終了させます。いずれの場合も真の値を返して上の cpufaultHandler_e関数のif文の中が実行されないようにしています。

まず初めにページディレクトリが正しいかどうかのチェックです。

bool PageManager::pageFaultHandler(LinearAddress address, dword error)
{
    PageEntry* table;
    dword directoryIndex = getDirectoryIndex(address);
    dword tableIndex     = getTableIndex(address);
    Process* current     = g_currentThread->process;

    dword realcr3;
    asm volatile("mov %%cr3, %%eax  \n"
                 "mov %%eax, %0     \n"
                 : "=m"(realcr3): : "eax");

//    if (realcr3 != (dword)current->getPageDirectory()) {
    if (realcr3 != g_currentThread->archinfo->cr3)
    {
        g_console->printf("PageFault[%s] addr=%x, error=%x\n", current->getName(), address, error);
        g_console->printf("realCR3=%x processCR3=%x\n", realcr3, current->getPageDirectory());
    }

現在のページディレクトリはCR3レジスタに保存されていますが、このアドレスと g_currentThread->archinfo->cr3の値と比較します。もしアドレスが違っていれば この関数に来るこれまでの段階で何か重大なエラーが起きたことを表します。 それでも一応処理を続行します。

次にページフォルトがどの様な領域で起こったのかを確かめ、それぞれにあった処理を させている部分です。

    /* search shared memory segment */
    List<SharedMemorySegment*>* list = current->getSharedList();
    for (int i = 0; i < list->size(); i++)
    {
        SharedMemorySegment* segment = list->get(i);

        if (segment->inRange(address))
        {
            return segment->faultHandler(address, FAULT_NOT_EXIST);
        }
    }

    /* heap */
    HeapSegment* heap = current->getHeapSegment();
    if (heap->inRange(address))
    {
        return heap->faultHandler(address, FAULT_NOT_EXIST);
    }

この部分でSegmentというクラスが出てきます。これはCPUが管理しているセグメントとは まったく別のものなので注意してください。これらのクラスはどこからどこまでの 領域は自分が管理しています、ということを明らかにしておりその中で起きた ページフォルトはそれぞれのSegmentクラスで処理するようになっています。 つまり、SharedMemorySegment内で起きたページフォルトはSharedMemorySegmentクラスの ページフォルトハンドラで処理します。このことについては Loader に詳しく書かれています。そして、 HeapSegment内で起きたページフォルトはHeapSegmentクラスのページフォルトハンドラで 処理します。

最後にどちらのSegment内のページフォルトでなかった場合、これは基本的にアクセス 違反なのでプロセスを終了させます。ソースコードは次のようになっています。

    /* physical page not exist */
    if (error & ARCH_FAULT_NOT_EXIST)
    {
        if (isPresent(&(g_page_directory[directoryIndex])))
        {
            table = (PageEntry*)(g_page_directory[directoryIndex] & 0xfffff000);
        } else
        {
            table = allocatePageTable();
            memset(table, 0, sizeof(PageEntry) * ARCH_PAGE_TABLE_NUM);
            setAttribute(&(g_page_directory[directoryIndex]), true, true, true, (PhysicalAddress)table);
        }

        bool allocateResult = allocatePhysicalPage(&(table[tableIndex]), true, true, true);
        if (allocateResult) flushPageCache();

        return allocateResult;

    /* access falut */
    }
    else
    {
#if 1
        ArchThreadInfo* i = g_currentThread->archinfo;
        logprintf("name=%s\n", g_currentThread->process->getName());
        logprintf("eax=%x ebx=%x ecx=%x edx=%x\n", i->eax, i->ebx, i->ecx, i->edx);
        logprintf("esp=%x ebp=%x esi=%x edi=%x\n", i->esp, i->ebp, i->esi, i->edi);
        logprintf("cs =%x ds =%x ss =%x cr3=%x, %x\n", i->cs , i->ds , i->ss , i->cr3, realcr3);
        logprintf("eflags=%x eip=%x\n", i->eflags, i->eip);
#endif
        g_console->printf("access denied.address = %x Process %s killed", address, current->getName());
        logprintf("access denied.address = %x Process %s killed", address, current->getName());

        ThreadOperation::kill();
        return true;
    }
    return true;
}

まず初めのif文ですが、ARCH_FAULT_NOT_EXISTという定数が0なのでif文の条件が 真になることはありません。したがって、ログにアクセス違反が起きた旨を 出力してスレッドをキルして終了します。


感想

今回の解析はIA-32プロセッサのメモリ管理が大半を占めており、 MONAの実装についてはあまり触れていません。しかも殆どが引用に なっています。 この次に「プロセスから見たメモリ管理」 というのを載せるつもりだったのですが、行数が1000行を超えたので とりあえずここまでの文をひとつの解析結果にすることにしました。 しかし、一つ一つの解析が普段のレポートよりも気合が入るというのは 不思議なものです。さっさとATA/ATAPIの解析をやりたいのですが その前にプロセス・スレッド・割り込み・システムコールをやっつけたいので まだまだ先になりそうです。

平成17年2月17日(木)


参考文献


トップページ
inserted by FC2 system