Rustでオペレーティングシステムを作成します。 ページメモリの実装(新規)

この記事では、コアにページメモリサポートを実装する方法を説明します。 まず、物理ページテーブルのフレームがカーネルで使用できるようにさまざまな方法を検討し、それらの長所と短所について説明します。 次に、アドレス変換機能と新しいマッピングを作成する機能を実装します。



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つの時間比較を作成します。





これで、カーネルは0 KiB



で始まるページに書き込むことでレベル2のテーブルにアクセスでき、 33 KiB



で始まるページに書き込むことでレベル4のテーブルにアクセスできます。



したがって、一時マッピングを使用したページテーブルの任意のフレームへのアクセスは、次のアクションで構成されます。





このアプローチでは、同じ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つの再帰的エントリのみ)で済むた​​め、これは最初の実験に適しています。



ただし、次のような欠点があります。





ブートローダーのサポート



上記のすべてのアプローチでは、ページテーブルと対応する設定を変更する必要があります。 たとえば、物理メモリを同じようにマッピングするか、第4レベルのテーブルのレコードを再帰的にマッピングします。 問題は、ページテーブルにアクセスせずにこれらの設定を行うことができないことです。



だから、ブートローダーの助けが必要です。 彼はページテーブルにアクセスできるため、必要なディスプレイを作成できます。 現在の実装では、 bootloader



クレートは、 カーゴ関数を使用した上記の2つのアプローチをサポートしています





カーネルでは、シンプルでプラットフォームに依存しない、より強力なアプローチであるため、最初のオプションを選択します(ページテーブルだけでなく、他のフレームにもアクセスできます)。ブートローダーからのサポートのために、依存関係に関数を追加しますmap_physical_memory







 [dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]}
      
      





この機能が有効な場合、ブートローダーは物理メモリ全体を未使用の仮想アドレスの範囲にマップします。ある範囲の仮想アドレスをカーネルに渡すために、ブートローダーはブート情報の構造を渡します



ブート情報



クレートbootloader



、カーネルに渡されるすべての情報とともにBootInfoの構造を定義します。構造はまだ確定中であるため、semverと互換性のない将来のバージョンにアップグレードする際にエラーが発生する可能性があります現在、構造には2つのフィールドがあります:memory_map



および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つの特性です。





特性はインターフェースのみを定義しますが、実装は提供しません。現在、サブラック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



を使用済み物理フレームのイテレーターに変換します。





これで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



は次のように欠落しているページテーブルを作成します。





関数create_example_mapping



は単なるサンプルコードですが、任意のページの新しいマッピングを作成できるようになりました。これは、今後の記事でメモリを割り当ててマルチスレッドを実装するために必要になります。



まとめ



この記事では、IDマッピング、完全な物理メモリのマッピング、一時マッピング、再帰ページテーブルなど、ページテーブルの物理フレームにアクセスするさまざまな方法について学びました。シンプルで強力な方法として、物理メモリ全体を表示することにしました。



ページテーブルにアクセスせずにカーネルから物理メモリをマップすることはできないため、ブートローダーのサポートが必要です。ラックbootloader



は、追加の貨物機能により必要なマッピングを作成します。必要な情報&BootInfo



を、エントリポイント関数の引数としてカーネルに渡します。



この実装では、最初にページテーブルを手動で調べ、翻訳関数を作成してから、MappedPageTable



クレートのタイプを使用しましたx86_64



また、ページテーブルで新しいマッピングを作成する方法とFrameAllocator



、ブートローダーによって送信されたメモリカードにマッピングを作成する方法も学びました。



次は?



次の記事では、カーネル用のヒープメモリ領域を作成します。これにより、メモリ割り当て、さまざまなタイプのコレクションを使用できるようになります



All Articles