👻

ARM64 ハイパーバイザー: 割り込みの仮想化

2022/09/24に公開

1. はじめに

ハイパーバイザー (EL2) の実装に必要な ARM64 の割り込みの仮想化についてまとめています。

2. ハードウェアによる仮想化支援

ハイパーバイザー上で動作するvCPU(ゲストOS, VM)でCPUの特定の命令が実行されたり、特定領域のメモリアクセス、割り込み等をハイパーバイザー側でトラップ(検出して必要があれば特定の処理を行い、結果を返すなどのエミュレーション)する必要があります。

ARMの場合、ARMv7-Aからハードウェアでこの仕組みが拡張機能としてサポートされています。

3. 割り込み仮想化の概要

割り込みの仮想化に関しては、大きく分けて2種類の対応方法があります。

1つは、割り込みコントローラー自体をハイパーバイザー側で仮想化(ソフトウェアエミュレーション)する方法です。

もう一つは、ARM 標準でかつほぼデファクトスタンダードとして利用されているGIC (ARM Generic Interrupt Controller) の仮想化の拡張機能を利用する方法です。

今回は、例外はあるもののARM64を搭載しているシステムで最も一般的な割り込みコントローラであるGICの方について説明します。さらに今回は、ラズパイ4でも利用されている GICv2 (version 2) にフォーカスします。

GIC v2仕様書

https://developer.arm.com/documentation/ihi0048/latest/

割り込みハードウェア

以下の図が割り込み関連のブロック図です。GICブロック内のDistributorCPU interfaceといった名前はGICv2仕様書の中で出てくるサブブロックの名称ですが、仮想化サポートのため通常の割り込み系統とは別で、vCPU (EL1のゲストOS) 用にCPUから仮装割り込みのvIRQ, vFIQを生成できるようになっています。

ハイパーバイザー (EL2) では、(EL1向けの)割り込みを一旦全て EL2 にルーティングするように設定し、必要に応じてを EL1 に仮想割り込みを発行することになります。vIRQおよびvFIQはEL1から見ると通常のIRQ, FIQと同じに見えます。

※FIQのソースはシステム(SoC)によって実装が異なります(例. ウォッチドックが発火した場合に緊急処理するための割り込み信号がつながっているなど)

4. 処理フロー

システムブート時の初期設定から実際の割り込み発生時の処理までのざっくりとした処理フローを説明します。なお、レジスタ設定に依存関係がない場合は多少処理が前後しても問題ありません。

  1. EL2 割り込みベクターテーブルの設定
  2. GIC Distributor (GICD_*レジスタ) の設定
  3. GIC CPU interface (GICC_*レジスタ) の設定
  4. GIC Virtual interface control (GICH_*レジスタ) の設定
  5. 該当割り込みの有効化と割り込みハンドラの設定
  6. 外部割り込み発生
  7. vIRQ発行のための設定 (GICH_LR[n]レジスタ)
  8. 割り込みクリア
  9. EL2 から 該当の EL1 (vCPU) にモード遷移(ここで vIRQ が割り込み発生)

1) 割り込みベクターテーブルの設定

EL毎 (EL3, EL2, EL1) に割り込みベクタテーブルを設定することが可能です。ハイパーバイザーの場合、まずは VBAR_EL2 レジスタに割り込みベクタテーブルの先頭アドレスを設定します。

.globl irq_init_irq_vector_table
irq_init_irq_vector_table:
  adr x0, vector_table_el2 // これが別途定義している割り込みベクタの先頭アドレス
  msr vbar_el2, x0
  ret

ベクターテーブルのメモリフォーマット(サイズは0x800バイト)は以下です(0x800バイトアライメントなので定義するときに注意)。

Current EL with SP0 / Current EL with SPx

CUrrent ELと書かれている通り、現在と同じEL (ハイパーバイザの場合はEL2) で発生した割り込みを示します。

SP0SPxの違いは、Nested IRQかどうかです。つまり、スタックの切り替えが必要かどうかです。ということで、Nested IRQをしない限りはSP0のやつは使う必要がないはずです。

Lower EL using AArch64 / Lower EL using AArch32

EL1/EL0向けの割り込みがトラップした場合にコールされます。ここで必要な処理を行い、その後でLE1/EL0向けに仮想割り込みを発行することになります。ゲストの動作モード(64 or 32ビット) ごとに別々のベクタを設定することが可能になっています。

