GitHubで公開されたこの一連の記事。 ご質問や問題がある場合は、対応するチケットをそこで開いてください。 記事のすべてのソースはこのスレッドにあります 。
ページングに関する別の記事?
このサイクルに従うと、1月末に記事「ページメモリ:上級レベル」が表示されます。 しかし、再帰的なページテーブルについては批判を受けました。 したがって、フレームにアクセスするための別のアプローチを使用して、記事を書き直すことにしました。
これが新しいオプションです。 この記事では、再帰的なページテーブルの機能について説明していますが、よりシンプルで強力な実装を使用しています。 以前の記事は削除しませんが、古い記事としてマークし、更新しません。
新しいオプションをお楽しみください!
内容
はじめに
前回の記事から、ページングメモリの原理と
x86_64
4レベルページテーブルの仕組みについて学びました。 また、ローダーがカーネルのページテーブル階層を既に設定しているため、カーネルは仮想アドレスで実行されます。 これにより、物理メモリをランダムに変更するのではなく、メモリへの不正アクセスによりページフォールトが発生するため、セキュリティが向上します。
この記事は、物理メモリに保存されており、カーネルが既に仮想アドレスで実行されているため、カーネルからページテーブルにアクセスできなくなりました。 ここでトピックを継続し、カーネルからページテーブルのフレームにアクセスするためのさまざまなオプションを検討します。 それぞれの長所と短所について説明し、コアに適したオプションを選択します。
ブートローダーのサポートが必要なので、最初に構成します。 次に、仮想アドレスを物理アドレスに変換するために、ページテーブルの階層全体を実行する関数を実装します。 最後に、ページテーブルに新しいマッピングを作成する方法と、未使用のメモリフレームを見つけて新しいテーブルを作成する方法を学習します。
依存関係の更新
この記事では、依存関係に
bootloader
バージョン0.4.0以降および
x86_64
バージョン0.5.2以降を登録する必要があります。
Cargo.toml
の依存関係を更新できます。
[dependencies] bootloader = "0.4.0" x86_64 = "0.5.2"
これらのバージョンの変更については、ブートローダーログとx86_64ログを参照してください。
ページテーブルへのアクセス
カーネルからページテーブルにアクセスすることは、見かけほど簡単ではありません。 問題を理解するには、前の記事の4レベルのテーブル階層をもう一度見てください。
重要なことは、各ページエントリが次のテーブルの物理アドレスを格納することです。 これにより、これらのアドレスの変換が回避されるため、パフォーマンスが低下し、無限ループが発生しやすくなります。
問題は、仮想アドレスでも機能するため、カーネルから物理アドレスに直接アクセスできないことです。 たとえば、アドレス
4 KiB
にアクセスすると、4番目のレベルのページのテーブルが格納されている物理アドレスではなく、 仮想アドレス
4 KiB
にアクセスできます。
4 KiB
物理アドレスにアクセスする場合は、それに変換される仮想アドレスを使用する必要があります。
したがって、ページテーブルのフレームにアクセスするには、これらのフレームにいくつかの仮想ページをマップする必要があります。 このようなマッピングを作成する方法はいくつかあります。
アイデンティティーマッピング
簡単な解決策は、 すべてのページテーブルを同一に表示することです 。
この例では、同じフレーム表示が見られます。 ページテーブルの物理アドレスは、同時に有効な仮想アドレスなので、レジスタCR3から開始して、すべてのレベルのページテーブルに簡単にアクセスできます。
ただし、この方法では仮想アドレス空間が乱雑になり、空きメモリの大きな連続した領域を見つけることが難しくなります。 たとえば、 メモリ内のファイルを表示するために、上の図で1000 KiBの仮想メモリ領域を作成するとします 。
28 KiB
リージョンから開始することはできません
28 KiB
すでに占有されているページに
1004 KiB
です。 したがって、たとえば
1008 KiB
などの適切な大きなフラグメントが見つかるまで、さらに検索する必要があります。 セグメント化されたメモリと同じフラグメンテーションの問題があります。
さらに、対応するページがまだ使用されていない物理フレームを見つける必要があるため、新しいページテーブルの作成ははるかに複雑です。 たとえば、ファイルの場合、アドレス
1008 KiB
から始まる1000 KiBの仮想メモリの領域を予約しました。 同じように表示できないため、物理アドレスが
1000 KiB
フレームは使用できなくなりました。
固定オフセットマップ
仮想アドレス空間が乱雑になるのを避けるために、ページテーブルを別のメモリ領域に表示できます。 したがって、同じようにマッピングする代わりに、仮想アドレス空間に固定オフセットでフレームをマッピングします。 たとえば、オフセットは10 TiBです。
この範囲の仮想メモリをページテーブルの表示専用に割り当てることにより、同一の表示の問題を回避できます。 仮想アドレス空間のこのような大きな領域の予約は、仮想アドレス空間が物理メモリのサイズよりもはるかに大きい場合にのみ可能です。
x86_64
、48ビットアドレス空間が256 TiBであるため
x86_64
これは問題ではありません。
ただし、この方法には、各ページテーブルを作成するときに新しいマッピングを作成する必要があるという欠点があります。 さらに、他のアドレス空間のテーブルへのアクセスを許可しません。これは、新しいプロセスを作成するときに役立ちます。
完全な物理メモリマッピング
これらの問題は、ページテーブルフレームだけでなく、 すべての物理メモリを表示することで解決できます。
このアプローチにより、カーネルは、他のアドレススペースのページテーブルフレームを含む任意の物理メモリにアクセスできます。 仮想メモリの範囲は以前と同じサイズで予約されていますが、一致しないページは残っていません。
このアプローチの欠点は、物理メモリを表示するために追加のページテーブルが必要になることです。 これらのページテーブルはどこかに格納する必要があるため、物理メモリの一部を使用します。これは、少量のRAMを搭載したデバイスでは問題になる可能性があります。
ただし、x86_64では、デフォルトサイズの4 KiBの代わりに、 巨大な 2 MiB ページを使用して表示できます。 したがって、32 GiBの物理メモリを表示するには、ページごとに132 KiBのみが必要です。1つの第3レベルテーブルと32の第2レベルテーブルのみです。 巨大なページは、動的変換バッファ(TLB)で使用するエントリが少ないため、より効率的にキャッシュされます。
一時的な表示
物理メモリが非常に少ないデバイスの場合、 ページテーブルにアクセスする必要がある場合にのみ一時的にページテーブルを表示できます。 一時的な比較の場合、最初のレベルのテーブルのみの同一の表示が必要です。
この図では、レベル1テーブルが仮想アドレス空間の最初の2 MiBを管理します。 これは、レベル4、3、および2のテーブルのヌルエントリを介してCR3レジスタからアクセスが実行されるために可能です。インデックス
8
のレコードは、
32 KiB
仮想ページを
32 KiB
の物理フレームに変換し、それによってレベル1テーブル自体を識別します。これは、図の水平矢印で示されています。
レベル1の同一にマッピングされたテーブルに書き込むことにより、カーネルは最大511個の時間比較(512からIDマッピングに必要なレコードを減算)を作成できます。 上記の例では、カーネルは2つの時間比較を作成します。
- レベル1のテーブルのヌルレコードを
24 KiB
フレームと一致させます。 これにより、0 KiB
仮想ページから、点線の矢印で示されているページレベル2テーブルの物理フレームへの一時的なマッピングが作成されます。 - レベル1のテーブルの9番目のレコードを
4 KiB
フレームと一致させます。 これにより、36 KiB
仮想ページから、点線の矢印で示されているページレベル4テーブルの物理フレームへの一時的なマッピングが作成されます。
これで、カーネルは
0 KiB
で始まるページに書き込むことでレベル2のテーブルにアクセスでき、
33 KiB
で始まるページに書き込むことでレベル4のテーブルにアクセスできます。
したがって、一時マッピングを使用したページテーブルの任意のフレームへのアクセスは、次のアクションで構成されます。
- 同じように表示されるレベル1の表で無料のエントリを見つけます。
- このエントリを、アクセスするページテーブルの物理フレームにマップします。
- エントリに関連付けられた仮想ページを介してこのフレームにアクセスします。
- レコードを未使用に戻し、一時マッピングを削除します。
このアプローチでは、同じ512の仮想ページが常に使用されるため、仮想アドレス空間はクリーンなままです。 不利な点は、特に新しい比較ではテーブルのいくつかのレベルを変更する必要があるため、多少の煩雑さです。つまり、説明したプロセスを数回繰り返す必要があります。
再帰ページテーブル
追加のページテーブルをまったく必要としない別の興味深いアプローチは、 再帰的なマッチングです。
考え方は、第4レベルのテーブルからのレコードをそれ自体に変換することです。 したがって、仮想アドレス空間の一部を実際に予約し、現在および将来のすべてのテーブルフレームをこの空間にマッピングします。
これがどのように機能するかを理解するために例を見てみましょう:

