カーネルはどのようにして物理メモリのベースアドレスを認識しますか?

Jan 02 2021

私は2つの密接に関連する問題を理解しようとしています。

  1. ブートローダー後、MMUを有効にする前に実行されるカーネルコードは、物理/ IDマップされた仮想メモリで動作します。異なるCPU間でこのコードをどのように移植可能にすると、異なる物理アドレス範囲のDRAMが使用される可能性がありますか?

  2. カーネルがページテーブルを管理するには、物理​​メモリのベースアドレスや使用可能な物理メモリなど、使用可能な物理メモリリソースをある程度認識している必要があるため、DRAMの範囲外の物理アドレスは割り当てられません。

これは実装にいくらか依存していると思いますが、さまざまなアーキテクチャがこの問題をどのように処理するかについての参照をいただければ幸いです。私がこれまでに持っているいくつかのアイデア:

  1. 物理アドレスのDRAM範囲、または少なくともベースアドレスは、カーネルのコンパイル時に組み込まれます。これは、同じISAであっても、異なるCPUに対して再コンパイルが必要であることを意味します。これは、ここでのこの回答に触発されています。これは、私が正しく理解していれば、カーネルベースアドレスの同じソリューションを説明しています。ベースアドレスはコンパイル時にわかっているため、カーネルコードは、DRAM /カーネルベースアドレスからのオフセットではなく、リテラルアドレスを参照します。

  2. DRAM情報は、残りの物理メモリマップとともにデバイスツリーから読み取られ、学習されます。これは、少なくとも、ザイリンクスのZynq SoCのの、のようなフォーラムの投稿をもとにしています私の印象である。この。このソリューションは柔軟性が高く、カーネル全体ではなくブートローダーを再コンパイルしてCPUを移植することができますが、X86パーソナルマシンが実行時にインストールしたDRAMの量をどのように検出できるのか疑問に思います。ページテーブルを管理するコードは、DRAMベースアドレスからのオフセットを参照するだけであり、異なるDRAM物理アドレス範囲を持つCPU間で再コンパイルすることなく移植可能です。

回答

3 HadiBrais Jan 04 2021 at 12:43

起動時に使用可能な物理メモリDIMM全体は、物理メモリアドレススペースの単一の連続した範囲にマップされない場合があり、通常はマップされないため、「ベースアドレス」はありません。ハードリセットでは、CPUファームウェアの実行が完了した後、プラットフォームファームウェア(通常はレガシーBIOSまたはUEFI)が実行されます。特定のマザーボードは、DIMMやプラットフォームファームウェアメモリデバイスなどの物理メモリを検出するために通常同じ方法を持つ限られたCPUコレクションのセットとのみ互換性があります。プラットフォームファームウェアの実装では、このメソッドを使用して、各エントリが物理メモリアドレス範囲を記述するメモリ記述エントリのテーブルを作成します。このプロセッサの外観の詳細については、「BIOSがDRAMを初期化する方法」を参照してください。。このテーブルは、この目的のために予約されていることがわかっており、実際のメモリによってバックアップされることになっているメインメモリ(DIMM)のアドレスに格納されます(システムはDIMMなしで起動できます)。

90年代半ば以降のINT 15h E820hx86PC BIOSのほとんどの実装は、リアルモード機能を提供します(15hは割り込み番号で、E820hはAXレジスタに渡される引数です)。これは、PhoenixBIOS v4.0(1992-1994、正確な年を特定できません)で最初に導入され、後に他のBIOSベンダーによって採用されたベンダー固有のBIOS機能です。このインターフェイスは、1996年にリリースされたACPI 1.0仕様と、PhoenixBIOSでサポートされているACPI以降のリビジョンによって拡張されました。対応するUEFIインターフェイスはですGetMemoryMap()。これはUEFIブートタイムサービスです(つまり、UEFI仕様で定義されているブート時にのみ呼び出すことができます)。カーネルは、これらのインターフェイスの1つを使用して、すべてのNUMAノードのメモリを説明するアドレスマップを取得できます。x86プラットフォームでの他の(古い)方法については、メモリの検出(x86)で説明しています。バージョン?から始まるACPI仕様の両方 およびバージョンから始まるUEFI仕様?DRAMDIMMおよびNVDIMMのメモリ範囲タイプをサポートします。

