エンジニアなら知っておきたい仮想マシンのしくみ

第6回プロセッサの仮想化をソースから知る[その1]

前回は、x86プロセッサの仮想化支援機能(Intel VT、AMD-V)について、その仕組みを紹介しました。

今回は、オープンソースの仮想マシンソフトウェアであるLinux KVMのソースコードを読み、仮想マシンソフトウェアがどのようにIntel VTやAMD-Vを利用してプロセッサを仮想化しているか、具体的に追っていきたいと思います。

Linux KVMのソースコード構成

Linux KVMは、Linux向けのカーネルモジュールとして、Linuxカーネルにマージされています。最新の開発版について興味があれば、Linux KVMの開発サイトから入手することをお勧めします。今回はLinux 2.6.38.2のソースツリーに含まれるLinux KVMのソースコードを基に解説します。

Linux KVMのソースコード ディレクトリ

Linux KVMのカーネルモジュールは、Linuxカーネルのソースコードツリーのうち、以下のディレクトリに収録されています。
virt/kvm/ ――アーキテクチャ非依存コード
Linux KVMの実装部分のうち、APIなど、ユーザモードから見える部分については、このディレクトリ以下に収録されています。たとえば、/dev/kvmデバイスのインターフェース実装などが含まれます。
arch/*/kvm/ ――各アーキテクチャ向けコード

仮想マシン機能を実現するために、x86などの各アーキテクチャによって実装が異なるハードウェア固有部分は、こちらのディレクトリ以下に、各アーキテクチャごとに収録されています。たとえばx86プロセッサ向けのコードはarch/x86/kvm/に見つかります。

Linux KVMはおもにx86で利用できますが、実際にはその他のアーキテクチャにも対応しています。現時点でLinux KVMがポートされているアーキテクチャについて整理すると、下記のとおりとなっています。

  • x86(x86アーキテクチャ 32ビット, 64ビット)
  • ia64(IA-64アーキテクチャ)
  • s390(S/390アーキテクチャ)
  • ppc(Power PCアーキテクチャ)

x86プロセッサ向け仮想化支援機能の抽象化

x86プロセッサに限って、ハードウェア固有部分には、さらにもうひとつのレイヤが存在します。これまで紹介してきたように、Linux KVMはIntel VT、およびAMD-Vの両方に対応しています。しかし両者には実装上の互換性がないため、別々に対応しなければなりません。

このため、Intel VT搭載機向けのコード、AMD-V搭載機向けのコードは別々のソースファイルに分離されています。そして、コンパイル時には、それぞれのソースファイルからkvm-intel.koおよびkvm-amd.koと2つカーネルモジュールが作られます。Linux KVMのロード時には、Linux KVM本体(kvm.ko)に加え、プロセッサの種別からkvm-intel.koもしくはkvm-amd.koのどちらかが併せてロードされる仕組みとなっています。

もっとも、Intel VTとAMD-Vの間で互換性がないといっても、これらのコードの目的はどちらも⁠x86プロセッサの仮想化⁠です。このため、実際には⁠どちらにも通用するコード⁠が存在し、この部分が切り出されたのがx86.cです。各仮想化支援機能は、x86.cはvmx.cやsvm.cのアダプタとしての役割を持っています。もし、将来、x86プロセッサ向けの新しいプロセッサ仮想化手法が登場した暁には、このインターフェースを踏襲する形で、ソースファイルの追加によっての対応も可能でしょう。

表1 おもなx86プロセッサの仮想化支援対応コード

ソースコード説明
x86.cx86アーキテクチャサポート(Intel VT/AMD-V対応)
vmx.cIntel VTサポート
svm.cAMD-Vサポート

Linux KVMからのIntel VTの利用

では、Linux KVMカーネルモジュールが具体的にどのような処理を行っているのか、vmx.cの内容を読んでいきましょう。

VMCSの生成

Intel VT-xでは、論理プロセッサを実行するためにVMCSが必要です。Linux KVMは、仮想マシンに対してvcpu(仮想CPU)が作成されたタイミングで、システムメモリ上にVMCS領域を生成します。仮想マシンが2つの vcpuを持つ場合、vcpuごとに1つ、合計2つのVMCSが生成されます。プロセス、仮想マシン、プロセッサの数を図で表すと図1のような関係になります。

図1 Linux KVMにおけるインスタンス、VMCSの数量関係
図1 Linux KVMにおけるインスタンス、VMCSの数量関係

VMCSは、システムメモリ上のページから割り当てて使います。ページとは、プロセッサが管理するメモリ空間の単位で、x86プロセッサの場合は4KBである場合が殆どです。Linux KVMは、Linuxカーネル本体のメモリ管理ルーチンよりシステムメモリの割り当てを受け、VMCSとして内容を初期化します。

arch/x86/kvm/vmx.c
1569 static struct vmcs *alloc_vmcs_cpu(int cpu)
1570 {
1571         int node = cpu_to_node(cpu);
1572         struct page *pages;
1573         struct vmcs *vmcs;
1574
1575         pages = alloc_pages_exact_node(node, GFP_KERNEL, vmcs_config.order);
1576         if (!pages)
1577                 return NULL;
1578         vmcs = page_address(pages);
1579         memset(vmcs, 0, vmcs_config.size);
1580         vmcs->revision_id = vmcs_config.revision_id; /* vmcs revision id */
1581         return vmcs;
1582 }