記事の冒頭の例との唯一の違いは、このテーブル自体にある物理フレーム
4 KiB
にマッピングされる、レベル4テーブルのインデックス
511
を持つ追加のレコードです。
CPUがこのレコードを処理するとき、CPUはレベル3のテーブルを参照せず、レベル4のテーブルを再び参照しますこれは、自分自身を呼び出す再帰関数に似ています。 プロセッサは、レベル4テーブルの各レコードがレベル3テーブルを指していると想定することが重要であるため、レベル4テーブルをレベル3テーブルとして扱います。これは、x86_64のすべてのレベルのテーブルが同じ構造を持つためです。
実際の変換を開始する前に再帰レコードを1回以上追跡することにより、プロセッサが通過するレベルの数を効果的に減らすことができます。 たとえば、再帰レコードを1回たどってレベル3のテーブルに移動すると、プロセッサはレベル3のテーブルがレベル2のテーブルであると判断します。物理メモリ内のフレーム。 これは、プロセッサがマップフレームであると認識しているため、レベル1ページテーブルの読み取りと書き込みができるようになったことを意味します。 次の図は、このような翻訳の5つのステップを示しています。

同様に、変換を開始する前に再帰的なエントリを2回たどって、完了したレベルの数を2つに減らすことができます。

この手順をステップごとに見ていきましょう。 まず、CPUはレベル4のテーブルの再帰的エントリに従い、それがレベル3のテーブルに到達したと考え、次に再帰的レコードを追跡し、レベル2に到達したと考えます。しかし、実際にはレベル4のままです。レベル3のテーブルになりますが、すでにレベル1のテーブルにあると考えます。最後に、レベル2のテーブルの次のエントリポイントで、プロセッサは物理メモリフレームにアクセスしたと判断します。 これにより、レベル2のテーブルを読み書きできます。
レベル3および4のテーブルにもアクセスします。レベル3のテーブルにアクセスするには、再帰的なエントリを3回実行します。プロセッサは、レベル1のテーブルに既にあると判断し、次のステップでCPUがマッピングフレームと見なすレベル3に到達します。 レベル4のテーブル自体にアクセスするには、プロセッサがレベル4のテーブル自体をマッピングされたフレームとして処理するまで、再帰レコードを4回たどるだけです(下図の青色)。

