Operating Systems Development Series
Physical Memory
by Mike, 2008, 2009

はじめに

ようこそ!

このチュートリアルでは、コンピュータシステム内の最も重要なリソースの1つである物理メモリを管理する方法を紹介します。物理メモリです。メモリ情報の取得方法、BIOS 割り込み、メモリマネージャの概念、そして完全な物理メモリマネージャの設計と実装について見ていきます。

これは誰もやりたがらないことの一つですが、最終的には作業がずっと楽になります。このことを念頭に置いて、このチュートリアルのリストを見てみましょう。

  • 物理メモリ
  • 翻訳ルックサイドバッファ(TLB)
  • メモリ管理ユニット(MMU)
  • メモリマネージャ
  • メモリ情報の取得
  • ブートローダからカーネルへの情報の受け渡し
  • 物理メモリマネージャの設計と開発
では、アリガトウございます!ここではページングや仮想メモリについては触れないことに注意してください。その代わり、物理メモリ管理と仮想メモリ管理を完全に分けて考えたいと思います。 その理由は単純で、一方に集中すれば他方は不要だからです。心配しないでください。ページングと仮想メモリについては、次のチュートリアルで仮想メモリマネージャの開発について説明します。

メモリより深く見る

メモリ管理に直接飛びつくのではなく、ここでは別のアプローチをとりたいと思います。つまり、メモリが何であるかを理解せずに、メモリ管理が何であるかを理解することはできません。 つまり、管理しようとしているものが何であるかを知る必要がありますよね。

このため、まず物理メモリとは何かについて見ていくことにします。ご存じでしょう...コンピュータの中にある小さなRAMチップのことです:)

さあ、始めましょう...!

物理メモリ

物理メモリです。概要

物理メモリとは、コンピュータのランダムアクセスメモリ(RAM)内に格納される抽象的なメモリブロックのことです。物理メモリがRAM内に「格納」される方法は、システムが使用するRAMの種類によって異なります。例えば、ダイナミック・ランダム・アクセス・メモリ(DRAM)は、データの各ビットを独自のコンデンサに格納し、定期的にリフレッシュする必要があります。コンデンサは、限られた時間だけ電流を蓄える電子デバイスです。このため、電流を蓄える(2進数の1)か、電流を蓄えない(2進数の0)かのどちらかを選択することができます。DRAMチップは、このようにしてコンピュータ内の個々のビットのデータを保存します。

ほとんどの場合、メモリタイプ(RAM、SRAM、DRAMなど)には、プロセッサとシステムバスとのインターフェースとして、特定のタイプのメモリコントローラが必要です。

メモリコントローラは、ソフトウェアを通じてメモリ位置を読み書きする方法を提供します。 メモリコントローラはまた、RAMチップが情報を保持できるように、常にリフレッシュする役割を果たします。

メモリコントローラにはマルチプレクサとデマルチプレクサ回路があり、アドレスバスのアドレスを参照する正確なRAMチップと位置を選択します。これにより、プロセッサはアドレスバスを介してメモリアドレスを送信することで、特定のメモリ位置を参照することができるようになります。

...ここで、プロセッサにどのメモリ・アドレスを読み出すかを指示するソフトウェアの出番となるわけです)

メモリコントローラは、RAMチップ内の位置を順番に選択します。つまり、システム内の総メモリ量よりも大きな物理メモリ位置にアクセスしても、何も起こらないということです。つまり、そのメモリ位置に値を書き込んで、それを読み出すと--データバスに残っているデータを何でも得ることができるのです。

物理アドレス空間には、メモリホールが発生する可能性があります。例えば、スロット1と3にRAMチップがあり、スロット2にはRAMチップがない場合です。これは、スロット1のRAMに格納された最後のバイトと、スロット3の最初のバイト-1の間に、存在しないメモリ領域があることを意味します。これらの場所への読み書きは、メモリを越えて読み書きする場合とほぼ同じ効果があります。この存在しないメモリ位置がメモリコントローラによってリマップされている場合、メモリの別の部分に読み書きを行っている可能性があります。メモリがリマップされていない場合(ほとんどのメモリはそうではありません)、存在しないメモリ位置への読み書きはまったく何も行いません。つまり、存在しないメモリ位置に書き込んでも、どこにも何も書き込まれません。存在しないメモリ位置から読み込むと、データバスに残っていたゴミがすべて読み込まれます。このように、存在しない場所に値を書き込んで読み込んでも、同じ値が得られるわけではないことを知って、ポインタ経由でメモリを手動で解析し、メモリのどの領域が良いのか悪いのかを判断する方法が考え出されました。しかし、この方法は、後で見るように危険です。

さて、以上が物理メモリの実態です。メモリがどのように各ビットを格納するかを知ることで、バイト、ワード、ドワード、クォード、バイトなどがどこに入ってくるかがわかると思います。このうち最も重要なのはバイトで、これはプロセッサがアクセスできる最小のデータだからです。しかし、プロセッサはどのようにしてバイトがメモリ内のどこにあるのかを知るのでしょうか。そこで登場するのが「物理アドレス空間」です。それでは見てみましょう。)

物理アドレス空間(PAS)

これは、物理メモリ(RAM)に格納された 8 ビットのデータ(バイト)を参照するために、プロセッサが使用する(そしてメモリコントローラが変換する)アドレス空間です。メモリアドレスとはメモリコントローラが1バイトのデータに対して選択した番号に過ぎません。例えば、メモリーアドレス0は物理メモリーの最初の8ビット、メモリーアドレス1は次の8ビットといった具合に、それぞれを参照することができます。物理アドレス空間は、これらのメモリアドレスと、メモリアドレスが参照する実際のメモリからなる配列である。

物理アドレス空間は、システム・アドレス・バスを通じて プロセッサからアクセスされます