さらに、作成されたVMCSは必要に応じてレジスタの初期値などが設定されます。また、ファイルとして保存されていた仮想マシンを復元する場合、マイグレーション機能により別の環境から移ってきた仮想マシンの場合は、その論理プロセッサの状態を復元する必要もあるでしょう。

論理プロセッサの実行

プロセッサの初期化が終わったとして、Linux KVMがIntel VT-xの論理プロセッサを用いて仮想マシンを実行する部分について読み進めていきましょう。

論理プロセッサへのモード遷移を行うにあたっての最初の処理は、Linux KVMを含む⁠Linux自体⁠のステートをシステムメモリ上に保存することです。保存されたステートは、仮想マシンから復帰した後に、プロセッサの状態を元に戻してLinux自体の実行を再開すために使われます。

セグメントレジスタなど一部のレジスタはIntel VT-xの機能を用いてステート保存しますが、それで全てではありません。Intel VT-xが保存しないものの、論理プロセッサから戻った後に必要なレジスタ値は、仮想マシンソフトウェア自身で保存しなければなりません。

論理プロセッサのステートは、VMCS内のほか、Linux KVMがvcpu->arch.regs[]で管理しています。論理プロセッサにモード遷移する前には、ここから論理プロセッサ上のレジスタをロードます。また、論理プロセッサから戻る際には、その時のレジスタ値で構造体を更新し、次の論理プロセッサへのモード遷移に備えます。

Intel VT-xを使った論理プロセッサへのモード遷移処理はvmx_vcpu_run()関数にあります。モード遷移の前後は、プロセッサのレジスタ値のレベルで細かくコントロール必要がありC言語でのプログラム記述が難しいことから、モード遷移前後の部分はインラインアセンブラにて記述されています。