概念は最初は理解するのが難しいですが、実際にはかなりうまく機能します。
アドレス計算
したがって、再帰レコードを1回以上追跡することで、すべてのレベルのテーブルにアクセスできます。 4レベルのテーブルのインデックスは仮想アドレスから直接派生するため、このメソッドには特別な仮想アドレスを作成する必要があります。 覚えているように、ページテーブルインデックスは次のようにアドレスから抽出されます。
特定のページを表示するレベル1のテーブルにアクセスするとします。 上記で学んだように、再帰的なレコードを1回通過してから、4番目、3番目、2番目のレベルのインデックスを調べる必要があります。 これを行うには、すべてのアドレスブロックを1ブロック右に移動し、再帰レコードのインデックスをレベル4の初期インデックスの場所に設定します。
このページのレベル2のテーブルにアクセスするには、すべてのインデックスブロックを2ブロック右に移動し、再帰インデックスを両方のソースブロックの場所(レベル4およびレベル3)に設定します。
レベル3のテーブルにアクセスするには、同じことを行います。すでに3つのアドレスブロックを右にシフトします。
最後に、レベル4のテーブルにアクセスするために、すべてを4ブロック右にシフトします。
これで、4つのレベルすべてのページテーブルの仮想アドレスを計算できます。 インデックスにページテーブルエントリのサイズである8を掛けることで、特定のページテーブルエントリを正確に指すアドレスを計算することもできます。
次の表は、さまざまなタイプのフレームにアクセスするためのアドレスの構造を示しています。
の仮想アドレス | アドレス構造( 8進数 ) |
---|---|
ページ | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
|
レベル1テーブルのエントリ | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
|
レベル2テーブルのエントリ | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
|
レベル3テーブルのエントリ | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
|
レベル4テーブルのエントリ | 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
|
ここで、
はレベル4インデックス、
はレベル3、
はレベル2、
DDD
は表示されたフレームのレベル1インデックス、
EEEE
はそのオフセットです。
RRR
は、再帰レコードのインデックスです。 インデックス(3桁)は、8(ページテーブルエントリのサイズ)を乗算してオフセット(4桁)に変換されます。 このオフセットにより、結果のアドレスは対応するページテーブルエントリを直接ポイントします。
SSSS
は、符号付き数字の拡張ビットです。つまり、それらはすべてビット47のコピーです。これは、 前の記事で説明したx86_64アーキテクチャの有効なアドレスに対する特別な要件です。
アドレスは8進数です 。各8進数の文字は3ビットを表すため、異なるレベルでテーブルの9ビットのインデックスを明確に分離できます。 これは、各文字が4ビットを表す16進システムでは不可能です。
錆コード
ビット単位演算を使用して、Rustコードでそのようなアドレスを構築できます。
// the virtual address whose corresponding page tables you want to access let addr: usize = […]; let r = 0o777; // recursive index let sign = 0o177777 << 48; // sign extension // retrieve the page table indices of the address that we want to translate let l4_idx = (addr >> 39) & 0o777; // level 4 index let l3_idx = (addr >> 30) & 0o777; // level 3 index let l2_idx = (addr >> 21) & 0o777; // level 2 index let l1_idx = (addr >> 12) & 0o777; // level 1 index let page_offset = addr & 0o7777; // calculate the table addresses let level_4_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (r << 12); let level_3_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (l4_idx << 12); let level_2_table_addr = sign | (r << 39) | (r << 30) | (l4_idx << 21) | (l3_idx << 12); let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12);
このコードは、インデックス
0o777
(511)を持つレベル4の最後のレコードの再帰マッピングが再帰的に一致することを
0o777
としています。 現在、これは当てはまらないため、コードはまだ機能しません。 ローダーに再帰的マッピングを設定するよう指示する方法については、以下を参照してください。
ビット単位の操作を手動で実行する代わりに、
x86_64
クレートの
RecursivePageTable
タイプを使用できます。これは、さまざまなテーブル操作の安全な抽象化を提供します。 たとえば、次のコードは、仮想アドレスを対応する物理アドレスに変換する方法を示しています。
// in src/memory.rs use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; use x86_64::{VirtAddr, PhysAddr}; /// Creates a RecursivePageTable instance from the level 4 address. let level_4_table_addr = […]; let level_4_table_ptr = level_4_table_addr as *mut PageTable; let recursive_page_table = unsafe { let level_4_table = &mut *level_4_table_ptr; RecursivePageTable::new(level_4_table).unwrap(); } /// Retrieve the physical address for the given virtual address let addr: u64 = […] let addr = VirtAddr::new(addr); let page: Page = Page::containing_address(addr); // perform the translation let frame = recursive_page_table.translate_page(page); frame.map(|frame| frame.start_address() + u64::from(addr.page_offset()))
繰り返しますが、このコードには正しい再帰マッピングが必要です。 このマッピングでは、最初のコード例のように、欠落している
level_4_table_addr
計算されます。
再帰的マッピングは、単一のテーブルを介した強力なマッチングを示す興味深い方法です。 実装は比較的簡単で、最小限のセットアップ(1つの再帰的エントリのみ)で済むため、これは最初の実験に適しています。
ただし、次のような欠点があります。
- 大量の仮想メモリ(512 GiB)。 これは、大きな48ビットアドレス空間では問題になりませんが、最適でないキャッシュ動作につながる可能性があります。
- 現在アクティブなアドレス空間へのアクセスのみを簡単に提供します。 他のアドレススペースへのアクセスは、再帰エントリを変更することで引き続き可能ですが、切り替えには一時的な一致が必要です。 これを行う方法については、以前の(廃止された)記事で説明しました。
- x86ページテーブル形式に大きく依存し、他のアーキテクチャでは動作しない場合があります。
ブートローダーのサポート
上記のすべてのアプローチでは、ページテーブルと対応する設定を変更する必要があります。 たとえば、物理メモリを同じようにマッピングするか、第4レベルのテーブルのレコードを再帰的にマッピングします。 問題は、ページテーブルにアクセスせずにこれらの設定を行うことができないことです。
だから、ブートローダーの助けが必要です。 彼はページテーブルにアクセスできるため、必要なディスプレイを作成できます。 現在の実装では、
bootloader
クレートは、 カーゴ関数を使用した上記の2つのアプローチをサポートしています 。
-
map_physical_memory
関数は、仮想アドレス空間のどこかに完全な物理メモリをマップします。 したがって、カーネルはすべての物理メモリにアクセスし、物理メモリ全体を表示するアプローチを適用できます。
-
recursive_page_table
関数を使用して、ローダーは第4レベルのページテーブルエントリを再帰的に表示します。 これにより、カーネルは「再帰ページテーブル」セクションで説明されている方法に従って動作できます。
カーネルでは、シンプルでプラットフォームに依存しない、より強力なアプローチであるため、最初のオプションを選択します(ページテーブルだけでなく、他のフレームにもアクセスできます)。ブートローダーからのサポートのために、依存関係に関数を追加します
map_physical_memory
:
[dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]}
この機能が有効な場合、ブートローダーは物理メモリ全体を未使用の仮想アドレスの範囲にマップします。ある範囲の仮想アドレスをカーネルに渡すために、ブートローダーはブート情報の構造を渡します。
ブート情報
クレート
bootloader
は、カーネルに渡されるすべての情報とともにBootInfoの構造を定義します。構造はまだ確定中であるため、semverと互換性のない将来のバージョンにアップグレードする際にエラーが発生する可能性があります。現在、構造には2つのフィールドがあります:
memory_map
および
physical_memory_offset
:
- このフィールドに
memory_map
は、使用可能な物理メモリの概要が表示されます。システムで使用可能な物理メモリの量と、VGAなどのデバイス用に予約されているメモリの領域をカーネルに通知します。メモリカードは、BIOSまたはUEFIファームウェアから要求できますが、起動プロセスの最初の段階でのみ要求できます。このため、カーネルはこの情報を受信できなくなるため、ローダーが提供する必要があります。メモリカードは、この記事の後半で役立ちます。
-
physical_memory_offset
物理メモリマッピングの仮想開始アドレスを報告します。このオフセットを物理アドレスに追加すると、対応する仮想アドレスが取得されます。これにより、カーネルから任意の物理メモリにアクセスできます。
ローダーは、構造体
BootInfo
を
&'static BootInfo
関数の引数としてカーネルに渡します
_start
。追加してください:
// in src/main.rs use bootloader::BootInfo; #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument […] }
コンパイラはエントリポイント関数の正しいシグネチャタイプを知らないため、正しい引数タイプを指定することが重要です。
エントリポイントマクロ
関数
_start
はブートローダーから外部で呼び出されるため、関数の署名は確認されません。これは、コンパイルエラーなしで任意の引数を受け入れることを許可できることを意味しますが、これはクラッシュにつながるか、未定義のランタイム動作を引き起こします。
エントリポイント関数が常に正しい署名を持つようにするために、クレート
bootloader
はマクロを提供します
entry_point
。このマクロを使用して関数を書き換えます:
// in src/main.rs use bootloader::{BootInfo, entry_point}; entry_point!(kernel_main); #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] }
エントリ・ポイントを使用する必要がなくなった
extern "C"
か、
no_mangle
私たちのためにマクロ定義本当の下位レベルのエントリポイントとして
_start
。関数は
kernel_main
完全に通常のRust関数になったため、任意の名前を選択できます。重要なことは、型によってチェックされることです。そのため、たとえば引数を追加したり、型を変更したりして間違った署名を使用すると、コンパイルエラーが発生します。
実装
これで物理メモリにアクセスでき、最終的にシステムの実装を開始できます。まず、カーネルが実行されている現在のアクティブページテーブルを検討します。2番目のステップでは、この仮想アドレスがマッピングされている物理アドレスを返す変換関数を作成します。最後のステップでは、ページテーブルを変更して新しいマッピングを作成します。
最初に、コードに新しいモジュールを作成します
memory
。
// in src/lib.rs pub mod memory;
モジュールの場合、空のファイルを作成します
src/memory.rs
。
ページテーブルへのアクセス
前の記事の最後で、カーネルが動作するページのテーブルを調べようとしましたが、registerが指す物理フレームにアクセスできませんでした
CR3
。この場所から作業を続けることができます。関数
active_level_4_table
は、4番目のレベルのページのアクティブなテーブルへのリンクを返します。
// in src/memory.rs use x86_64::structures::paging::PageTable; /// Returns a mutable reference to the active level 4 table. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable { use x86_64::{registers::control::Cr3, VirtAddr}; let (level_4_table_frame, _) = Cr3::read(); let phys = level_4_table_frame.start_address(); let virt = VirtAddr::new(phys.as_u64() + physical_memory_offset); let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); &mut *page_table_ptr // unsafe }
最初に、レジスタから4番目のレベルのアクティブテーブルの物理フレームを読み取ります
CR3
。次に、物理的な開始アドレスを取得し、を追加して仮想アドレスに変換します
physical_memory_offset
。最後に、
*mut PageTable
メソッドによってアドレスを生のポインターに変換し、
as_mut_ptr
そこから安全にリンクを作成します
&mut PageTable
。
&mut
代わり
&
にリンクを作成します。記事の後半でこれらのページテーブルを変更するためです。
Rustはボディ全体
unsafe fn
を1つの大きな安全でないブロックと見なしているため、安全でないブロックをここに挿入する必要はありません。これにより、前の行で誤って安全でない操作が導入される可能性があるため、リスクが高まります。また、安全でない操作の検出が困難になります。Rustのこの動作を変更するために、RFCがすでに作成されています。
これで、この関数を使用して、第4レベルのテーブルのレコードを出力できます。
// in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::active_level_4_table; let l4_table = unsafe { active_level_4_table(boot_info.physical_memory_offset) }; for (i, entry) in l4_table.iter().enumerate() { if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); } } println!("It did not crash!"); blog_os::hlt_loop(); }
physical_memory_offset
構造体の対応するフィールドを渡し
BootInfo
ます。次に、関数を使用
iter
してページテーブルエントリとコンビネータ
enumerate
を反復処理し、
i
各要素にインデックスを追加します。 512エントリすべてが画面に収まらないため、空でないエントリのみが表示されます。
コードを実行すると、次の結果

