👻

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

2022/09/24に公開約3,900字

1. はじめに

ハイパーバイザーの実装に必要なARM64の割り込みの仮想化についてまとめています。割り込み部分に特化した内容なのでご注意を。

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

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

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

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

例外はあるものの、ARM64を搭載しているシステムで最も一般的な割り込みコントローラであるGIC (ARM Generic Interrupt Controller) を前提に説明します。今回は、ラズパイ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) 割り込みベクターテーブルの設定

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

初期設定

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

ログインするとコメントできます