VMIについて調査した事を以下に記述する。

VMIとは透過的な準仮想化インターフェイスと公式サイトで説明されています。http://www.vmware.com/jp/interfaces/paravirtualization.html VMIはparavirt-opsの一つの実装なので、VMIを説明する前にparavirt-opsについて説明します。

paravirt-ops

paravirt-opsLinux 2.6.20(x86)で導入された準仮想化を行うための関数の表です。この関数の表にはタイマ、割り込み、特権命令(センシティブな命令を含む)、MMU(ページテーブル)の設定、スピンロックなどが記述されいます。paravirt-opsは準仮想化を行う上でのスタブと考えてもいいかもしれません。このスタブの受け側はVMMで処理され現在のところXenおよびVMwareに実装されています。

具体的な関数の表は以下のようになっています。以下の例ではコントロールレジスタに読み書きする表が記述されています。

struct pv_mmu_ops pv_mmu_ops = {
	/* 省略 */
	.read_cr2 = native_read_cr2,
	.write_cr2 = native_write_cr2,
	.read_cr3 = native_read_cr3,
	.write_cr3 = native_write_cr3,
	/* 省略 */
}

paravirt-opsの欠点として入出力を行う関数の表が抜けています。

VMI(Virtual Machine Interface)

VMIはVMware用のparavirt-opsの一つ実装です。http://www.vmware.com/jp/interfaces/techpreview.html VMIの特徴してparavirt-opsに含まれていない入出力命令を行う関数の表が含まれています。
VMIのコードはLinuxカーネル(x86)にマージされているので、VMIを有効にしてコンパイルするだけで準仮想化を行う事ができます。

VMIのコードは現在のところ4つのファイルから構成されております。

  • linux/arch/x86/kernel/vmi_32.c
    • 割り込み、特権命令(センシティブな命令を含む)、MMU(ページテーブル)の設定のスタブ
  • linux/arch/x86/kernel/vmiclock_32.c
    • タイマのスタブ
  • linux/include/asm-x86/vmi.h
  • linux/include/asm-x86/vmi_time.h

タイマの準仮想ドライバが実装されているのはかなりすごいですね、またVMIの仕様書を見る限りロックの準仮想の実装はないようです。

具体的なコードはparavirt-opsの関数の表をVMware用に関数のポインタを上書きしています。para_fillはマクロであり第二引数のハイパーバイザコールを第一引数のparavirt-opsの関数の表のエントリに上書きしています。

/* linux-2.6.27-rc2/arch/x86/kernel/vmi_32.c */
static inline int __init activate_vmi(void)
{
	/* 省略 */
	para_fill(pv_cpu_ops.read_cr0, GetCR0);
	para_fill(pv_mmu_ops.read_cr2, GetCR2);
	para_fill(pv_mmu_ops.read_cr3, GetCR3);
	para_fill(pv_cpu_ops.read_cr4, GetCR4);
	para_fill(pv_cpu_ops.write_cr0, SetCR0);
	para_fill(pv_mmu_ops.write_cr2, SetCR2);
	para_fill(pv_mmu_ops.write_cr3, SetCR3);
	para_fill(pv_cpu_ops.write_cr4, SetCR4);
	/* 省略 */
}

VMIROM

VMIROMはハイパーバイザコールの呼び出しを行う上での処理および、VMIのAPIを提供します。VMware社からVMIROMをダウンロードして展開すると以下のようなディレクトリ構成になっています。

vmirom
|-- COPYING
|-- Makefile
|-- fixRom
|   |-- fixRom
|   `-- fixRom.c
|-- geninfo
|   |-- Subdir.mk
|   |-- genasminfo.pl
|   |-- vmiGenInfo.c
|   `-- vmiGenInfo.h
|-- make
|   `-- vmm-genrom-ld.pl
|-- rom
|   |-- Subdir.mk
|   |-- apic.c
|   |-- cpu.c
|   |-- cpuasm.S
|   |-- hypercallInterface.h
|   |-- hypervisorCalls.h
|   |-- mmu.c
|   |-- oprom_defs.h
|   |-- paravirtualInterface.h
|   |-- pci_defs.h
|   |-- relocated.c
|   |-- romHeader.S
|   |-- stubs.S
|   |-- support.c
|   |-- vm_basic_defs.h
|   |-- vm_basic_types.h
|   |-- vmi.h
|   |-- vmiCalls.h
|   |-- vmiatomic.h
|   |-- x86_defs.h
|   `-- x86types.h
`-- vmi_spec.txt

VMIROMをコンパイルすると最終的にはvmi.elfが生成されます。vmi.elfは静的リンクライブラリであり、ハイパーバイザコールなど以外の関数を呼べないように公開するシンボルを制限しています。

VMIROMは以下のようにビルドされます。

  1. gcc -fPICオプションで全てのvmirom/rom以下のCファイルをコンパイル
  2. gcc -o fixRom fixRom.c
  3. ar rcs vmirom/rom/*.o vmi.a
    • vmi.aの公開しているシンボルは615行 (nm vmi.a | wc)
  4. vmm-genrom-ld.pl でvmi.ldのリンカスクリプトを作成
  5. ld vmi.a vmi.elf -T vmi.ld
    • vmi.aの公開しているシンボルは220行 (nm vmi.elf | wc)
  6. fixRom -o vmi.elf

このようにしてVMIROMはゲストカーネルの関数名が重ならないよう公開するシンボルを制限して実装されています。

VMIROMの初期化およびVMIとVMIROMの関係

ゲストカーネルのVMIROMのロードは以下のコードのようにして行います。まず、VMMがゲストのメモリ空間の0xc0000から0xe0000の間(128KB)にVMIROMを保存します。ゲストカーネルが起動時にVMIROMを見つけます。

/* linux-2.6.27-rc2/arch/x86/kernel/vmi_32.c */
static inline int __init probe_vmi_rom(void)
{
        unsigned long base;

        /* VMI ROM is in option ROM area, check signature */
        for (base = 0xC0000; base < 0xE0000; base += 2048) {
                struct vrom_header *romstart;
                romstart = (struct vrom_header *)isa_bus_to_virt(base);
                if (check_vmi_rom(romstart)) {
                        vmi_rom = romstart; /* オフセットの先頭 */
                        return 1;
                }
        }
        return 0;
}

VMIの関数の呼び出しは、VMIROMのオフセットから関数呼び出しをしています。

/* linux-2.6.27-rc2/arch/x86/kernel/vmi_32.c */
static void *vmi_get_function(int vmicall)
{
        u64 reloc;
        const struct vmi_relocation_info *rel = (struct vmi_relocation_info *)&reloc;
        reloc = call_vrom_long_func(vmi_rom, get_reloc, vmicall); /* オフセットを元に関数呼び出し */
        BUG_ON(rel->type == VMI_RELOCATION_JUMP_REL);
        if (rel->type == VMI_RELOCATION_CALL_REL)
                return (void *)rel->eip;
        else
                return NULL;
}

VMIROMとゲストカーネルを静的リンクするのではなく、起動時にリンクする考えはすばらしい考えだと思います。
自分の研究で使用している仮想計算機でも同様な技術があるのですが静的リンクで行っているので見習いたいと思います。