たとえば、ACPI互換のLinuxカーネルが、x86 ACPI対応のBIOSプラットフォームで使用可能(つまり、実際のメモリに支えられている)および使用可能(つまり、空き)の物理アドレス範囲をどのように決定するかを考えてみます。BIOSファームウェアは、指定された起動可能なストレージデバイスからこの目的専用のメモリ位置にブートローダーをロードします。ファームウェアの実行が完了すると、ブートローダーにジャンプします。ブートローダーは、ストレージメディア上のカーネルイメージを検索し、それをメモリにロードして、制御をカーネルに転送します。ブートローダー自体は、現在のメモリマップを認識し、その操作のためにメモリを割り当てる必要があります。E820h関数を呼び出してメモリマップを取得しようとします。サポートされていない場合は、古いPCBIOSインターフェイスを使用します。カーネル・ブート・プロトコル・メモリ範囲は、ブートローダとメモリ範囲で使用できる定義は、カーネルのために利用可能なままにしなければなりません。

ブートローダー自体は、メモリマップを変更したり、カーネルにマップを提供したりしません。代わりに、カーネルが実行を開始すると、E820h関数を呼び出しES:DI、ブートプロトコルに従ってx86プラットフォームで空いていることがカーネルが認識しているバッファーへの20ビットポインター(in )を渡します。各呼び出しは、サイズが少なくとも20バイトのメモリ範囲記述子を返します。詳細については、ACPI仕様の最新バージョンを参照してください。ほとんどのBIOS実装はACPIをサポートしています。

アップストリームデフォルトのブートパラメータを持つLinuxカーネルを想定すると、コマンドdmesg | grep 'BIOS-provided\|e820'を使用して、返されるメモリ範囲記述子テーブルを確認できます。私のシステムでは、次のようになります。

[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x00000000000917ff] usable
[    0.000000] BIOS-e820: [mem 0x0000000000091800-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000e0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000d2982fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d2983000-0x00000000d2989fff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x00000000d298a000-0x00000000d2db9fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d2dba000-0x00000000d323cfff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d323d000-0x00000000d7eeafff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d7eeb000-0x00000000d7ffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d8000000-0x00000000d875ffff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d8760000-0x00000000d87fffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d8800000-0x00000000d8fadfff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d8fae000-0x00000000d8ffffff] ACPI data
[    0.000000] BIOS-e820: [mem 0x00000000d9000000-0x00000000da718fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000da719000-0x00000000da7fffff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x00000000da800000-0x00000000dbe11fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000dbe12000-0x00000000dbffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000dd000000-0x00000000df1fffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000f8000000-0x00000000fbffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fec00000-0x00000000fec00fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fed00000-0x00000000fed03fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fee00000-0x00000000fee00fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000ff000000-0x00000000ffffffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000041edfffff] usable
[    0.002320] e820: update [mem 0x00000000-0x00000fff] usable ==> reserved
[    0.002321] e820: remove [mem 0x000a0000-0x000fffff] usable
[    0.002937] e820: update [mem 0xdd000000-0xffffffff] usable ==> reserved
[    0.169287] e820: reserve RAM buffer [mem 0x00091800-0x0009ffff]
[    0.169288] e820: reserve RAM buffer [mem 0xd2983000-0xd3ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd2dba000-0xd3ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd7eeb000-0xd7ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd8760000-0xdbffffff]
[    0.169290] e820: reserve RAM buffer [mem 0xd8fae000-0xdbffffff]
[    0.169291] e820: reserve RAM buffer [mem 0xda719000-0xdbffffff]
[    0.169291] e820: reserve RAM buffer [mem 0xdbe12000-0xdbffffff]
[    0.169292] e820: reserve RAM buffer [mem 0x41ee00000-0x41fffffff]

その表に「BIOS-e820」で始まるメモリ範囲を示します。最初の行は、この情報のソースを明確に示しています。この情報の正確な形式は、Linuxカーネルのバージョンによって異なります。いずれの場合も、各エントリに範囲とタイプが表示されます。「e820」で始まる行(「BIOS-」部分なし)は、カーネル自体がテーブルに加えた変更です。の実装にE820hバグがあるか、異なるエントリで取得された範囲が重複している可能性があります。カーネルは必要なチェックを実行し、それに応じて変更します。「使用可能」とマークされた範囲は、ACPI仕様で説明されており、カーネルが認識している例外を除いて、カーネルはほとんど無料で使用できます。PC BIOS実装の大部分は、最大128のメモリ範囲記述子を返します。Linuxカーネルの古いバージョンは最大128のメモリ範囲しか処理できなかったためE820h、128番目以降から返されたエントリはすべて無視されます。バージョン?以降、この制限は緩和されました。詳細については、「x86ブート:セットアップデータのリンクリストを介して128を超えるE820メモリマップエントリを渡す」というタイトルの一連のカーネルパッチを参照してください。

