MONAにおけるメモリ管理においてIA-32プロセッサが どのようにしてメモリを見ているのか、そして、MONA自身がどの様なメモリの管理の 仕方をしているのかを簡単に説明した。ここではIA-32プロセッサでのメモリの設定 方法とMONA自身のソースコードを挙げることでより深いメモリに関する理解を得ることを 目的にしている。
メモリ管理で初めにやらなければならないことはセグメントの設定です。つまり、 いくつセグメントを定義するのか、それぞれのセグメントはどこからどこまでの 領域を含むのかということを設定しなければなりません。これらの設定はメモリ上の グローバルデスクリプタテーブル(Global Descriptor Table : GDT)と呼ばれる領域で 指定できます。この領域はメモリ上のどこにでもとることが出来るため、セグメントの 設定はおおよそ次のような手順をとります。
セグメントの設定はいつでも出来ますが、CPUの動作モードがリアルモードから プロテクトモードに移行する前には必ず設定しなければなりません。そうでなければ CPUが強制的に再起動をさせられます。そして、グローバルデスクリプタテーブル用の メモリ領域はRAM上のどこにとってもかまいませんが、出来るならば1Mbyte以降のメモリ 領域にとったほうが互換性の面から見ても安全です。最後に、グローバルデスクリプタ テーブルのありかをCPUに教えることですが、これはLGDT命令を用います。この命令は グローバルデスクリプタテーブルの開始アドレスを長さを格納したメモリ領域の 先頭アドレスを指定するものですが、メモリアドレスは全て物理アドレスで指定します。 セグメンテーションもページングも開始していないこの段階では当然のことです。 つぎに、グローバルデスクリプタテーブルについて説明します。
グローバルデスクリプタテーブルはセグメントデスクリプタと呼ばれる64ビット、 8バイトのエントリの配列になっています。このセグメントデスクリプタが一つ一つの セグメントの開始アドレス、長さ、アクセス権限などを定めています。 グローバルデスクリプタテーブルの0番目(一番初め)のエントリは常に無効な エントリとして予約されています
次に重要なフィールドについて簡単に説明します。
セグメントデスクリプタのセグメントタイプフィールドでそのセグメントがどのように 用いられるのかを設定します。このフィールドの意味はセグメントがカーネル・ユーザー プロセスのためのセグメントであるのか、CPUが自分自身の管理のために用いるセグメント かで、意味が大きく変わっています。まず、カーネル・ユーザープロセスのための設定を 説明します。
ビット3(最上位) | 値は1 |
ビット2 | コンフォーミングかそうでないか |
ビット1 | 読み取り可能か |
ビット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ビットのセグメントセレクタを セグメントレジスタにロードする必要があります。このセレクタでどのセグメントを 指定するのかは、グローバルデスクリプタテーブルもしくはローカルデスクリプタテーブル に格納してあるセグメントデスクリプタのインデックスを指定します。セグメント セレクタの構造は次のようになっています。
それぞれのフィールドの意味は次のとおりです。
以上がセグメントセレクタについての説明です。セグメントセレクタを用いることで グローバルデスクリプタテーブルにある任意のデスクリプタをロードすることが出来、 セグメントを用いることが出来ます。たとえば、インデックス1にあるデスクリプタの セグメントを使用したければ0x80(特権レベル0)をセレクタとしてロードすればいいわけです。 なお、0x83とすると特権レベルが3になりユーザーモードでの使用となります。
グローバルデスクリプタテーブルを指定する方法はLGDT命令を使用します。 この命令は48ビットのメモリ領域をオペランドにとり、グローバル デスクリプタテーブルをロードします。命令のオペランドの内容は次のとおりです。
上で示したビット配置にしてそれを命令のオペランドに取るわけです。 初めが16ビットデータなのでC言語のインラインアセンブラを用いて命令を 書くときは#pragmaを使わなければならないでしょう。テーブルリミットは テーブルが存在するオフセットの最大値です。つまり、「テーブルの大きさ−1」が リミットになります。テーブルのベースアドレスは論理アドレスでテーブルの 開始アドレスを指定します。
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; }
この関数の中では次のようなセグメントが定義されています。
コード・データ・スタックのセグメントはどれもベースアドレスが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つに分けます。このように分割した上で変換は次のようになります。
一つ一つの変換表にはそれそれ2^10=1024個のエントリが格納されるので大きさは4Kbyteと なります。さっきの4Mbyteと比べればホコリみないなものです。それから、 初めに参照される変換表をページディレクトリといい、ページディレクトリから参照される 変換表をページテーブルといいます。これらのテーブルはメモリ上のどこにでも存在 出来ますが、必ず4Kbyteのアライメントに沿わなければなりません。つまり、ページ ディレクトリやページテーブルはそれ自身がひとつのページをなしているといえます。 また、アドレス変換の大元となるページディレクトリの先頭アドレスはCR3と呼ばれる コントロールレジスタに保存されています。また、ページングの開始はCR0と呼ばれる コントロールレジスタの最上位ビットを立てることで為されます。
ページディレクトリやページテーブルに格納されている配列の情報の一つ一つを ページエントリといい、32ビットの大きさを持っています。ページエントリはそれぞれ 目的の物理ページのベースアドレスを上位20ビットを保持しています。また、 ページディレクトリのエントリはそれぞれページテーブルを格納している物理ページの ベースアドレスを格納しています。
プロセッサが論理アドレスにアクセスしたとき、そのページから物理アドレスの ページにアクセスできなかったときにページフォルト例外を発します。 ページフォルト例外を発する条件は次のとおりです。
ページフォルトがどのような状態で発生したのかは例外ハンドラに送られる 32ビットのエラーコードに示されています。エラーコードは次のように なってします。
ページフォルトが発生するとプロセスの処理が一時中断し、カーネルに処理が移ります。 カーネルはページフォルトが起きた原因を解析して問題を解決、例えば 論理ページに物理ページを割り当てる、して処理を元のプロセスに戻します。 元のプロセスで再び同じーページにアクセスし、問題が解決していれば 何事もなかったかのように処理を続行します。しかし、問題解決が 不可能とであればプロセスを中断させることになります。
これまで説明したページング機構のことを踏まえうえで、 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日(木)