arch/x86/kvm/vmx.c
3965 static void vmx_vcpu_run(struct kvm_vcpu *vcpu)
3966 {
3967         struct vcpu_vmx *vmx = to_vmx(vcpu);
  《中略》
3991         asm(
3992                 /* Store host registers */
3993                 "push %%"R"dx; push %%"R"bp;"
3994                 "push %%"R"cx \n\t"
3995                 "cmp %%"R"sp, %c[host_rsp](%0) \n\t"
3996                 "je 1f \n\t"
3997                 "mov %%"R"sp, %c[host_rsp](%0) \n\t"
3998                 __ex(ASM_VMX_VMWRITE_RSP_RDX) "\n\t"
3999                 "1: \n\t"
4000                 /* Reload cr2 if changed */
4001                 "mov %c[cr2](%0), %%"R"ax \n\t"
4002                 "mov %%cr2, %%"R"dx \n\t"
4003                 "cmp %%"R"ax, %%"R"dx \n\t"
4004                 "je 2f \n\t"
4005                 "mov %%"R"ax, %%cr2 \n\t"
4006                 "2: \n\t"
4007                 /* Check if vmlaunch of vmresume is needed */
4008                 "cmpl $0, %c[launched](%0) \n\t"
4009                 /* Load guest registers.  Don't clobber flags. */
4010                 "mov %c[rax](%0), %%"R"ax \n\t"
4011                 "mov %c[rbx](%0), %%"R"bx \n\t"
4012                 "mov %c[rdx](%0), %%"R"dx \n\t"
4013                 "mov %c[rsi](%0), %%"R"si \n\t"
4014                 "mov %c[rdi](%0), %%"R"di \n\t"
4015                 "mov %c[rbp](%0), %%"R"bp \n\t"
4016 #ifdef CONFIG_X86_64
4017                 "mov %c[r8](%0),  %%r8  \n\t"
4018                 "mov %c[r9](%0),  %%r9  \n\t"
4019                 "mov %c[r10](%0), %%r10 \n\t"
4020                 "mov %c[r11](%0), %%r11 \n\t"
4021                 "mov %c[r12](%0), %%r12 \n\t"
4022                 "mov %c[r13](%0), %%r13 \n\t"
4023                 "mov %c[r14](%0), %%r14 \n\t"
4024                 "mov %c[r15](%0), %%r15 \n\t"
4025 #endif
4026                 "mov %c[rcx](%0), %%"R"cx \n\t" /* kills %0 (ecx) */
4027

準備ができたらVMRESUME命令(初回のみVMLAUNCH命令)を実行し、論理プロセッサにモード遷移します。

4028                 /* Enter guest mode */
4029                 "jne .Llaunched \n\t"
4030                 __ex(ASM_VMX_VMLAUNCH) "\n\t"
4031                 "jmp .Lkvm_vmx_return \n\t"
4032                 ".Llaunched: " __ex(ASM_VMX_VMRESUME) "\n\t"

何らかの理由により論理プロセッサの実行が終了すると、VMRESUME命令(VMLAUNCH命令)の次の命令から実行が再開されます。論理プロセッサから戻った後に行う最初の処理は、論理プロセッサのステート保存です。

4033                 ".Lkvm_vmx_return: "
4034                 /* Save guest registers, load host registers, keep flags */
4035                 "xchg %0,     (%%"R"sp) \n\t"
4036                 "mov %%"R"ax, %c[rax](%0) \n\t"
4037                 "mov %%"R"bx, %c[rbx](%0) \n\t"
4038                 "push"Q" (%%"R"sp); pop"Q" %c[rcx](%0) \n\t"
4039                 "mov %%"R"dx, %c[rdx](%0) \n\t"
4040                 "mov %%"R"si, %c[rsi](%0) \n\t"
4041                 "mov %%"R"di, %c[rdi](%0) \n\t"
4042                 "mov %%"R"bp, %c[rbp](%0) \n\t"
4043 #ifdef CONFIG_X86_64
4044                 "mov %%r8,  %c[r8](%0) \n\t"
4045                 "mov %%r9,  %c[r9](%0) \n\t"
4046                 "mov %%r10, %c[r10](%0) \n\t"
4047                 "mov %%r11, %c[r11](%0) \n\t"
4048                 "mov %%r12, %c[r12](%0) \n\t"
4049                 "mov %%r13, %c[r13](%0) \n\t"
4050                 "mov %%r14, %c[r14](%0) \n\t"
4051                 "mov %%r15, %c[r15](%0) \n\t"
4052 #endif
4053                 "mov %%cr2, %%"R"ax   \n\t"
4054                 "mov %%"R"ax, %c[cr2](%0) \n\t"
4055 
4056                 "pop  %%"R"bp; pop  %%"R"bp; pop  %%"R"dx \n\t"
4057                 "setbe %c[fail](%0) \n\t"
4058               : : "c"(vmx), "d"((unsigned long)HOST_RSP),
4059                 [launched]"i"(offsetof(struct vcpu_vmx, launched)),
4060                 [fail]"i"(offsetof(struct vcpu_vmx, fail)),
4061                 [host_rsp]"i"(offsetof(struct vcpu_vmx, host_rsp)),
4062                 [rax]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RAX])),
4063                 [rbx]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RBX])),
4064                 [rcx]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RCX])),
4065                 [rdx]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RDX])),
4066                 [rsi]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RSI])),
4067                 [rdi]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RDI])),
4068                 [rbp]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RBP])),
4069 #ifdef CONFIG_X86_64
4070                 [r8]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R8])),
4071                 [r9]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R9])),
4072                 [r10]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R10])),
4073                 [r11]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R11])),
4074                 [r12]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R12])),
4075                 [r13]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R13])),
4076                 [r14]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R14])),
4077                 [r15]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R15])),
4078 #endif
4079                 [cr2]"i"(offsetof(struct vcpu_vmx, vcpu.arch.cr2))
4080               : "cc", "memory"
4081                 , R"ax", R"bx", R"di", R"si"
4082 #ifdef CONFIG_X86_64
4083                 , "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"
4084 #endif
4085               );

