ARM64: ハイパーバイザー MMU Stage2 メモリ仮想化

2023/03/14に公開

1. はじめに

本記事は、ハイパーバイザー (EL2) 観点で ARM64 のメモリ仮想化 (MMU) の Stage2 (正確にはStage 2 translation) について解説しています。Stage2 によるメモリ仮想化はハイパーバイザーでゲスト OS (EL1) のメモリ空間を仮想化するために利用されます。

前提知識: EL (Exception Level)

AArch64 (ARMv8) アーキテクチャの場合、レジスタなどのアクセス権限などに関するレベルが EL0 から EL3 まで存在します。数字が大きいほど出来ることが多く(権限が強く)なります。

EL 用途
EL0 アプリケーション
EL1 OS(通常のLinuxカーネルなど)
EL2 ハイパーバイザー
EL3 Firmware (セキュアモニタ)

2. EL2 によるゲストOSのメモリ空間の仮想化

通常の OS のメモリ空間の仮想化と同じような仕組みがハイパーバイザー (EL2) 側にも用意されており、それを使ってハイパーバイザーも自身が利用するメモリ空間の仮想化が可能です。通常はメモリアドレスをどこかに再マップしたりすることはなく、物理アドレス=仮想アドレスで利用すると思います(キャッシュは利用する)。

このハイパーバイザー自身が利用する(主にキャッシュ利用目的)ためのメモリの仮想化は Stage1 と呼ばれます。そしてこれとは別にもう一段階の仮想化の仕組みが Stage2 として用意されています。

なぜ Stage2 が必要なのか?

ハイパーバイザーの場合、ゲスト OS による特定のメモリ空間のアクセスをトラップ(主にハードウェアのエミュレーションなどの目的で)したり、特定のメモリへのアクセスは禁止する(見えなくする)ために、ゲストOS向けにさらにメモリを仮想化したい場合があります。

というか、実際には必須だと思います。

ハードウェアによる仮想化支援機構

そこで、ARM64 では Stage2 というハードウェアによる仮想化支援の機能が実装されています。Stage2 の設定は Stage1 の MMU の設定とは若干異なるのですが、大まかには Stage1 の MMU の設定と同じ感じで対応出来ます(ページテーブルを作るなど)。

MMU Stage1 によるハイパーバイザーが利用する仮想アドレスは、ゲスト OS 向けに Stage2 でもう1段階、仮想化を行います。あるメモリ空間はアクセス禁止、ある空間はハードウェアへの直接アクセスを許可するなど、Stage2 を利用することでハイパーバイザーが自由にメモリ空間を構築可能です。

ゲスト OS 側から見るとハードウェアの物理アドレスだと思って利用しているものの、実際にはそれは中間物理アドレス(IPA)であり、IPA は Stage2 で VTTBR_EL2 で設定されたページテーブルによって行われるアドレス変換により EL2 空間からみた仮想アドレスに変換され、そして Stage1 により EL2 空間からみた物理アドレスに変換されます。

と、このように EL2 ではメモリの仮想化が2段階構成になっています。

3. ページテーブルエントリー (PTE)

Stage2 のページエントリーは、 Stage1 のそれとは同じようで実際には若干仕様が異なるため注意が必要です(ビットアサインや名称など)。

Stage2 ページテーブル (L0 - L2) 仕様

まず、L0 - L2 のページテーブルの仕様は基本的には同じですが、Stage2 だと Bit[63:59] が Reserved 扱いな点だけが異なります。

Stage2 ページエントリー (L3) 仕様

Stage2 と Stage1 では、Upper attributes と Lower attributes のフィールドの定義が少し異なります。

XN, bits[54]

XN(Execute Never)は、メモリページに対して実行権限を与えるかどうかを指定するビットフィールドです。

XNビットが0の場合、メモリページには実行権限が与えられ、CPU はそのページ内にある命令を実行できます。

一方、XNビットが1の場合、メモリページには実行権限が与えられず、CPU はそのページ内にある命令を実行できません。この機能は、悪意のあるコードが実行されるのを防ぐためにも役立つ様子です。

Contiguous, bits[52]

複数のページを連続した物理アドレス領域にマッピングするための機能です。具体的には、ページテーブルエントリーにおいて、本ビットフィールドが 1 に設定されている場合、該当するページテーブルエントリーは、物理アドレスが連続する複数のページをマッピングすることを示します。

本機能を利用することで、物理アドレス領域を効率的にマッピングすることができます。例えば、複数の物理ページを連続したアドレス空間にマッピングする場合、Contiguous ビットフィールドを使用することで、単一のページテーブルエントリーでこれらの物理ページをマッピングすることができます。これにより、ページテーブルのエントリー数を削減し、メモリ使用量を削減することができます。

DBM, bits[51]

ハードウェアによって制御され、値が 1 であれば該当ページがダーティ状態であることを示します。VTCR_EL2.HD (Hardware management of dirty state in stage 2 translations) レジスタに 1 をセットすることで本機能を有効に出来ます。

AF, bits[10]

TLB 関連のハードウェア制御で利用されるビットで、ソフトウェアでエントリーを更新する場合には必ず 1 をセットする必要があります。

SH[1:0], bits[9:8]

該当のメモリ (キャッシュ/バッファ) を共有するための機能であり、複数のプロセッサやコアが同じ物理アドレス空間を共有している場合に有用です。以下の4つのオプションがあります。