さて、それでは...プロセッサは、メモリのバイトを参照するためにアドレスを使用することができます。通常、アドレスは0から始まり、メモリ内の各バイトを増分していきます。これはとてもシンプルなことです。しかし、これだけでは、ソフトウェアがどのようにメモリにアクセスするのか、まだ説明できません。確かに、プロセッサはメモリを参照する方法を持っていますが、ソフトウエアはそうではありません。プロセッサはそのニーズに応じて、ソフトウェアにメモリを参照する方法を提供するための特定の方法を提供する必要があります。ちょっと、何?そうです。メモリにアクセスするための、さまざまなアドレス指定方法です。

アドレッシング・モード

アドレスモードは、ソフトウェアが物理アドレス空間にアクセスする方法を管理するためにプロセッサが作った抽象化されたものです。通常、ソフトウェアがプロセッサのレジスタを設定し、プロセッサがメモリを参照する方法を知ることができるようにします。すでに、セグメント:オフセットメモリーアドレッシングと ディスクリプタ:オフセットメモリーアドレッシングの2つを見てきました。

メモリにアクセスする方法をソフトウェアに提供するためにプロセッサが提供するインタフェースです。

第4セグメント:オフセットアドレシングモード、第8章でディスクリプタ:オフセットメモリーアドレッシングモードを取り上げました。

メモリの仕組み詳細

さて、次は新しい方法でメモリを見てみましょう。メモリとは何か、アドレス空間とアドレッシングモードについて、すでに多くのことを学びました。さて、すべてをまとめてみましょうか。

このセクションの情報の多くは必要ではありませんが、完全性を期すために含めることにしました。ここですべてを理解できなくても、あまり気にしないでください。

第7章では、コンピュータシステムとシステムアーキテクチャの基本的な概要について見てきました。 プロセッサのシステムバスと メモリコントローラがどのように接続され、システムが物理RAMを制御する方法を提供するために使用されているのかについて説明しました。このような感じです。

そうなんです。物理 RAM がシステムの他の部分とどのように接続され、通信しているかを示しています。上の画像では、DDR コントローラーがメモリコントローラーです。メモリコントローラーとプロセッサーの間には、Translation Lookaside Buffer (TLB)が配置されています。これによって、システムバスは アドレスバスデータバスコントロールバスを介して、これら3つを接続している。コントロールバスのうち、今重要なのはRWラインと CLKラインの2本だけだ。

TLBはページングが有効なときのみ使用されます。このため、TLBについては後でもう少し詳しく見ていくことにします。

では、物理的なメモリ位置にデータを書き込むと、実際に何が起こるのでしょうか。書き込み操作の間、プロセッサはRWピンをHigh(論理1)に設定します。プロセッサはIO Control ラインを ロー(論理 0)にリセットします。これにより、IOサブシステムはコマンドを無視し(IN/OUTポート命令ではないことを意味する)、むしろメモリコントローラのためのものであることが保証されます。その後、プロセッサは書き込み先のアドレスをアドレスバスに、書き込みデータをデータバスにコピーします。これらのラインはメモリコントローラに間接的に接続されているため、メモリコントローラは書き込み操作であることを認識することができる。そこで、メモリコントローラは、アドレスバス上のメモリアドレスをデマルチプレクサ回路で変換して、使用するRAMチップを探し、リニアオフセットバイトをRAMチップのメモリスペースに書き込むだけでよいのです。次に、メモリコントローラは、データバスからこの位置にデータをコピーし、次のクロック信号でメモリの状態を更新します。

読み出し動作では、書き込み動作とほぼ同じ処理が行われます。ただし、RWラインは読み出し動作を示すためにLowにセットされます。また、メモリコントローラは、メモリアドレスをRAMチップのオフセットに変換した後、その場所に格納されているデータをコピーして、プロセッサのデータバスに配置します。その後、メモリコントローラは次のクロック信号でメモリ状態をリフレッシュします。

CLK信号は、読み出しと書き込みによるアドレスとデータ値の交換を同期させるために使用されます。 メモリチップとの通信は、CLKラインが論理1(ハイ)になると開始されます。 CLKラインがハイに保たれている間、アドレスはアドレス線に置かれ、R/Wラインは書き込みの場合はハイ、読み込みの場合はローとなります。

実行中、プロセッサはメモリコントローラとの読み書きを実行するため、常にクロックラインをハイ/ローに切り替えています。

ページングを無効にしている間、TLB自体はまったく何もしません。メモリの読み書きの際には、TLBは全く使用されないことに注意してください。

物理メモリマネージャ

ご存知のように、メモリの管理は非常に重要です。私たちのデータとコードはすべて同じ物理アドレス空間を共有しています。もし、より多くのデータやプログラムをロードして作業しようとするなら、それを可能にするために、何らかの方法でメモリを管理する方法を見つけなければなりません。

この段階で、カーネルはコンピュータのハードウェアとメモリのすべてを完全に制御することができます。これは素晴らしいことですが、同時に悪いことでもあります。カーネルはメモリのどの領域が現在使われているか、どの領域が空いているかを知る術がないのです。このため、プログラムの破損、データの破損、メモリがどのようにマッピングされているか分からない、トリプルフォルトやその他の例外エラーなど、問題が発生する可能性のないメモリを扱う方法は現実には存在しません。その結果、予期せぬ事態が発生する可能性があります。

このため、物理メモリを効果的に管理することは非常に重要です。もっと詳しく見てみましょう。

メモリの検出

概要

まず、コンピュータシステム内のRAMの量を調べる必要があります。これにはさまざまな方法があります。システムによっては、この方法が有効な場合もあれば、そうでない場合もあります。

メモリ量の取得はシステムに大きく依存する場合があります。具体的には、マザーボードのチップセットに依存します。初期化中にBIOSはメモリコントローラからメモリ情報を取得し、検出されたメモリで動作するようにチップセットを設定します。このため、オペレーティングシステムは、システムBIOSを経由して、この情報を取得する必要があります。でも、保護モードではBIOSを使えないんじゃなかったっけ?その通りです。その代わり、他の方法でこの情報を取得しなければなりません。ブートローダとか?