が表示されます。さまざまな第3レベルのテーブルにマップされる空でないレコードがいくつか表示されます。カーネルコード、カーネルスタック、物理メモリの変換、およびブート情報に別々の領域が必要なため、メモリの多くの領域が使用されます。
ページテーブルを調べて第3レベルのテーブルを見るには、表示されたフレームを仮想アドレスに再度変換します。
// in the for loop in src/main.rs use x86_64::{structures::paging::PageTable, VirtAddr}; if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); // get the physical address from the entry and convert it let phys = entry.frame().unwrap().start_address(); let virt = phys.as_u64() + boot_info.physical_memory_offset; let ptr = VirtAddr::new(virt).as_mut_ptr(); let l3_table: &PageTable = unsafe { &*ptr }; // print non-empty entries of the level 3 table for (i, entry) in l3_table.iter().enumerate() { if !entry.is_unused() { println!(" L3 Entry {}: {:?}", i, entry); } } }
2番目と1番目のレベルのテーブルを表示するには、3番目と2番目のレベルのレコードに対して、このプロセスをそれぞれ繰り返します。ご想像のとおり、コードの量は非常に急速に増加しているため、完全なリストは公開しません。
プロセッサがアドレスを変換する方法を理解するのに役立つため、テーブルを手動で移動することは興味深いです。ただし、通常は特定の仮想アドレスに対して1つの物理アドレスのみを表示することに関心があるため、このための関数を作成しましょう。
アドレス変換
仮想アドレスを物理アドレスに変換するには、マッピングされたフレームに到達するまで4レベルのページテーブルを調べる必要があります。このアドレス変換を実行する関数を作成しましょう:
// in src/memory.rs use x86_64::{PhysAddr, VirtAddr}; /// Translates the given virtual address to the mapped physical address, or /// `None` if the address is not mapped. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { translate_addr_inner(addr, physical_memory_offset) }
安全
translate_addr_inner
でないコードの量を制限する安全な関数を参照します。上記のように、Rustは身体全体を
unsafe fn
大きな安全でないブロックと見なしています。 1つの安全な関数を呼び出すことにより、各操作を再び明示的にし
unsafe
ます。
特別な内部関数には実際の機能があります。
// in src/memory.rs /// Private function that is called by `translate_addr`. /// /// This function is safe to limit the scope of `unsafe` because Rust treats /// the whole body of unsafe functions as an unsafe block. This function must /// only be reachable through `unsafe fn` from outside of this module. fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { use x86_64::structures::paging::page_table::FrameError; use x86_64::registers::control::Cr3; // read the active level 4 frame from the CR3 register let (level_4_table_frame, _) = Cr3::read(); let table_indexes = [ addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index() ]; let mut frame = level_4_table_frame; // traverse the multi-level page table for &index in &table_indexes { // convert the frame into a page table reference let virt = frame.start_address().as_u64() + physical_memory_offset; let table_ptr: *const PageTable = VirtAddr::new(virt).as_ptr(); let table = unsafe {&*table_ptr}; // read the page table entry and update `frame` let entry = &table[index]; frame = match entry.frame() { Ok(frame) => frame, Err(FrameError::FrameNotPresent) => return None, Err(FrameError::HugeFrame) => panic!("huge pages not supported"), }; } // calculate the physical address by adding the page offset Some(frame.start_address() + u64::from(addr.page_offset())) }
関数
active_level_4_table
を再利用する代わりに、registerから第4レベルのフレームを再読み取りし
CR3
ます。これにより、プロトタイプの実装が簡単になるためです。心配しないでください、私たちはすぐに解決策を改善します。
この構造
VirtAddr
は、4つのレベルのページのテーブルでインデックスを計算するメソッドをすでに提供しています。すべてのテーブルをループできるため、これらのインデックスを小さな配列に格納し
for
ます。ループの外側では、後で物理アドレスを計算するために訪れた最後のフレームを覚えています。
frame
反復中のページテーブルのフレームと、最後の反復の後、つまりレベル1レコードを通過した後の関連フレームを指します。
ループ内で、再度適用します
physical_memory_offset
フレームをページテーブルリンクに変換します。次に、現在のページテーブルのレコードを読み取り、関数
PageTableEntry::frame
を使用して一致したフレームを取得します。レコードがフレームにマッピングされていない場合、を返し
None
ます。レコードに巨大な2 MiBまたは1 GiBページが表示される場合、これまでのところパニックになります。
それで、いくつかのアドレスで翻訳関数をチェックしましょう:
// in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::translate_addr; use x86_64::VirtAddr; let addresses = [ // the identity-mapped vga buffer page 0xb8000, // some code page 0x20010a, // some stack page 0x57ac_001f_fe48, // virtual address mapped to physical address 0 boot_info.physical_memory_offset, ]; for &address in &addresses { let virt = VirtAddr::new(address); let phys = unsafe { translate_addr(virt, boot_info.physical_memory_offset) }; println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); }
コードを実行すると、次の結果が得られます。

