AMD 向けハイパーバイザ YmirC を自作した際にゲストの外部割り込みをどのように実装したかを紹介する。あくまで一例。リポジトリはこちら: gingk1212/ymirc

設計は例によって Writing Hypervisor in Zig (割り込みの注入) に沿っている。やりたいことはホストとゲストの両者が外部割り込みを受け取ることができるようにすることであり、そのために以下のような設計となっている。

  • ゲスト実行中に外部割り込みが発生した場合は #VMEXIT してホストの割り込みハンドラを実行させる
  • 割り込みハンドラ処理の中でゲストに注入すべき IRQ 番号を保存しておく
  • VMRUN を実行する前に割り込みに関する情報を VMCB に書き込んでおくことで外部割り込みをゲストに注入する

それでは、これらを実現するために AMD-V/SVM の機能を使ってどのように実装したかについて説明していく。まず、VMCB の初期設定として以下のフィールドのビットを立てておく。

  vmcb->intercept_intr = 1;
  vmcb->v_intr_masking = 1;
  vmcb->v_ign_tpr = 1;

これらのフィールドの説明に入る前に以降使用する用語を整理しておく。AMD64 Architecture Programmer’s Manual (以下、APM) に倣って、物理的な外部割り込みを physical INTR、ホストからゲストに注入された仮想的な外部割り込みを virtual INTR と呼ぶこととする。まず、INTERCEPT_INTR は physical INTR が発生した際に #VMEXIT を発生させるかどうかを設定するフィールド。当然 #VMEXIT させたいので 1 をセットしている。なお、発生した physical INTR がマスクされていた場合は #VMEXIT は発生しない。

V_INTR_MASKING は EFLAGS.IF の影響をコントロールするためのフィールドであり、次のような意味となる。

  • V_INTR_MASKING set to 0:
    • ゲストの EFLAGS.IF により virtual/physical INTR を受け付けるかどうかが制御される
  • V_INTR_MASKING set to 1:
    • ゲスト実行中であっても、ホストの EFLAGS.IF により physical INTR を受け付けるかどうかが制御される
    • ゲストの EFLAGS.IF は virtual INTR のみが制御対象となる

先述した通りホストとゲストの両者が外部割り込みを受け取るようにしたいので、たとえゲスト実行中かつゲストが EFLAGS.IF を 0 にセットしていたとしても外部割り込みが発生した場合はホストに制御を移したい。そのため、V_INTR_MASKING は 1 をセットしている。そのうえで、VMRUN する際はホストの EFLAGS.IF は必ずセットしておく必要がある。

最後に、V_IGN_TPR はゲストで TPR を無視するかどうかを設定するフィールド。YmirC では APIC ではなく PIC を使用しており TPR を使用してほしくないため無視させている。

次に、ゲスト実行中に physical INTR が発生して #VMEXIT してきた際にホスト側で実行されるコード (を一部抜粋したもの) がこちら。

  case SVM_EXIT_CODE_INTR:
    __asm__ volatile("stgi; clgi");
    inject_ext_intr(vcpu);
    break;

インラインアセンブラで実行している STGI, CLGI 命令の説明をするには、Global Interrupt Flag (GIF) の説明をしておく必要がある。GIF とは割り込みやその他イベントを制御するためのフラグであり、セットされている場合は割り込みなどがいつも通り発生するが、クリアされている場合はセットされるまで pending される。STGI 命令によりセット、CLGI 命令によりクリアできる。また、VMRUN 実行時にセットされ、#VMEXIT が発生した際にクリアされる。これについて APM には atomic なホスト/ゲスト間のスイッチを可能にするため、と書いてある。VMRUN 直前は割り込みが発生すると困るので GIF はクリアされているだろうから裏でセットしてくれる、#VMEXIT 直後も同じく割り込みが発生すると困るであろうから裏でクリアしてくれる、ということだと思っている。

話を戻すと、physical INTR が発生して #VMEXIT によりホストに戻ってきた際も当然 GIF はクリアされているので、上記のコードでは STGI を実行して割り込みを一時的に受け付けるようにしている。こうすることで #VMEXIT が発生する原因となった physical INTR をホスト側で処理することができる。そして、すぐに CLGI により GIF をクリアしておく。

無事ホストは外部割り込みを処理することができたので、次はゲストへ割り込みを注入する。inject_ext_intr() 関数の中身を一部抜粋したものがこちら。

void inject_ext_intr(Vcpu *vcpu) {
  ...
  vcpu->vmcb->v_irq = 1;
  vcpu->vmcb->v_intr_vector = vector;
  ...

VMCB の V_IRQ をセットしておくことでゲストへ virtual INTR を注入することができる。V_INTR_VECTOR は注入する割り込みのベクタを指定するためのフィールド。これらをセットしたうえで VMRUN を実行すると、ゲスト側で割り込みハンドラが実行され割り込みが処理される。なお、上記の処理の前にゲスト側で EFLAGS.IF がセットされているか、指定するベクタの割り込みがマスクされていないかをチェックしており、ともに yes の場合のみ各値をセットするようにしている。

ゲスト側で割り込みが処理された場合は V_IRQ がクリアされるため、ホストに処理が戻ってきた際にこのフィールドを確認することで注入した virtual INTR が処理済みかどうかを知ることができる。