システム内のメモリ量を取得するためにできる他の方法があることを指摘しなければなりません。例えば、CMOS、PnP、SMBiosなどです。しかし、正しい量を得ることを保証する唯一の方法は、それを設定するデバイスからです。BIOSです。

最後に、すべてのPCは、追加可能なデバイス(メモリマップドハードウェアまたはBIOS ROM)で使用するために、4GB未満のメモリ領域を持つ必要があります。 これを回避する方法については、後で少し見ていきますが、心配しないでください :)

「ローメモリ」とはコンベンショナルメモリと呼ばれる1MB以下のメモリで、1MB以上のメモリはエクステンドメモリと呼ばれます。

このことを念頭に置いて、私たちを助けてくれるいくつかの素晴らしいBios割り込みを見てみましょう...

Biosメモリサイズの取得

以下のルーチンはすべて、このチュートリアルの最後にある、第2ステージのブートローダーのデモにあるmemory.incで見ることができます。

BIOS INT 0x12 - メモリサイズの取得(コンベンショナルメモリ)

戻る

CF = 成功すればクリア
AX = 従来のメモリの KB 数
AH = エラー時の状態 (0x80:無効なコマンド、0x86) サポートされていない機能

これは最も簡単な方法です。この割り込みは、BIOSデータ領域(物理アドレス0x413のワード)にある値を返します。WORDサイズの値を返すので、0xFFFF(10進数で65535)までに制限されます。つまり、64KB以下のメモリしか検出できません。このため、64KB以上のメモリを搭載したシステムでは、正しいサイズが返されません。したがって、私はこの方法を使用しません。

この方法は、完全なメモリサイズを返さないかもしれませんが、すべてではないにしても、ほとんどすべてのPCで動作することが保証されている唯一の方法です。

BiosGetMemorySize: int 0x12 jc .error test ax, ax ; if size=0 je .error cmp ah, 0x86 ;unsupported function je .error cmp ah, 0x80 ;invalid command je .error ret .error: mov ax, -1 ret

BIOS INT 0x15 ファンクション 0x88 - 拡張メモリサイズ取得

戻る

CF = 成功すればクリア
AX = 1MB 物理アドレスから始まる連続した KB 数
AH = エラー時の状態 (0x80:無効なコマンド; 0x86) 未対応の機能

この割り込みは、AXにあるKB拡張メモリの量を返します。16ビットのレジスタを使用するため、64MBまたは0xFFFFF(65535)を返すように制限されています。Windowsのバージョンによっては、この関数の代わりに15MBを返す場合があります。

BiosGetExtendedMemorySize: mov ax, 0x88 int 0x15 jc .error test ax, ax ; if size=0 je .error cmp ah, 0x86 ;unsupported function je .error cmp ah, 0x80 ;invalid command je .error ret .error: mov ax, -1 ret

BIOS INT 0x15関数 0xE881 - 64MB以上のメモリサイズ取得 (32 Bit)

リターン

CF = 成功すればクリア
EAX = 1MBから16MBまでの拡張メモリ(KB単位)
EBX = 16MB以上の拡張メモリ、64KBブロック単位
ECX = 1MBから16MBまでの構成されたメモリ、KB単位
EDX = 16MB以上の構成メモリ、64JBブロック単位

この割り込みは、拡張レジスタ(EAX/EBX/ECX/EDX)を使用する以外は、INT 0x15 Function 0xE801 と全く同じです。

BIOS INT 0x15 Function 0xE801 - Get Memory Size For > 64 MB Configuations(64ビット以上のメモリサイズの取得

戻り値

CF = 成功すればクリア
EAX = 1MB から 16MB までの拡張メモリ、KB 単位
EBX = 16MB以上の拡張メモリ、64KBブロック単位
ECX = 1MBから16MBまでの構成されたメモリ、KB単位
EDX = 16MB以上の構成メモリ、64JBブロック単位

これは、私がよく使う方法です。この割り込みは、Windows NTとLinuxの両方で、INT 0x15 function 0xe820 がサポートされていない場合、メモリサイズを検出するために起動時に使用されます(Get System Memory Map)。これについては後ほど見ていきます。この方法は1994年頃からあるため、古いシステムではサポートされていない可能性があります。

Extended Memory」と「Configured Memory」の値は、ほとんど同じです。BIOSによっては、EAXとEBX、またはECXとEDXのいずれかに結果を格納する場合があります。言い換えれば、あるBIOSはEAXとEBXを使用するが、ECXとEDXはそのままにしておくかもしれない。他のBIOSは全く逆のことをするかもしれません。標準化でヨロシク!:)あ、そうですか・・・すみません ;)

この方法の典型的な使い方は、BIOSを呼び出す前に、まず汎用レジスタをすべてNULLにすることです。こうすることで、BIOSを呼び出した後、レジスタがNULLかどうかをテストし、どのレジスタのペアを使用すればよいかを知ることができます。EAX/EBXまたはECX/EDXです。

;--------------------------------------------- ; Get memory size for >64M configuations ; ret\ ax=KB between 1MB and 16MB ; ret\ bx=number of 64K blocks above 16MB ; ret\ bx=0 and ax= -1 on error ;--------------------------------------------- BiosGetMemorySize64MB: push ecx push edx xor ecx, ecx ;clear all registers. This is needed for testing later xor edx, edx mov ax, 0xe801 int 0x15 jc .error cmp ah, 0x86 ;unsupported function je .error cmp ah, 0x80 ;invalid command je .error jcxz .use_ax ;bios may have stored it in ax,bx or cx,dx. test if cx is 0 mov ax, cx ;its not, so it should contain mem size; store it mov bx, dx .use_ax: pop edx ;mem size is in ax and bx already, return it pop ecx ret .error: mov ax, -1 mov bx, 0 pop edx pop ecx ret
このルーチンが返すものに注目してください。システム内のKBの量を得るためには、いくつかの計算をする必要があります。EBXには、64KBのメモリブロックの数が入っています。これを64倍すれば、EBXの値が16MB以上のKBの量に実質的に変換されます。1メガバイトが1024キロバイトなので、これに1024を足せば、システム全体のキロバイト数になるわけだ。