予想どおり、同じマッピングで、アドレスは
0xb8000
同じ物理アドレスに変換されます。コードページとスタックページは、ローダーがカーネルの初期マッピングをどのように作成したかに応じて、任意の物理アドレスに変換されます。マッピング
physical_memory_offset
は物理アドレスを指す必要
0
がありますが、変換は効率のために大きなページを使用するため失敗します。ローダーの将来のバージョンでは、カーネルとスタックページに同じ最適化が適用される可能性があります。
MappedPageTableを使用する
仮想アドレスの物理アドレスへの変換は、OSのカーネルの典型的なタスクです。したがって、クレート
x86_64
はそれを抽象化します。それは既に巨大なページといくつかの他の機能をサポートしていますが
translate_addr
、それ以外の場合は、独自の実装に大きなページのサポートを追加する代わりに使用します。
抽象化の基礎は、ページテーブルのさまざまな変換関数を定義する2つの特性です。
- この特性
Mapper
は、ページで機能する機能を提供します。たとえば、translate_page
このページを同じサイズのフレームにmap_to
変換し、テーブルに新しいマッピングを作成します。
- この特性は、すべてのページサイズの
MapperAllSizes
適用Mapper
を意味します。また、translate_addr
またはなどの異なるサイズのページで機能する機能を提供しますtranslate
。
特性はインターフェースのみを定義しますが、実装は提供しません。現在、サブラック
x86_64
は、特性を実装する2つのタイプを提供します:
MappedPageTable
と
RecursivePageTable
。最初の方法では、ページテーブルの各フレームをどこかに表示する必要があります(たとえば、オフセット付き)。2番目のタイプは、4番目のレベルのテーブルが再帰的に表示される場合に使用できます。
すべての物理メモリがにマップされている
physical_memory_offset
ため、MappedPageTableタイプを使用できます。初期化するに
init
は、モジュールに新しい関数を作成します
memory
:
use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr; /// Initialize a new MappedPageTable. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn init(physical_memory_offset: u64) -> impl MapperAllSizes { let level_4_table = active_level_4_table(physical_memory_offset); let phys_to_virt = move |frame: PhysFrame| -> *mut PageTable { let phys = frame.start_address().as_u64(); let virt = VirtAddr::new(phys + physical_memory_offset); virt.as_mut_ptr() }; MappedPageTable::new(level_4_table, phys_to_virt) } // make private unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable {…}
MappedPageTable
クロージャー型に共通しているため、関数から直接戻ることはできません。この問題を構文構造で回避します
impl Trait
。追加の利点は
RecursivePageTable
、関数のシグネチャを変更せずにカーネルを切り替えることができることです。
この関数で
MappedPageTable::new
は、レベル4のページテーブルへの可変リンクと
phys_to_virt
、物理フレームをページテーブルポインターに変換するクロージャーという2つのパラメーターが必要です
*mut PageTable
。最初のパラメーターでは、関数を再利用できます
active_level_4_table
。 2つ目は
physical_memory_offset
、変換の実行に使用するクロージャーを作成します。
また、これを
active_level_4_table
プライベート関数にし
init
ます。これからは、からのみ呼び出されるからです。
メソッドを使用するには
MapperAllSizes::translate_addr
独自の関数の代わりに
memory::translate_addr
、以下の数行を変更する必要があります
kernel_main
。
// in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS // new: different imports use blog_os::memory; use x86_64::{structures::paging::MapperAllSizes, VirtAddr}; // new: initialize a mapper let mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let addresses = […]; // same as before for &address in &addresses { let virt = VirtAddr::new(address); // new: use the `mapper.translate_addr` method let phys = mapper.translate_addr(virt); println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); }
開始後、以前と同じ変換結果が表示されますが、巨大ページのみが機能する