タイプusableとの範囲ACPI data。タイプの範囲はreserved、DRAM DIMMによってバックアップされるか、CPUまたはプラットフォームファームウェアによってMMIO用に切り取られます。タイプの範囲はACPI NVS、ファームウェアメモリによって支えられています。他のすべての範囲は、ファームウェアが認識できる限り、実際のメモリによって戻されるわけではありません。ファームウェアは、インストールされているすべてのDRAMDIMMまたはNVDIMMをマップしないことを選択する場合があることに注意してください。これは、物理メモリ構成がそのままサポートされていない場合、またはDIMMの問題のために、ファームウェアが取り付けられたDIMMから情報を取得できない場合に発生する可能性があります。

インストールされているDRAMDIMMおよびNVDIMMのメモリ量を、ファームウェアによってカーネルが利用できるようにする量を計算できます。私のシステムには、16GBのDRAMDIMMを取り付けました。したがって、一部のDIMMが正しくインストールされていない、正しく機能していない、ファームウェアにバグがある、またはプラットフォームやプロセッサでサポートされていない場合を除いて、カーネルで使用できるのは16GBより少し少ないはずです。

すべてのusable範囲の合計は0x3FA42B800バイトになります。範囲の最後のアドレスは包括的であることに注意してください。つまり、範囲の一部であるバイト位置を指します。物理的に取り付けられたDIMMの合計量は、16GBまたは0x400000000バイトです。したがって、カーネルで使用可能にされなかったインストール済みメモリの合計量は、0x400000000-0x3FA42B800、つまり合計16GBのうち約92MBです。このメモリは、一部のreserved範囲とすべての範囲によって使用されましたACPI data。DRAM DIMMまたはNVDIMMの特定の場所が、プラットフォームファームウェアによって信頼できないと判断された場合、それらもとして切り取られreservedます。

E820ACPI仕様に従って、範囲0x000a0000-0x000fffffがメモリマップに記述されていないことに注意してください。これは640KB-1MBの上位メモリ領域です。カーネルは、古いシステムとの互換性を維持するために、使用可能なメモリ領域からこの範囲を削除したことを示すメッセージを出力します。

この時点で、ほとんどのPCIeデバイスのMMIOとして使用されるメモリはまだ割り当てられていません。私のプロセッサは39ビットの物理アドレス空間をサポートしています。つまり、0から2 ^ 39までのアドレスをマッピングに使用できます。これまでのところ、このスペースの下部16.5GBのほとんどだけが何かにマップされています。この範囲にはまだマップされていないギャップがあることに注意してください。カーネルは、これらのギャップ(数百MB)と残りの物理アドレス空間(約495.5 GB)を使用して、IOデバイスにアドレス範囲を割り当てることができます。カーネルは最終的にPCIeデバイスを検出し、デバイスごとに、可能な場合は互換性のあるドライバーをロードしようとします。次に、ドライバーは、デバイスに必要なメモリの量と、デバイスによって課されるメモリアドレスの制限を決定し、カーネルからデバイスにメモリを割り当てて、デバイスが所有するMMIOメモリとして構成するように要求します。コマンドを使用して、最終的なメモリマップを表示できますsudo cat /proc/iomem

既存のメモリ範囲のメモリタイプを手動で変更したい場合(テスト用など)、新しい範囲を作成したい場合(DRAMで永続メモリをエミュレートする場合、またはファームウェアがすべてを検出できない場合など)があります。何らかの理由で使用可能なメモリ)、カーネルで使用可能なメモリの量を減らす(たとえば、ベアメタルハイパーバイザーが制限を超えてメモリを使用するのを防ぎ、残りをゲストが使用できるようにする)、またはから返されたテーブル全体を完全にオーバーライドするE820hmemそしてmemmapカーネル偶然には、このような目的のために使用することができます。これらのパラメータの1つ以上が有効な値で指定されている場合、カーネルは最初にBIOSが提供するメモリマップを読み取り、それに応じて変更を加えます。カーネルは、最終的なメモリマップを「ユーザー定義の物理RAMマップ」として出力します。カーネルメッセージリングバッファ内。これらのメッセージはで表示できますdmesg | grep user:(各メモリ範囲の行は「user:」で始まります)。これらのメッセージは、「BIOS-e820」メッセージの後に出力されます。