メモリを手動で調査する

手動でメモリを調べるとは、メモリに直接アクセスしてポインタから手動でメモリを検出することです。 この方法は、すべてのメモリを検出できる可能性がありますが、最も危険な方法でもあります。私たちの知らないところで、メモリの領域を別の用途に使っているデバイスがあるかもしれないことを忘れないでください。また、メモリマップドデバイス、ROM BIOS、その他メモリを使用するデバイスがあるかもしれません。 また、物理アドレス空間内のメモリホールも考慮していません。

メモリを直接調べることは、存在しないメモリに読み書きしても何も起こらないことに由来しています。つまり、存在しない物理メモリアドレスに書き込んでも、エラーは発生しません。しかし、その同じ場所から再び読み出そうとすると、返ってくる値はデータバスに残された、完全にランダムなゴミかもしれない。

このように、メモリを調べるには、1k(またはそれくらい)ごとにループに入ればよいのです。ポインタを使用して、メモリ位置の読み取りと書き込みを行います。ポインタから読み出された値が無効な値を含むまで、ポインタをインクリメントし続ける(したがって、メモリの別の場所から読み出す)。

このメソッドのデモコードを少し作るかもしれませんが、問題が多いので、おそらく使うことはないでしょう。しかし、この方法は最も危険な方法であり、予測できない結果を引き起こす可能性があるため、私はこの方法を含めることにしました。自己責任で使用してください。

メモリーマップの取得

よっしゃー!これで、システム内のメモリ量が判明しました。しかし、このメモリがすべて利用できるわけではありません。

ここで、メモリーマップの出番です。メモリマップは、メモリのどの領域が何に使われるかを定義します。これを使って、どの領域が安全に使えるかを知ることもできます。

BIOS INT 0x15関数 0xE820 - メモリマップの取得

入力

EAX = 0x0000E820
EBX = マップの先頭で開始するための継続値または0
ECX = 結果のバッファサイズ (>= 20バイトでなければならない)
EDX = 0x534D4150h ('SMAP')
ES:DI = 結果を格納するバッファ

戻り値

CF = 成功したらクリア
EAX = 0x534D4150h ('SMAP')
EBX = コピー元の次のエントリのオフセット、または完了した場合は0
ECX = 実際に返される長さ(バイト)
ES:DI = バッファが満たされた
エラーの場合、AHはエラーコードを含む

アドレス範囲記述子

この割り込みが使用するバッファは、以下のフォーマットに従ったディスクリプタの配列として使用します。

struc MemoryMapEntry .baseAddress resq 1 ; base address of address range .length resq 1 ; length of address range in bytes .type resd 1 ; type of address range .acpi_null resd 1 ; reserved endstruc
アドレス空間の種類

この関数で定義されるアドレス範囲の種類を以下に示す。

  • 1: 利用可能なメモリ
  • 2: 予約済み、使用不可。(例:システムROM、メモリマップドデバイス)
  • 3: ACPI Reclaim Memory (ACPIテーブルを読み込んだ後、OSが使用可能なメモリ)
  • 4: ACPI NVS Memory (OSはNVSセッションの間にこのメモリを保存することが必要です)
  • それ以外の値は未定義として扱う必要があります。
メモリマップの取得

この割り込みは少し複雑に見えるかもしれませんが、それほど悪くはありません。

まず、この割り込みが必要とする入力を見てみましょう。もちろん、AXには関数番号(0xe820)を入れています。しかし、BIOSによっては、EAXの上半分を0にする必要があります。このため、ここではAXの代わりにEAXを使用する必要があります。

また、EDXには'SMAP'の値を入れなければならないことに注意してください。BIOSによっては、割り込みを呼び出した後、このレジスタをゴミ箱に捨ててしまうことがあります。

なるほど...この割り込みを実行すると、BIOSはメモリマップの1つのエントリを返します(このエントリにはフォーマットがあります。 上記のAddress Range Descriptorを参照してください)。もし、割り込みをかけた後、EBXが0でなければ、メモリマップにもっと多くのエントリがあることになります。 マップの各エントリに対してループする必要があります。エントリの長さが0であれば、そのエントリには何もないのでスキップし、リストの次のエントリに進み、最後に到達します。

このルーチンは、上で定義した MemoryMapEntry 構造体を使用して、バイオから取得したエントリから情報を取得します。

;--------------------------------------------- ; Get memory map from bios ; /in es:di->destination buffer for entries ; /ret bp=entry count ;--------------------------------------------- BiosGetMemoryMap: pushad xor ebx, ebx xor bp, bp ; number of entries stored here mov edx, 'PAMS' ; 'SMAP' mov eax, 0xe820 mov ecx, 24 ; memory map entry struct is 24 bytes int 0x15 ; get first entry jc .error cmp eax, 'PAMS' ; bios returns SMAP in eax jne .error test ebx, ebx ; if ebx=0 then list is one entry long; bail out je .error jmp .start .next_entry: mov edx, 'PAMS' ; some bios's trash this register mov ecx, 24 ; entry is 24 bytes mov eax, 0xe820 int 0x15 ; get next entry .start: jcxz .skip_entry ; if actual returned bytes is 0, skip entry .notext: mov ecx, [es:di + MemoryMapEntry.length] ; get length (low dword) test ecx, ecx ; if length is 0 skip it jne short .good_entry mov ecx, [es:di + MemoryMapEntry.length + 4]; get length (upper dword) jecxz .skip_entry ; if length is 0 skip it .good_entry: inc bp ; increment entry count add di, 24 ; point di to next entry in buffer .skip_entry: cmp ebx, 0 ; if ebx return is 0, list is done jne .next_entry ; get next entry jmp .done .error: stc .done: popad ret