論理プロセッサの実行を終えた後の処理として注目したいのが、プロセッサがVMCSに設定したVMEXITの原因コードをVMCSから読み取っている点です。この値は、後にLinux KVM内部、およびユーザモードプロセスであるqemu-kvmにより参照され、VMEXITの理由を判断して仮想マシンソフトウェアとしての処理(メモリの割り当てやデバイスI/Oのエミュレーション等)を進める際に使われます。

4097         vmx->exit_reason = vmcs_read32(VM_EXIT_REASON);
4098         vmx->exit_intr_info = vmcs_read32(VM_EXIT_INTR_INFO);
  《中略》
4103 }

VMEXIT後のイベントディスパッチ

次に、vmx_handle_exit()関数を見て見ましょう。ここには、論理プロセッサから戻った後に、論理プロセッサが停止した理由をチェックし必要な処理を行うこと――すなわち、trap-and-emulate処理のうち、センシティブ命令をトラップし、論理プロセッサの停止理由に従って適切なイベントハンドラを呼び出す機能があります。

arch/x86/kvm/vmx.c
3756 /*
3757  * The guest has exited.  See if we can fix it or if we need userspace
3758  * assistance.
3759  */
3760 static int vmx_handle_exit(struct kvm_vcpu *vcpu)
3761 {
3762         struct vcpu_vmx *vmx = to_vmx(vcpu);
3763         u32 exit_reason = vmx->exit_reason;
3764         u32 vectoring_info = vmx->idt_vectoring_info;
  《中略》
3812         if (exit_reason < kvm_vmx_max_exit_handlers
3813             && kvm_vmx_exit_handlers[exit_reason])
3814                 return kvm_vmx_exit_handlers[exit_reason](vcpu);
3815         else {
3816                 vcpu->run->exit_reason = KVM_EXIT_UNKNOWN;
3817                 vcpu->run->hw.hardware_exit_reason = exit_reason;
3818         }
3819         return 0;
3820 }

実際のイベントハンドラはkvm_vmx_exit_handlers[]テーブルに登録されており、VMCSから読み出したVMEXITの理由番号を使って、このテーブルからルックアップします。たとえば、EXIT_REASON_EXCEPTION_NMIが発生した場合には、本表に従って handle_exception(vcpu)が呼び出されることになります。

