QEMU/KVM上のゲストのハイパーコールをQEMU側に渡す方法

前回説明したように,KVMでは,ゲストのハイパーコール(Intel CPUの場合はVMCALL命令,AMDの場合はVMMCALL命令)はKVM側で処理され,ioctl側に戻ることなくゲストに戻ります.

QEMU/KVMにおいて,独自にハイパーコールを追加してQEMU側で処理するには修正が必要です.

対象環境

方法

KVM

diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h
index fbda5a917c5b..fba797631a3f 100644
--- a/arch/x86/include/asm/kvm_host.h
+++ b/arch/x86/include/asm/kvm_host.h
@@ -78,6 +78,7 @@
 #define KVM_REQ_HV_STIMER              KVM_ARCH_REQ(22)
 #define KVM_REQ_LOAD_EOI_EXITMAP       KVM_ARCH_REQ(23)
 #define KVM_REQ_GET_VMCS12_PAGES       KVM_ARCH_REQ(24)
+#define KVM_REQ_HYPERCALL              KVM_ARCH_REQ(25)
 
 #define CR0_RESERVED_BITS                                               \
        (~(unsigned long)(X86_CR0_PE | X86_CR0_MP | X86_CR0_EM | X86_CR0_TS \
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index f049ecfac7bb..2accaba0e75a 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -7002,6 +7002,10 @@ int kvm_emulate_hypercall(struct kvm_vcpu *vcpu)
                ret = kvm_pv_send_ipi(vcpu->kvm, a0, a1, a2, a3, op_64_bit);
                break;
 #endif
+       case KVM_HC_MY_HYPERCALL:
+               kvm_make_request(KVM_REQ_HYPERCALL, vcpu);
+               ret = 0;
+               break;
        default:
                ret = -KVM_ENOSYS;
                break;
@@ -7624,6 +7628,11 @@ static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
                        r = 0;
                        goto out;
                }
+               if (kvm_check_request(KVM_REQ_HYPERCALL, vcpu)) {
+                       vcpu->run->exit_reason = KVM_EXIT_MY_HYPERCALL;
+                       r = 0;
+                       goto out;
+               }
 
                /*
                 * KVM_REQ_HV_STIMER has to be processed after
diff --git a/include/uapi/linux/kvm.h b/include/uapi/linux/kvm.h
index 2b7a652c9fa4..1c2707d5b70c 100644
--- a/include/uapi/linux/kvm.h
+++ b/include/uapi/linux/kvm.h
@@ -235,6 +235,7 @@ struct kvm_hyperv_exit {
 #define KVM_EXIT_S390_STSI        25
 #define KVM_EXIT_IOAPIC_EOI       26
 #define KVM_EXIT_HYPERV           27
+#define KVM_EXIT_MY_HYPERCALL     28
 
 /* For KVM_EXIT_INTERNAL_ERROR */
 /* Emulate instruction failed. */
diff --git a/include/uapi/linux/kvm_para.h b/include/uapi/linux/kvm_para.h
index 6c0ce49931e5..a6a0417ee029 100644
--- a/include/uapi/linux/kvm_para.h
+++ b/include/uapi/linux/kvm_para.h
@@ -28,6 +28,7 @@
 #define KVM_HC_MIPS_CONSOLE_OUTPUT     8
 #define KVM_HC_CLOCK_PAIRING           9
 #define KVM_HC_SEND_IPI                10
+#define KVM_HC_MY_HYPERCALL            11
 
 /*
  * hypercalls use architecture specific

QEMU

diff --git a/linux-headers/linux/kvm.h b/linux-headers/linux/kvm.h
index f11a7eb49c..0b44980eda 100644
--- a/linux-headers/linux/kvm.h
+++ b/linux-headers/linux/kvm.h
@@ -235,6 +235,7 @@ struct kvm_hyperv_exit {
 #define KVM_EXIT_S390_STSI        25
 #define KVM_EXIT_IOAPIC_EOI       26
 #define KVM_EXIT_HYPERV           27
+#define KVM_EXIT_MY_HYPERCALL     28
 
 /* For KVM_EXIT_INTERNAL_ERROR */
 /* Emulate instruction failed. */
diff --git a/target/i386/kvm.c b/target/i386/kvm.c
index b2401d13ea..685c3ac58e 100644
--- a/target/i386/kvm.c
+++ b/target/i386/kvm.c
@@ -3622,6 +3622,21 @@ int kvm_arch_handle_exit(CPUState *cs, struct kvm_run *run)
         ioapic_eoi_broadcast(run->eoi.vector);
         ret = 0;
         break;
+    case KVM_EXIT_MY_HYPERCALL:
+        ret = kvm_getput_regs(cpu, 0);
+        if (ret < 0) {
+            return ret;
+        }
+        fprintf(stderr, "KVM: KVM_EXIT_MY_HYPERCALL %d, %ld, %ld, %ld, %ld, %ld\n",
+                                                    run->exit_reason,
+                                                    cpu->env.regs[R_EAX],
+                                                    cpu->env.regs[R_EBX],
+                                                    cpu->env.regs[R_ECX],
+                                                    cpu->env.regs[R_EDX],
+                                                    cpu->env.regs[R_ESI]);
+        cpu->env.regs[R_EAX] = 0;
+        ret = kvm_getput_regs(cpu, 1);
+        break;
     default:
         fprintf(stderr, "KVM: unknown exit reason %d\n", run->exit_reason);
         ret = -1;

解説

QEMU側からkvm_vcpu_ioctl(cpu, KVM_RUN, 0)すると,いろいろあってKVMvcpu_enter_guest()にきます. この中のkvm_x86_ops->run(vcpu)でVMENTRY, kvm_x86_ops->handle_exit(vcpu)でVMEIXT後の処理がおこなわれます. ちなみにVMENTRYやVMEXITが関数ポインタになっているのはIntelAMD双方に対応するためです.

VMCALL命令でVMEXITした場合は,handle_vmcall()からkvm_emulate_hypercall()が呼ばれ,ここでハイパーコールが処理されます. vcpu_enter_guest()のコメントに書いてある通り,kvm_emulate_hypercall()の戻り値で1以外の値を返せばioctl側に制御が戻ります.そうしてもいいのですが,するとQEMU側で命令エミュレーション(RIPを進める処理)をおこなう必要があります.

今回はその代わりにKVMVCPU requestという機能を使っています. これはもともとはカーネルのスレッドがvCPU側に何かリクエストをするために使用する機能のようです. ハイパーコール処理の中で,自分が定義したハイパーコール番号だった場合,kvm_make_request()でリクエストの登録し,あとはこれまで通り命令のエミュレーション自体はKVMにまかせます.このままだとまたKVMはゲストにVMENTRYしますが,vcpu_enter_guest()の先頭の方でkvm_check_request()し,もし対応するリクエストが存在したらexit_reasonをその旨に書き換えてユーザランド側に戻るようにしています.

QEMUはioctlから戻ってきたら,kvm_arch_handle_exit() (accel/kvm/kvm-all.c => target/i386/kvm.c) でVMEXIT後の処理をするので,ここにハイパーコール用の処理を追加しています. QEMUからはkvm_getput_regs()を使ってゲストのレジスタを取得することが可能です.