マルチブート仕様

マルチブート仕様の解説をすぐにするつもりはありません。しかし、ブートローダ内のBIOSから取得した情報をカーネルに渡す方法が必要です。これはどのような方法でも可能です。マルチブート仕様では、標準的なブートタイムの情報構造を定義していますし、私たちがマルチブート仕様を完全にサポートするかどうかは分かりません。

また、他のブートローダ(GRUBなど)を使うことにした場合、そのローダでカーネルを起動させることも可能です。

とにかく、仕様全体がかなり大きいので、メモリ管理に関するチュートリアルでカバーするのは良いアイデアではありません;)。そこで、必要な情報を渡すために使えるよう、十分な内容を説明します。

概要

マルチブート仕様とは、OSカーネルをロードして実行するためのブートローダーの規格を記述するために使用される規格の一覧です。この仕様では、オペレーティングシステムが制御を開始する前にマシンが置かなければならない標準的な状態が記述されているため、複数のオペレーティングシステムを簡単に起動できるようになります。また、ブートローダからカーネルにどのように、どのような情報を渡すかも含まれています。

今すぐマルチブート仕様のすべてをカバーするつもりはありません。しかし、カーネルが実行されたときにマシンの状態がどうなっていなければならないかについては見ていきます。また、ブートローダからカーネルに渡される情報を格納したマルチブート情報構造体についても少し見ていきます。また、この構造体を使ってブートローダのメモリ情報も渡します。

マシンの状態

マルチブート仕様では、32ビットオペレーティングシステムを起動する(つまり、カーネルを実行する)とき、マシンのレジスタを特定の状態に設定しなければならないとしています。具体的にはカーネルを実行するときに、レジスタを以下の値に設定する。
  • EAX - マジックナンバー。0x2BADB002でなければならない。これは、ブートローダがマルチブート標準であることをカーネルに示すものです。
  • EBX -マルチブート情報構造体の物理アドレスが格納されています。
  • CS - 32ビットの読み取り/実行コードセグメントで、オフセットは`0'、リミットは`0xFFFFFF'でなければなりません。正確な値は未定義です。
  • DS,ES,FS,GS,SS - オフセットが `0' で、リミットが `0xFFFFFFFF' の32ビット読み書き可能なデータセグメントである必要があります。正確な値はすべて未定義です。
  • A20ゲートが有効でなければなりません
  • CR0 - Bit 31 (PG) ビットはクリアされなければならず (ページング無効)、Bit 0 (PE) ビットは設定されなければなりません (プロテクトモード有効)。その他のビットは未定義
それ以外のレジスタは未定義です。このほとんどは、既存のブートローダで既に行われています。追加しなければならないのは、EAXレジスタとEBXの2つだけです。

私たちにとって最も重要なものはEBXに格納されています。これは、マルチブート情報構造体の物理アドレスが格納されます。それでは見てみましょう。

マルチブート情報構造体

これはおそらく、マルチブート仕様に含まれる最も重要な構造体の1つです。この構造体の情報は、EBXレジスタからカーネルに渡されます。これにより、ブートローダがカーネルに情報を渡す標準的な方法が実現します。

これはかなり大きな構造体ですが、それほど悪くはありません。これらのメンバーすべてが必要なわけではありません。仕様では、オペレーティングシステムは、構造体のどのメンバーが存在し、何が存在しないかを決定するために、flagsメンバーを使用する必要があると述べています。

struc multiboot_info .flags resd 1 ; required .memoryLo resd 1 ; memory size. Present if flags[0] is set .memoryHi resd 1 .bootDevice resd 1 ; boot device. Present if flags[1] is set .cmdLine resd 1 ; kernel command line. Present if flags[2] is set .mods_count resd 1 ; number of modules loaded along with kernel. present if flags[3] is set .mods_addr resd 1 .syms0 resd 1 ; symbol table info. present if flags[4] or flags[5] is set .syms1 resd 1 .syms2 resd 1 .mmap_length resd 1 ; memory map. Present if flags[6] is set .mmap_addr resd 1 .drives_length resd 1 ; phys address of first drive structure. present if flags[7] is set .drives_addr resd 1 .config_table resd 1 ; ROM configuation table. present if flags[8] is set .bootloader_name resd 1 ; Bootloader name. present if flags[9] is set .apm_table resd 1 ; advanced power management (apm) table. present if flags[10] is set .vbe_control_info resd 1 ; video bios extension (vbe). present if flags[11] is set .vbe_mode_info resd 1 .vbe_mode resw 1 .vbe_interface_seg resw 1 .vbe_interface_off resw 1 .vbe_interface_len resw 1 endstruc
この構造体には多くの情報が含まれています。memLoと memHiは、BIOSから検出したメモリ量です。mmap_lengthと mmap_addrは、BIOSから取得したメモリマップを指します。

これで完了です。これで、カーネルにメモリ情報(と、それ以上)を渡す良い方法ができました。

mov eax, 0x2BADB002 ; multiboot specs say eax should be this mov ebx, 0 mov edx, [ImageSize] push dword boot_info call ebp ; Execute Kernel add esp, 4 cli hlt
...そして、カーネル内部です。
//! kernel entry point is called by boot loader void __cdecl kernel_entry (multiboot_info* bootinfo) { //*snip* }
カーネルの multiboot_info 構造体は上に示したものと同じですが、C言語で書かれています。この設定のおかげで、カーネルがすべきことはbootinfo を通してメモリ情報 (とブートローダから渡された情報) にアクセスするだけになりました。クールでしょう?