ようになりました。予想どおり、仮想アドレスは
physical_memory_offset
物理アドレスに変換されます
0x0
。typeの変換関数を使用
MappedPageTable
すると、巨大なページのサポートを実装する必要がなくなります。また
map_to
、次のセクションで使用するような他のページ機能にもアクセスできます。この段階で、関数
memory::translate_addr
は必要なくなりました。必要に応じて削除できます。
新しいマッピングを作成する
これまでのところ、ページテーブルのみを見てきましたが、何も変更していません。以前に表示されていないページの新しいマッピングを作成しましょう。trait
の関数を使用するため、最初にこの関数を検討します。ドキュメントには、4つの引数が必要であると書かれています。表示するページ。ページがマップされるフレーム。ページテーブルとフレームディストリビュータを書くためのフラグのセット。このページのマッピングには、バックアップストレージとして未使用のフレームを必要とする追加テーブルの作成が必要になる場合があるため、フレームアロケーターが必要です。
map_to
Mapper
frame_allocator
機能 create_example_mapping
実装の最初のステップは、
create_example_mapping
このページを
0xb8000
VGAテキストバッファーの物理フレームにマップする新しい関数を作成することです。このフレームを選択するのは、ディスプレイが正しく作成されたかどうかを簡単に確認できるためです。最近表示したページに書き込み、画面に表示されるかどうかを確認するだけです。
関数
create_example_mapping
は次のようになります。
// in src/memory.rs use x86_64::structures::paging::{Page, Size4KiB, Mapper, FrameAllocator}; /// Creates an example mapping for the given page to frame `0xb8000`. pub fn create_example_mapping( page: Page, mapper: &mut impl Mapper<Size4KiB>, frame_allocator: &mut impl FrameAllocator<Size4KiB>, ) { use x86_64::structures::paging::PageTableFlags as Flags; let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); }
page
マップされるページに加えて、関数は
mapper
およびのインスタンスを予期します
frame_allocator
。型は、メソッドが提供する
mapper
特性
Mapper<Size4KiB>
を実装します
map_to
。
Size4KiB
特性
Mapper
は特性に共通であるため、一般的なパラメーターが必要です。これは
PageSize
、標準の4 KiBページと2 MiBと1 GiBの巨大ページの両方で機能するためです。 4つのKiBページのみを作成するため
Mapper<Size4KiB>
、要件の代わりに使用できます
MapperAllSizes
。
比較のために、フラグを設定します
PRESENT
。これは、すべての有効なエントリに必要な
WRITABLE
ので、表示されたページを書き込み可能にするためのフラグです。挑戦する
map_to
安全でない:無効な引数を使用するとメモリセキュリティに違反する可能性があるため、ブロックを使用する必要があります
unsafe
。可能なすべてのフラグのリストについては、前の記事の「ページテーブル形式」セクションを参照してください。
関数
map_to
は失敗する可能性があるため、を返します
Result
。これは信頼性の低いコードの例にすぎないため
expect
、エラー発生時にパニックを起こすために使用します。成功した場合、関数は
MapperFlush
メソッドを使用して、最近表示されたページを動的翻訳バッファー(TLB)から簡単にクリアするタイプを返します
flush
。同様に
Result
、このタイプは[
#[must_use]
] 属性を適用します誤って使用を忘れた場合に警告を発行する。
架空の FrameAllocator
呼び出すには
create_example_mapping
、最初にを作成する必要があります
FrameAllocator
。上記のように、新しいディスプレイを作成する複雑さは、表示する仮想ページによって異なります。最も単純なケースでは、ページのレベル1テーブルがすでに存在し、1つのレコードを作成するだけです。最も難しいケースでは、ページはレベル3がまだ作成されていないメモリ領域にあるため、最初にレベル3、2、および1の
ページテーブルを作成する必要があります。簡単なケースから始めて、新しいページテーブルを作成する必要がないと仮定します。このためには、常に戻るフレームディストリビュータで十分
None
です。
EmptyFrameAllocator
テスト用にこのような表示関数を作成します。
// in src/memory.rs /// A FrameAllocator that always returns `None`. pub struct EmptyFrameAllocator; impl FrameAllocator<Size4KiB> for EmptyFrameAllocator { fn allocate_frame(&mut self) -> Option<PhysFrame> { None } }
次に、新しいページテーブルを作成せずに表示できるページを見つける必要があります。ローダーは仮想アドレス空間の最初のメガバイトにロードされるため、この領域には有効なレベル1のテーブルがあることがわかります
0x1000
。
関数をテストするには、最初にページ
0x1000
を表示し、次にメモリの内容を表示します。
// in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory; use x86_64::{structures::paging::Page, VirtAddr}; let mut mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let mut frame_allocator = memory::EmptyFrameAllocator; // map a previously unmapped page let page = Page::containing_address(VirtAddr::new(0x1000)); memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); // write the string `New!` to the screen through the new mapping let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); }
まず、のページのマッピングを作成し、インスタンスとへの可変リンクを使用して
0x1000
関数
create_example_mapping
を呼び出します。これにより、ページがVGAテキストバッファフレームにマップされるため、画面に表示されている内容が表示されます。次に、ページを生のポインターに変換し、値をoffsetに書き込みます。 VGAバッファーの一番上の行は次のように画面から直接シフトされるため、ページの上部には書き込みません。白い背景に文字列「New!」に一致する値を書き込みます。「VGAテキストモード」の記事で学んだように、VGAバッファーへの書き込みは揮発性である必要があるため、メソッドを使用します。
mapper
frame_allocator
0x1000
400
println
0x_f021_f077_f065_f04e
write_volatile
QEMUでコードを実行すると、次の結果が表示されます。