arch/x86/kvm/vmx.c
3704 /*
3705  * The exit handlers return 1 if the exit was handled fully and guest execution
3706  * may resume.  Otherwise they set the kvm_run parameter to indicate what needs
3707  * to be done to userspace and return 0.
3708  */
3709 static int (*kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {
3710         [EXIT_REASON_EXCEPTION_NMI]           = handle_exception,
3711         [EXIT_REASON_EXTERNAL_INTERRUPT]      = handle_external_interrupt,
3712         [EXIT_REASON_TRIPLE_FAULT]            = handle_triple_fault,
3713         [EXIT_REASON_NMI_WINDOW]              = handle_nmi_window,
3714         [EXIT_REASON_IO_INSTRUCTION]          = handle_io,
3715         [EXIT_REASON_CR_ACCESS]               = handle_cr,
3716         [EXIT_REASON_DR_ACCESS]               = handle_dr,
3717         [EXIT_REASON_CPUID]                   = handle_cpuid,
3718         [EXIT_REASON_MSR_READ]                = handle_rdmsr,
3719         [EXIT_REASON_MSR_WRITE]               = handle_wrmsr,
3720         [EXIT_REASON_PENDING_INTERRUPT]       = handle_interrupt_window,
3721         [EXIT_REASON_HLT]                     = handle_halt,
3722         [EXIT_REASON_INVD]                    = handle_invd,
3723         [EXIT_REASON_INVLPG]                  = handle_invlpg,
  《中略》
3745 };

VMEXITハンドラの例 ――RDMSR/WRMSRのエミュレーション

Intel VT-xでは、論理プロセッサの実行中にセンシティブ命令に遭遇するとVMEXITを起こします。仮想マシンソフトウェアは、VMEXITの理由を確認し、必要なエミュレーションをしなければなりません。

ここでは、センシティブ命令の一例として、仮想マシンソフトウェアでエミュレーションが必要となるRDMSR命令/WRMSR命令に遭遇した際、Linux KVMがどのように命令をエミュレーションしているかを見ておきましょう。

MSRの読み取り(handle_rdmsr)

RDMSR命令は、Machine Specific Register (MSR)の設定値を汎用レジスタにロードするためのリング0の命令です。あらかじめECXレジスタを読み取りたいMSRの番号を設定しておきRDMSR 命令を実行すると、MSRの上位32ビット分がEDX、下位32ビット分がEAXにコピーされる、というものです。

Intel VT-xの論理プロセッサ上でこの命令が実行された場合には、handle_rdmsr()関数が呼び出されます。この関数では、論理プロセッサのECX の値が示すMSRレジスタの値を、KVMのMSR管理ルーチンから読み出しまて、取得されたMSRレジスタの値を、論理プロセッサのEAX, EDXに書き戻し、⁠次に実行すべき命令のアドレス」を指すインストラクションポインタ(IP)を、RDMSRの命令の長さだけ進める、という、命令エミュレーション処理を行っています。このため、次に論理プロセッサがが実行される際には、まるでRDMSRが実行された直後のような状態になっており、次の命令から実行されることになります。

arch/x86/kvm/vmx.c
3329 static int handle_rdmsr(struct kvm_vcpu *vcpu)
3330 {
3331         u32 ecx = vcpu->arch.regs[VCPU_REGS_RCX];
3332         u64 data;
3333 
3334         if (vmx_get_msr(vcpu, ecx, &data)) {
3335                 trace_kvm_msr_read_ex(ecx);
3336                 kvm_inject_gp(vcpu, 0);
3337                 return 1;
3338         }
3339 
3340         trace_kvm_msr_read(ecx, data);
3341 
3342         /* FIXME: handling of bits 32:63 of rax, rdx */
3343         vcpu->arch.regs[VCPU_REGS_RAX] = data & -1u;
3344         vcpu->arch.regs[VCPU_REGS_RDX] = (data >> 32) & -1u;
3345         skip_emulated_instruction(vcpu);
3346         return 1;
3347 }
MSRの書き込み(handle_wrmsr)
handle_rdmsr()の仕組みを理解した後であれば、handle_wrmsr()関数もすぐに理解できるでしょう。こちらはECXの値が指すMSRに対してEDX:EAXの64ビット値を書き込み、次の命令から論理プロセッサの実行を再開するという仕組みです。
arch/x86/kvm/vmx.c
3349 static int handle_wrmsr(struct kvm_vcpu *vcpu)
3350 {
3351         u32 ecx = vcpu->arch.regs[VCPU_REGS_RCX];
3352         u64 data = (vcpu->arch.regs[VCPU_REGS_RAX] & -1u)
3353                 | ((u64)(vcpu->arch.regs[VCPU_REGS_RDX] & -1u) << 32);
3354 
3355         if (vmx_set_msr(vcpu, ecx, data) != 0) {
3356                 trace_kvm_msr_write_ex(ecx, data);
3357                 kvm_inject_gp(vcpu, 0);
3358                 return 1;
3359         }
3360 
3361         trace_kvm_msr_write(ecx, data);
3362         skip_emulated_instruction(vcpu);
3363         return 1;
3364 }

ここまではLinux KVMのソースコードを読んできましたが、次回はLinux KVMの理解を深めるために、Linux KVMの呼び出し元であるqemu-kvmのソースコードも読んでみることにします。お楽しみに。

おすすめ記事

記事・ニュース一覧