これで、Biosからメモリ情報を取得してカーネルに渡したので、カーネルはそれを物理メモリマネージャに使用できるようになりました。そうです、いよいよ物理メモリマネージャの開発です。

物理メモリ管理

私たちはすでに多くのことをカバーしていると思いませんか?BIOS からメモリ情報を取得する方法と、その情報をカーネルに渡すためにマルチブート情報構造体を使う方法について見てきました。 これにより、カーネルはいつでもメモリ情報を取得することができるようになりました。しかし、まだ最も重要なトピックを扱っていません。このメモリを管理することです。もっと詳しく見てみましょう。

メモリ管理概要

さて、メモリを管理する方法が必要なことはわかりました。そのためには、もちろん、メモリがどのように使用されているかを追跡する方法が必要です。しかし、メモリ内のすべてのバイトを管理することは不可能です。メモリが足りなくなることなく、すべてのバイトの情報を記憶することは不可能です。だから、別の方法を考えなければならない。

メモリの残りを管理するデータ構造は、メモリの総容量よりも小さくする必要があります。例えば、バイトの配列を使うことができます。各バイトには、より大きなメモリブロックの情報を格納することができます。これは、メモリ不足にならないように保証する唯一の方法です。

メモリの「ブロック」の大きさは、実現可能で効率的な大きさでなければなりません。この方法で、物理アドレス空間を「ブロック」サイズのチャンクに分割することができます。メモリを割り当てるときは、バイト単位ではなく、ブロック単位で割り当てます。これが、物理メモリマネージャの役割です。

物理メモリ・マネージャの目的は、コンピュータの物理アドレス空間をブロック・サイズのメモリ・チャンクに分割し、それらを割り当てたり解放したりする方法を提供することです。

x86アーキテクチャでは、ページングが有効な場合、各ページは4KBのメモリブロックを表します。このため、シンプルにするために、物理メモリマネージャの各メモリブロックも4KBのサイズにします。

設定する

さて、物理メモリ・マネージャが重要であることはわかった。物理メモリ・マネージャは物理アドレス空間を分割し、どのメモリ・ブロックが使用中か使用可能かを追跡する必要があることも分かっています。しかし、ちょっと待ってください。カーネルはメモリを管理するためのメモリ領域を必要とします。メモリを割り当てる前に、どのようにしてメモリの領域を確保すればよいのでしょうか?

私たちはできません。このため、唯一の方法は、メモリ内の場所へのポインタを使用することです。この場所は、BIOS、Bios Data Area (BDA) やカーネルと同じように、単に予約されたメモリだと考えてください。私たちはこれを予約されたメモリのどこかに貼り付けたいのですが、カーネルの最後ではどうでしょうか?その後、この領域(カーネル自身と一緒に)をデータ構造内で予約されたものとしてマークし、何も触れないようにすることができます。

素晴らしい!これで、メモリ内のある場所へのポインタができたので、メモリ内の各ブロックを追跡するために必要な情報を格納することができます。でも...どうやって?つまり、ポインタしかないのです。このポインタが指すデータは、メモリの領域を有効に利用するために、何らかの利用しやすい構造になっている必要があります。では、物理メモリを管理する構造体を作るにはどうしたらよいのでしょうか?

これには2つの一般的な解決策がある。スタックとビットマップです。

スタックベースのアロケーション

ビットマップアロケーション

これは最も簡単な実装です。物理メモリ・マネージャが知る必要があるのは、メモリ・ブロックが割り当てられたかどうかだけです。割り当て済みであれば、バイナリビット1を使用することができます。割り当てられていない場合は、バイナリビット0を使用します。つまり、メモリ内のすべてのブロックに対して、割り当て済みかどうかを1つのビットで表現するのです。これが、今回使用する方法です。しかし、物理メモリマネージャは、他の方法(スタックベースのアプローチのような)も可能なように設計されています。

ビットマップ方式は、サイズ的に非常に効率的です。各ビットはメモリのブロックを表すので、このビットマップ方式では32ビットが32ブロックに相当します。32ビットは4バイトですから、4バイトのメモリで32ブロックのメモリを監視できることになります。

しかし、この方法では、メモリブロックを確保するたびに、ビットマップから空きブロック(最初のビットが0)を探す必要があるため、少し時間がかかります。

物理メモリマネージャ(PMM)の開発

今度のデモコードでは、物理メモリマネージャー全体はmmngr_phys.hmmngr_phys.cpp にあります。また、ブートローダからカーネルにどのようにメモリ情報が渡されるのか、カーネルがどのように PMM を初期化するのかを見るために、更新された第2ステージのブートローダを勉強するのにも役立つかもしれません。

グローバルと定数

お気づきのように、私は「マジックナンバー」が好きではありません;)このため、これらの数値はすべて読みやすい定数の後ろに隠す傾向があります。
//! 8 blocks per byte #define PMMNGR_BLOCKS_PER_BYTE 8 //! block size (4k) #define PMMNGR_BLOCK_SIZE 4096 //! block alignment #define PMMNGR_BLOCK_ALIGN PMMNGR_BLOCK_SIZE
これらは単にコードの可読性を高めるためです。PMM はメモリブロックと呼ばれる抽象化されたものを作成します。メモリブロックは 4096 バイト(4K)の大きさです。これはページングを有効にしたときのページのサイズでもあり、重要です。

また、すべてを把握するためのグローバルもいくつか定義されています。

//! size of physical memory static uint32_t _mmngr_memory_size=0; //! number of blocks currently in use static uint32_t _mmngr_used_blocks=0; //! maximum number of available memory blocks static uint32_t _mmngr_max_blocks=0; //! memory map bit array. Each bit represents a memory block static uint32_t* _mmngr_memory_map= 0;
これらのうち最も重要なのは_mmngr_memory_mapです。これは、すべての物理メモリを追跡するために使用するビットマップ構造体へのポインタです。_mmngr_max_blocksには、使用可能なメモリーブロックの量が格納されています。これは、物理メモリのサイズ(ブートローダからBIOSから取得)をPMMNGR_BLOCK_SIZEで割ったものです。これは本質的に物理アドレス空間をメモリブロックに分割します(以前からこれを覚えていますか?)_mmngr_used_blocksは現在使用中のブロックの量、_mmngr_memory_sizeは参照用で、物理メモリの量(KB)を含んでいます。