ページへの書き込み後
0x1000
、画面に「New!」という碑文が表示されました。これで、ページテーブルに新しいマッピングが正常に作成されました。
照合用のレベル1のテーブルが既に存在したため、この照合は機能しました
0x1000
。レベル1のテーブルがまだ存在しないページに一致させようとすると、
map_to
フレームを割り当て
EmptyFrameAllocator
て新しいテーブルを作成しようとするため、関数は失敗します。次の
0xdeadbeaf000
代わりにページを表示しようとすると、これが発生することがわかります
0x1000
。
// in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); […] }
これが開始されると、次のエラーメッセージでパニックが発生します。
panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5
ページレベル1のテーブルがまだないページを表示するには、正しいテーブルを作成する必要があります
FrameAllocator
。しかし、どのフレームが無料で、どのくらいの物理メモリが利用可能であるかをどうやって知るのでしょうか?
フレーム選択
新しいページテーブルの場合、正しいフレームディストリビュータを作成する必要があります。一般的なスケルトンから始めましょう:
// in src/memory.rs pub struct BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { frames: I, } impl<I> FrameAllocator<Size4KiB> for BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { fn allocate_frame(&mut self) -> Option<PhysFrame> { self.frames.next() } }
フィールド
frames
は、任意のフレーム反復子で初期化できます。これにより、
alloc
メソッドへの呼び出しを簡単に委任できます
Iterator::next
。
初期化には、ブートローダーが構造の一部として転送
BootInfoFrameAllocator
するメモリカードを使用します。ブート情報セクションで説明したように、メモリカードはBIOS / UEFIファームウェアによって提供されます。ブートプロセスの最初の段階でのみ要求できるため、ブートローダーは必要な関数を既に呼び出しています。メモリカードは、各メモリ領域の開始アドレス、長さ、およびタイプ(たとえば、未使用、予約済みなど)を含む構造のリストで構成されます。未使用領域からフレームを生成するイテレータを作成することにより、有効なイテレータを作成できます。
memory_map
BootInfo
MemoryRegion
BootInfoFrameAllocator
初期化
BootInfoFrameAllocator
は新しい関数で行われます
init_frame_allocator
:
// in src/memory.rs use bootloader::bootinfo::{MemoryMap, MemoryRegionType}; /// Create a FrameAllocator from the passed memory map pub fn init_frame_allocator( memory_map: &'static MemoryMap, ) -> BootInfoFrameAllocator<impl Iterator<Item = PhysFrame>> { // get usable regions from memory map let regions = memory_map .iter() .filter(|r| r.region_type == MemoryRegionType::Usable); // map each region to its address range let addr_ranges = regions.map(|r| r.range.start_addr()..r.range.end_addr()); // transform to an iterator of frame start addresses let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096)); // create `PhysFrame` types from the start addresses let frames = frame_addresses.map(|addr| { PhysFrame::containing_address(PhysAddr::new(addr)) }); BootInfoFrameAllocator { frames } }
この関数は、コンビネーターを使用して、初期マップ
MemoryMap
を使用済み物理フレームのイテレーターに変換します。
- まず、メソッドを呼び出して
iter
メモリカードをイテレータに変換しますMemoryRegion
。次に、メソッドを使用して、filter
予約済みまたはアクセスできない領域をスキップします。ローダーは、作成するすべてのマッピングに対してメモリカードを更新するため、カーネルが使用するフレーム(コード、データ、またはスタック)またはブートに関する情報を保存するためのフレームは、すでにInUse
または同様にマークされています。したがって、フレームが他の場所でUsable
使用されていないことを確認できます。
-
map
range Rust .
- :
into_iter
, 4096-step_by
. 4096 (= 4 ) — , . , .flat_map
map
,Iterator<Item = u64>
Iterator<Item = Iterator<Item = u64>>
.
-
PhysFrame
,Iterator<Item = PhysFrame>
.BootInfoFrameAllocator
.
これで
kernel_main
、
BootInfoFrameAllocator
代わりにインスタンスを渡すように関数を変更できます
EmptyFrameAllocator
。
// in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let mut frame_allocator = memory::init_frame_allocator(&boot_info.memory_map); […] }
今回は、アドレスマッピングが成功し、画面に黒と白の「New!」が再び表示されます。背後で、このメソッド
map_to
は次のように欠落しているページテーブルを作成します。
- 送信されたフレームから未使用のフレームを選択し
frame_allocator
ます。
- 新しい空のページテーブルを作成するゼロフレーム。
- 上位レベルのテーブルエントリをこのフレームにマップします。
- 表の次のレベルに移動します。
関数
create_example_mapping
は単なるサンプルコードですが、任意のページの新しいマッピングを作成できるようになりました。これは、今後の記事でメモリを割り当ててマルチスレッドを実装するために必要になります。
まとめ
この記事では、IDマッピング、完全な物理メモリのマッピング、一時マッピング、再帰ページテーブルなど、ページテーブルの物理フレームにアクセスするさまざまな方法について学びました。シンプルで強力な方法として、物理メモリ全体を表示することにしました。
ページテーブルにアクセスせずにカーネルから物理メモリをマップすることはできないため、ブートローダーのサポートが必要です。ラック
bootloader
は、追加の貨物機能により必要なマッピングを作成します。必要な情報
&BootInfo
を、エントリポイント関数の引数としてカーネルに渡します。
この実装では、最初にページテーブルを手動で調べ、翻訳関数を作成してから、
MappedPageTable
クレートのタイプを使用しました
x86_64
。また、ページテーブルで新しいマッピングを作成する方法と
FrameAllocator
、ブートローダーによって送信されたメモリカードにマッピングを作成する方法も学びました。
次は?
次の記事では、カーネル用のヒープメモリ領域を作成します。これにより、メモリを割り当て、さまざまなタイプのコレクションを使用できるようになります。