参照元: https://developer.arm.com/documentation/100933/0100/AArch64-exception-vector-table

2) EL1からEL2にルーティング

HCR_EL2レジスタの4ビット目のIMOに1を設定し、割り込みをEL2側にルーティングします。

.globl irq_route_irq_el2
irq_route_irq_el2:
  mrs x0, hcr_el2
  orr x1, x0, #0x10
  msr hcr_el2, x1
  ret

HCR_EL2レジスタの詳細についてはこちらもどうぞ。

https://zenn.dev/hidenori3/articles/1c33e080cd643e

3) GICの設定

ここから割り込みコントローラのGICの設定を行なっていきます。レジスタはサブブロック毎に以下の4種類があります。

  • GICD_*: Distributor
  • GICC_*: CPU interface
  • GICH_*: Hypervisor
  • GICV_*: vCPU interface

MMU Stage2を利用した仮想化

ゲストOS側には何も意識させずに通常通りのGICを見せてあげる必要がある(見かけ上)ため、GICの仮想化が必要です。ここでは詳細は説明しませんが(別途、別の記事で)、MMUのStage2の機能を利用して対応が必要です。

まず、GICD_*レジスタは、ゲストOSにはアクセス禁止にして、ハイパーバイザー側でトラップして、ゲスト毎にレジスタ制御をエミュレートしてあげる必要があります。

次に、GICC_*レジスタは、ゲストOS側にはそれらの代わりにメモリ空間を少しシフトさせてGICV_*レジスタを見せてあげる必要があります。

初期設定

GICD_CTLRレジスタに1を設定し、Distributorを有効化します。同様にGICC_CTLRレジスタに1を設定し、CPU interfaceブロックを有効化します。

対象ハードウェアの割り込みを有効化

対象の割り込みIDを有効化、優先度および割り込み通知先のCPUコアを設定します。

どのハードウェアが割り込みID何番なのかというのはチップ毎に異なるため、ユーザマニュアル等から調べる必要があります。目的の割り込みIDが分かったら、GICD_ISENABLERn (nは複数個ある。例えば0から31まで。割り込みID個数分存在する) の対象ビットに設定します。LSB側から順番にID=0, ID=1, ..., ID=31のように対応しているため、以下のようにmodを取る感じで設定することが可能です。

GICD_ISENABLER[id / 32] |= 1 << (id % 32);

次に、割り込みの優先度をGICD_IPRIORITYRnレジスタに設定します。1つの割り込みあたり、0-255(8ビット)を設定することが可能で、値が小さいほど優先度が高いことを示します。ビット割り当てもLSB側からID=0となり、GICD_ISENABLERnと同じ感じです(1IDあたり8ビットですが)。

最後に、割り込み通知先のCPUコアを設定していきます。なお、1つの割り込みを複数コアに通知することも可能です。優先度設定同様、1つのIDあたり8ビットがLSB側から割り当てられているため、目的のコア番号(0-255) を設定します。

4) 割り込み発生時の処理

実際割り込みが発生してから、EL2でやることそしてEL1でやることを説明していきます。なお、コンテキストスイッチを考慮して、Lower EL using AArch64のケースを前提とします。

  1. EL1からEL2にコンテキストスイッチする(ハイパーバイザーで動く)ため、必要なレジスタ(EL1で動作していた関連するレジスタも全て含む)を全てスタックに保存する
  2. 割り込み要因を確認
  3. 必要な処理を行う(ハードウェアのエミュレーション)
  4. EL1へvIRQ/vFIQ発行
  5. 割り込みをクリア
  6. No1.で保存したデータを全て復元し、EL2からEL1にコンテキストスイッチ
  7. eret命令で割り込みベクタ終了

割り込み要因の確認

GICC_IARレジスタの下位10ビットが割り込みIDを示しています。

割り込みをクリア

GICC_EOIRレジスタに先ほどリードしたであろうGICC_IARレジスタの値をそのままライトするだけです。

EL1へvIRQ/vFIQ発行

HCR_EL2VI (7ビット目)、VF (6ビット目) に1をセットすることで発行することが可能です。HCR_EL2レジスタの詳細についてはこちらもどうぞ。

https://zenn.dev/hidenori3/articles/1c33e080cd643e

.globl assert_virq_el2
assert_virq_el2:
  mrs x0, hcr_el2
  orr x1, x0, #0x80
  msr hcr_el2, x1
  ret

Discussion