メモリビットマップ

それじゃ!mmngr_memory_mapはuint32_tへのポインタですよね?もちろん、そうです。 むしろ、「一連のビットへのポインタ」と考えるべきでしょう。 各ビットは、そのブロックが未割り当て(使用可能)なら0、予約済み(使用中)なら1です。 この配列のビット数は_mmngr_max_blocksです。つまり、各ビットが1つのメモリブロックを表し、それが4KBの物理メモリとなります。

これで、ビットの設定と解除、そしてビットが設定されているかどうかのテストができるようになりました。では、見てみましょう。

mmap_set () - ビットマップのビットを設定する

私たちがやりたいことは、メモリマップを int の配列ではなく、ビットの配列として考える方法を提供することです。これはそれほど難しいことではありません。

inline void mmap_set (int bit) { _mmngr_memory_map[bit / 32] |= (1 << (bit % 32)); }
ビットは0〜xの値で、xはメモリマップに設定したいビットです。 ビットを32で割ると、そのビットがある_mmngr_memory_mapの整数のインデックスが得られます。

このルーチンを使用するには、設定したいビットを渡して呼び出すだけでよい。mmp_set(62) は、メモリマップのビット配列の62番目のビットを設定します。

mmap_unset () - ビットマップのビットをアンセットする

これは上記のルーチンに非常に似ていますが、代わりにビットをクリアします。

inline void mmap_unset (int bit) { _mmngr_memory_map[bit / 32] &= ~ (1 << (bit % 32)); }

mmap_test () - ビットが設定されているかどうかをテストする。

このルーチンは、ビットが1であれば真を、0であれば偽を返すだけです。これは上記のルーチンと非常に似ていますが、ビットを設定する代わりに、それをマスクとして使用し、その値を返します。

inline bool mmap_test (int bit) { return _mmngr_memory_map[bit / 32] & (1 << (bit % 32)); }
以上です。さて、ビットマップ内のビットをセット、アンセット、テストする方法ができたので、ビットマップ内の空きビットを検索する方法が必要です。これらは、使用可能な空きメモリブロックを見つけるために使用されます。

mmap_first_free () - ビットマップの最初の空きビットのインデックスを返す。

このルーチンは少し複雑です。メモリビットマップ内のビットをセット、クリア、テストする方法があります。 メモリブロックを割り当てたいとします。どうやって空いているメモリブロックを見つけるのでしょうか?ビットマップのおかげで、セットされていないビットを探すためにビットマップを走査するだけでよいのです。これはそれほど複雑ではありません。