意味
0b00 Non-shareable, 共有しない
0b01 Reserved
0b10 Inner Shareable, 異なるプロセッサ/コア間でもデータを共有可能
0b11 Outer Shareable, 同一のプロセッサ/コア間でのみ共有可能

S2AP[1:0], bits[7:6]

S2AP (Stage 2 data Access Permissions) は、EL1/0からのアクセスパーミッションを指定するためのビットフィールドです。

意味
0b00 EL1/0からのアクセスはRead/Write共に禁止
0b01 EL1/0からのアクセスはReadアクセスのみ許可
0b10 EL1/0からのアクセスはWriteアクセスのみ許可
0b11 EL1/0からのアクセスはRead/Write共に許可

MemAttr[3:0], bits[5:2]

MemAttr(Memory Attributes)は、メモリページにアクセスする際の属性を指定します。対象のアドレスがDRAMなのかI/Oなのか、キャッシュポリシーなどを指定可能です。

  • MemAttr[0]:メモリアクセスのセキュリティ属性(Secure/Non-Secure)
  • MemAttr[1]:メモリアクセスのキャッシュ属性(Cacheable/Non-Cacheable)
  • MemAttr[2]:メモリアクセスのBuffer属性(Bufferable/Non-Bufferable)
  • MemAttr[3]:メモリアクセスの共有属性(Shareable/Non-Shareable)

また、MAIR_EL2 レジスタのビットフィールド0と1にキャッシュポリシーを定義設定し、それをページテーブルエントリーの本ビットフィールドでインデックス指定として設定することが出来ます。

VTCR_EL2 レジスタ

VTCR_EL2 (Virtualization Translation Control Register (EL2)) は Stage2 の制御用レジスタです。詳細は追って更新予定。。。

4. IPA から EL2 仮想アドレスへの変換

デバイスのレジスタなど、ハイパーバイザー側でゲスト OS のメモリアクセスをトラップする場合(Stage2のページテーブルエントリーの設定次第)、FAR_EL2レジスタにアクセスされたアドレスが格納され、ESR_EL2にアクセスのタイプ(リードライトなど)情報が格納されています。この2つの情報を参照し、ハイパーバイザー側で必要なトラップ処理を行います。

ここで注意が必要なのが、FAR_EL2レジスタに格納されている情報は EL1 の物理アドレス(つまり IPA)であることです。

AT (Address translation) 命令

IPA を EL2 の仮想アドレス(Stage1 が未使用であれば物理アドレス)に変換するためには、AT (Address translation) 命令を利用します。

AT 命令には、AT S1E1R、AT S1E1W、AT S1E0R、AT S1E0W、AT S1E2R、AT S1E2W、AT S12E1R、AT S12E1W、AT S12E0R、AT S12E0W、AT S1E3R、AT S1E3Wなどの複数のオペレーションが存在します。

mmu_el1_ipa_to_el2_va:
  at s12e1r, x0     // Translation stage 1 by stage 2 translation (EL1)
  mrs x0, par_el1   // Read result
  ret

https://armv8-ref.codingbelief.com/en/chapter_d4/d42_11_address_translation_instructions.html

5. ページテーブル更新後は TLB (Translation Lookaside Buffer) と データキャッシュに注意

これは通常の OS (EL1) も同様ですが、ハイパーバイザーでもゲスト OS に必要なページを割り当てる場合に、必要になった(ページフォルトが発生)時に新規ページを割り当てる、オンデマンドページングを行います。

この時、ハイパーバイザーは Stage2 のページテーブルを新規追加(もしくは更新)することになりますが、この時には TLB とデータキャッシュを適切に扱う必要があります。適切に処理を行わないと、ページテーブルは更新したけど、それが Stage2 の MMU (TLB) に正常に反映されずに永遠にページフォルトが発生するというバグに遭遇することになります。

データキャッシュのフラッシュ

ページテーブルエントリーを更新後は、そのアドレスのキャッシュラインを適切にフラッシュ(キャッシュラインを書き戻し、かつそのキャッシュラインを無効化)する必要があります。

データキャッシュ操作は dc 命令を利用します。civacClean and Invalidate data cache by address to Point of Coherency の略称で、指定されたアドレスのキャッシュラインに対してフラッシュ(クリーンとインバリデート)を行います。

void flush_dcache(void* addr) {
  __asm__ __volatile__("dc civac, %0" : : "r" (addr) : "memory");
  __asm__ __volatile__("dsb sy");
}

https://developer.arm.com/documentation/ddi0601/2022-03/AArch64-Instructions/DC-CIVAC--Data-or-unified-Cache-line-Clean-and-Invalidate-by-VA-to-PoC

TLB のインバリデート

上記のデータキャッシュのフラッシュさえしていれば、TLB に対して操作はする必要がないと思っていますが、もしも間違っていたらコメント下さい...

ちなみに、TLB は TLBI 命令を利用してインバリデート出来ます。Stage1 & 2 全部のエントリーをインバリデートするなら、VTTBR_EL2に対象のゲスト OS のページテーブルのアドレスを設定した上で、以下のようなコードを実行すると出来ちゃいます。

void tlb_invalidate_stage12_all() {
  __asm__ __volatile__("dsb ish");
  __asm__ __volatile__("tlbi vmalls12e1is");
  __asm__ __volatile__("dsb ish");
  __asm__ __volatile__("isb");
}

6. 参考文献

Discussion