互換性サポートモジュールをサポートするUEFIファームウェアで起動されたx86プラットフォーム(詳細については、UEFIとは別のCSM仕様を参照)で、レガシーリアルモードE820hインターフェイスがサポートされ、Linuxカーネルはデフォルトでそれを使用します。カーネルがCSMをサポートしないUEFIを備えたx86プラットフォームで実行されている場合、E820hインターフェイスはすべてまたは一部のメモリ範囲を提供しない可能性があります。このadd_efi_memmapようなプラットフォームでは、カーネルパラメータを使用する必要がある場合があります。例は、UEFIメモリVE820メモリにあります。1つ以上のメモリ範囲がから提供されるGetMemoryMap()と、カーネルはこれらの範囲をE820hインターフェイスからの範囲とマージします。結果のメモリマップはdmesg | grep 'efi:'、メモリマップに影響を与える別のUEFI関連のカーネルパラメータを使用して表示できますefi_fake_mem

ACPI仕様(セクション6.3)は、IOまたはDIMMデバイスが任意のS状態でシステムに挿入されたとき、またはシステムから取り外されたときにカーネルに通知する通知メカニズムを提供します。(ただし、S状態でDIMMの取り外しをサポートするマザーボードがあるかどうかはわかりません。これは通常、G3状態、場合によってはS4またはS5でのみ可能です)このようなイベントが発生した場合、カーネルまたはファームウェアはそれに応じてメモリマップを変更します。これらの変更はに反映されsudo cat /proc/iomemます。

mevets Jan 05 2021 at 10:13

pc相対アドレス指定とは、プログラムが任意のアドレスで動作できるプログラミング手法を指します。再配置レジスタ(セグメントなど)がパスになったため、ほとんどのPC相対プログラミングは明示的に実行されます。これは、一般的な種類のマシンコードの例です。

.text
entry:
    call reloc  /* call is pc relative */
reloc:
    pop %r0     /* r0 now contains physical address of reloc */
    sub $reloc, %r0, %r14 /* r14 contains difference between link address of reloc */ /* At this point, r14 is a relocation register. A virtual address + r14 == the corresponding physical address. */ add $proot, %r14, %r0  /* physical address of page table root */
    add $entry, %r14, %r1 /* entry is where we were loaded into ram */ test $0xfff, %r1   /* someone is being funny and not page aligning us */
    jnz bad_alignment
    or   $0x7, %r1 /* put mythical page protection bits in r1 */ mov $1024, %r2     /* number of pages in r2 */
loop:
    store %r1, (%r0)   /* store a page table entry */
    add $0x1000, %r1 /* setup next one 4096 bytes farther */ add $4, %r0        /* point to next page table entry */
    sub $1, r2 /* are we done? */ cmp %0, r2 jne loop /* nope, setup next entry */ add $proot, %r14, %r0
    loadsysreg %r0, page_table_base_register
    mov $1, %r0 mov $v_entry, %r1
    loadsysreg %r0, page_table_enabled
    jmp %r1
v_entry:
        /* now we are virtually addressed */
    call main
1:  jmp 1b   /* main shouldn't return. */


.data
.align 12   /* 4096 byte pages */
proot:
.zero 4096
.text

この神話上のマシンは非常にシンプルで、1つのフラットページテーブルがあり、カーネルはアドレス0でリンクされていますが、最初の4M(1024 * 4096)のどこからでも実行できます。実際のマシンは、これのより詳細なバージョンです。一般に、Cアドレス空間を最初に設定するまでは、のようなシステム言語でさえ信頼することはできません。一度そうなると、その中のコードははるかに複雑なページテーブルを構築し、デバイスツリーのようなデータベースや、RAMレイアウトなどの詳細についてのapic / uefiのような怪物にさえクエリすることができます。

内部ノードがリーフノードと互換性のある形式(たとえば、x86-classic)であるフォワードマップページテーブルアーキテクチャでは、単一のページテーブルを再帰的に使用して、より柔軟なリンクアドレスを許可できます。たとえば、prootの最後のエントリ(つまり、proot [1023])をprootに戻すと、OSを0xffffc000にリンクでき、このコードは(x86に変換されると)機能します。