int mmap_first_free () { //! find the first free bit for (uint32_t i=0; i< pmmngr_get_block_count() / 32; i++) if (_mmngr_memory_map[i] != 0xffffffff) for (int j=0; j<32; j++) { //! test each bit in the dword int bit = 1 << j; if (! (_mmngr_memory_map[i] & bit) ) return i*4*8+j; } return -1; }
pmmngr_get_block_count()はこのシステムのメモリブロックの最大数を返します (これはビット配列のビット数でもあることを覚えていますか?) これを 32 (1dword あたり 32 ビット) で割ると、このビットマップ内の整数の量が得られます。つまり一番外側のループは、配列の各整数を単純にループしています。

次にドワードがすべてセットされているかどうかをテストします。ビット単位ではなくワード単位でループさせることで、より効率的かつ高速に処理することができます。0xffffffffでないことを確認してテストしています。そうでない場合は、ビットをクリアする必要があります。その後、そのドワードの各ビットを調べて空きビットを見つけ、その物理フレームアドレスを返すだけです。

物理メモリマネージャはこのルーチンの別のバージョンを含んでいます-- mmap_first_free_s()は、特定のサイズのフレームの最初の空き系列のインデックスを返します。これにより、単一のブロックではなく、特定の領域のメモリブロックが解放されていることを確認することができます。このルーチンは少しトリッキーです。もし読者がこのコードを理解できない場合は、このチュートリアルでより詳細に説明したいと思います。

物理メモリの割り当て

これでメモリを管理する方法ができました。ちょっと、何?そうなんです。この方法は、ビットマップの各ビットが4KBの物理メモリを表していることを思い出せばいいのです。もし最初のメモリブロック(最初の4k)を割り当てたいなら、ビット0を設定するだけです。2番目の4kを割り当てたい場合は、ビット1を設定するだけです。 これは、メモリの終わりまで続きます。これにより、4kブロックのメモリで作業するだけでなく、どのメモリが現在使用中か、予約済みか(ビットは1)、使用可能か(ビット0)を知ることができる。これらはすべて、上記の3つのルーチンとビットマップ配列によって提供されています。かっこいいでしょう?

あとは実際のアロケーションとデアロケーションのルーチンが必要です。しかし、その前に、ビットマップ領域をBIOSメモリマップのものに初期化する必要があります。さらにその前に、カーネルが物理メモリマネージャが使用する情報を提供する方法を提供する必要があります。 見てみましょう...

pmmngr_init () - 物理メモリマネージャを初期化する

memSizeは PMM がアクセスできる最大メモリ量です。これは、ブートローダからカーネルに渡されるKB単位のRAMサイズであるべきです。bitmapは、PMMがそのメモリビットマップ構造体に使用する場所です。もう一つ重要なことは、memset()コールを使ってどのようにメモリビットマップのすべてのビットをセットしているかです。これには理由があり、近日中に説明します。

void pmmngr_init (size_t memSize, physical_addr bitmap) { _mmngr_memory_size = memSize; _mmngr_memory_map = (uint32_t*) bitmap; _mmngr_max_blocks = (pmmngr_get_memory_size()*1024) / PMMNGR_BLOCK_SIZE; _mmngr_used_blocks = pmmngr_get_block_count(); //! By default, all of memory is in use memset (_mmngr_memory_map, 0xf, pmmngr_get_block_count() / PMMNGR_BLOCKS_PER_BYTE ); }

pmmngr_init_region () - 使用するメモリ領域を初期化します。

メモリマップを覚えていますか?どのメモリ領域が安全に動作するかは、カーネルだけが知っています。このため、デフォルトでは、すべてのメモリが使用されています。カーネルはカーネルからメモリマップを取得し、このルーチンを使って私たちが使用できるメモリ領域を初期化します。

このルーチンは非常にシンプルです。このルーチンは、どの程度のメモリブロックを設定するかを見つけ出し、メモリビットマップ内の適切なビットをクリアしてループするだけです。これにより、アロケーションルーチンは、メモリのこれらの自由な領域を再び使用することができます。

void pmmngr_init_region (physical_addr base, size_t size) { int align = base / PMMNGR_BLOCK_SIZE; int blocks = size / PMMNGR_BLOCK_SIZE; for (; blocks>0; blocks--) { mmap_unset (align++); _mmngr_used_blocks--; } mmap_set (0); //first block is always set. This insures allocs cant be 0 }
最後にmmap_set()を呼び出していることに注目してください。PMM では、最初のメモリブロックは常にセットされます。 これにより、PMM はアロケーションエラーに対して NULL ポインタを返すことができるようになります。これはまた、割り込みベクターテーブル(IVT)とBiosデータエリア(BDA)を含む、メモリの最初の64KB内で定義されたデータ構造が上書きされたり触れられたりしないことを保証しています。

pmmngr_deinit_region () - 使用するメモリ領域を非初期化する。

このルーチンは上記のルーチンに似ていますが、ビットをクリアするのではなく、ビットをセットします。ビットが2進数の1になるため、ビットが表す4KBのメモリ・ブロックは実質的に予約に設定され、このルーチンが呼ばれたときにメモリのその領域は決して触れないようにします。

void pmmngr_deinit_region (physical_addr base, size_t size) { int align = base / PMMNGR_BLOCK_SIZE; int blocks = size / PMMNGR_BLOCK_SIZE; for (; blocks>0; blocks--) { mmap_set (align++); _mmngr_used_blocks++; } }
うぉーーーーこれで、使用するメモリ領域を初期化および非初期化する方法と、PMM を初期化する方法がわかったので、次はブロックの割り当てと割り当て解除に取りかかることができます!

pmmngr_alloc_block () および pmmngr_alloc_blocks () - 物理メモリブロックを1つ割り当てる

メモリブロックを確保するのは非常に簡単です。物理メモリはすべてすでに存在しており、あとは空きメモリブロックへのポインタを返せばよいのです。mmap_first_free()ルーチンを使ってビットマップを調べれば、空きメモリブロックを見つけることができます。また、mmap_ first_frame()から返された同じフレームを設定するためにmmap_setを呼び出していることに注意してください。 これは、割り当てられたばかりのメモリブロックが今「使用中」であることを示すものです。このルーチンは、割り当てられたばかりの4KBの物理メモリにvoid*を返します。

void* pmmngr_alloc_block () { if (pmmngr_get_free_block_count() <= 0) return 0; //out of memory int frame = mmap_first_free (); if (frame == -1) return 0; //out of memory mmap_set (frame); physical_addr addr = frame * PMMNGR_BLOCK_SIZE; _mmngr_used_blocks++; return (void*)addr; }
PMM はもうひとつの割り当てルーチンであるpmmngr_alloc_blocks() も含んでいます。このルーチンは上記のルーチンとほとんど同じなので、このチュートリアルでは紙面の関係で割愛することにしました。このルーチンは、単一のブロックではなく、連続した量のブロックを割り当てる方法を提供します。

pmmngr_free_block () および pmmngr_free_blocks () - 物理メモリブロックを解放する

さて、これで物理メモリブロックを確保する方法ができました。次に、メモリが不足しないように、これらのメモリブロックを解放する方法が必要です。これはあまりにも簡単です。

void pmmngr_free_block (void* p) { physical_addr addr = (physical_addr)p; int frame = addr / PMMNGR_BLOCK_SIZE; mmap_unset (frame); _mmngr_used_blocks--; }
pmmngr_free_blocks()はほとんど同じように動作しますが、pmmngr_alloc_blocks()と組み合わせて使用し、単一ブロックではなく、ブロックの連続した量を解放するために使用されます。

まとめ

このチュートリアルは、悪くないと思いませんか?

物理メモリとは何か、どのように機能するのか、物理アドレス空間とアドレッシングモードについて理解しました。また、BIOS からメモリ情報を取得し、カーネルに渡す方法と、物理メモリマネージャの開発についても見てき ました。

これで物理メモリブロックの割り当てと解放ができるようになりました。これは素晴らしいことですが、まだいくつかの問題があります。つまり、ファイルやプログラムをロードする場合、物理メモリ・マネージャを使ってファイルやプログラムに十分な大きさのメモリ領域を確保すればいいのです。しかし......もし、十分な大きさの領域がなかったらどうするか?この場合、ロードするプログラムは、カーネルがロードする特定のアドレスにリンクされていなければなりません。

そこで登場するのが、仮想メモリとページングです。次のチュートリアルでは、ページングと仮想メモリについて見ていきます。4GB のアドレス空間全体をどのようにマッピングし、制御するかを学びます。仮想アドレッシングとは何なのか、そしてそれを使って上記の問題を解決する方法と、それ以上のことを見ていきます。それではまた。