仮想マシンとBIOSと準仮想化

はじめに

PCエミュレータや完全仮想化の仮想マシンの場合は、当然ながらBIOS(もしくはEFI)をエミュレーションする必要があります。BIOSが用意しなければならない情報には、例えばNUMAにおけるCPUやメモリの構成情報があります*1。物理マシンの場合は、基本的な構成は固定のため(BIOSでon/offやパラメタ変更はできますが)、それらの情報はBIOS ROMに固定値を書きこんでおくことができます。一方、仮想マシンの場合は、柔軟にマシン構成を変更するために、BIOSに固定値を持たせるのは好ましくありません。

高機能なPCエミュレータ仮想マシンでは、起動時にマシン構成をBIOSソフトウェアに渡す機能が備わっています。KVM(qemu)では、コマンドライン引数で渡された構成情報(例えば-smpなど)をBIOS(現在はSeaBIOS)ソフトウェアに渡すための機能を持っています。もちろんBIOSソフトウェア側も情報を受け取る仕組みを持っています。つまり、BIOSソフトウェアも準仮想化対応していると言えます。

今回はSeaBIOSがどうやってqemuから情報を受け取っているのか、当該ソースコードを追ってみたいと思います。

概要

  • SeaBIOSはI/Oポート経由でqemuからデータを受け取っている
  • コントロール用とデータ用のポートが用意されている
  • 扱うデータ毎に識別番号(とデータサイズ/形式)が決められている
  • 動作手順
    • qemu: 識別番号とそのデータのペアを登録
    • SeaBIOS: コントロールポートにデータ識別番号を書き込む
    • qemu: 書きこまれた識別番号を元にデータを用意する
    • SeaBIOS: データポートを読み込む
    • qemu: 用意したデータを返す

ポート番号、識別番号(とデータサイズ/形式)、動作手順がプロトコルになっている。

注意

実はこの辺りのことはあまり詳しくないので、間違っているかもしれません。ご注意ください。(ツッコミ歓迎です。)

調査対象

SeaBIOSのファイルリスト

まずはSeaBIOSのファイルリストを見てみましょう。

$ ls -1 src/
# 省略
paravirt.c
paravirt.h
# 省略
virtio-blk.c
virtio-blk.h
virtio-pci.c
virtio-pci.h
virtio-ring.c
virtio-ring.h

paravirtといういかにも準仮想化ぽい名前のファイルがあります。またvirtio向けのファイルもあります。どうやらSeaBIOSはvirtio-blkを読むことができるようです。確かによく考えるとvirtio-blkデバイスからブートするとき(MBRからブートローダを読み出すとき)には必要ですよね。

virtioの方は置いておいて、今回はparavirt方を見ていくことにします。

SeaBIOSの準仮想化コード

paravirt.cparavirt.hを見てみるとわかりますが、マクロや関数名のプリフィックスqemuになっています。完全にqemu向けに作られていますね。

このqemu_*関数を呼び出しているファイルは以下の通りです。

$ grep -r qemu_ src |cut -f 1 -d ' ' |cut -f 1 -d ':' |sort -u
src/acpi.c
src/boot.c
src/mptable.c
src/optionroms.c
src/paravirt.c
src/paravirt.h
src/post.c
src/shadow.c
src/smbios.c
src/smp.c
src/util.h

以下では、わかり易そうなsmp.c(CPU数関連)のコードを見ていきます。当該コードはこんな感じです。

    MaxCountCPUs = qemu_cfg_get_max_cpus();
    if (!MaxCountCPUs || MaxCountCPUs < CountCPUs)
        MaxCountCPUs = CountCPUs;

    dprintf(1, "Found %d cpu(s) max supported %d cpu(s)\n", readl(&CountCPUs),
        MaxCountCPUs);

どうやら最大CPU数を受け取っているようです。コードを見てみると、稼働中のCPUは別の場所でprobeしているみたいなので、空きソケットを含めたCPUソケット(もしくはコアの数)のことのようです。

今度はMaxCountCPUsでgrepしてみます。

$ grep -r MaxCountCPUs src
src/smp.c:u32 MaxCountCPUs VAR16VISIBLE;
src/smp.c:        MaxCountCPUs = 1;
src/smp.c:    MaxCountCPUs = qemu_cfg_get_max_cpus();
src/smp.c:    if (!MaxCountCPUs || MaxCountCPUs < CountCPUs)
src/smp.c:        MaxCountCPUs = CountCPUs;
src/smp.c:        MaxCountCPUs);
src/mptable.c:    for (i = 0; i < MaxCountCPUs; i+=pkgcpus) {
src/acpi.c:                     + sizeof(struct madt_processor_apic) * MaxCountCPUs
src/acpi.c:    for (i=0; i<MaxCountCPUs; i++) {
src/acpi.c:    int acpi_cpus = MaxCountCPUs > 0xff ? 0xff : MaxCountCPUs;
src/acpi.c:    u64 *numadata = malloc_tmphigh(sizeof(u64) * (MaxCountCPUs + nb_numa_nodes));
src/acpi.c:    qemu_cfg_get_numa_data(numadata, MaxCountCPUs + nb_numa_nodes);
src/acpi.c:        sizeof(struct srat_processor_affinity) * MaxCountCPUs +
src/acpi.c:    for (i = 0; i < MaxCountCPUs; ++i) {src/smbios.c:    for (cpu_num = 1; cpu_num <= MaxCountCPUs; cpu_num++)
src/util.h:extern u32 MaxCountCPUs;

MP-TableやACPIのデーブルを生成するときに使っているようです。

どう使われてるかは置いておいて、最大CPU数をどうやってqemuから受け取っているのか調べるため、qemu_cfg_get_max_cpus関数を見てみます。

u16 qemu_cfg_get_max_cpus(void)
{       
    u16 cnt;
        
    if (!qemu_cfg_present)
        return 0;
        
    qemu_cfg_read_entry(&cnt, QEMU_CFG_MAX_CPUS, sizeof(cnt));
        
    return cnt;
}       
static void
qemu_cfg_read_entry(void *buf, int e, int len)
{
    qemu_cfg_select(e);
    qemu_cfg_read(buf, len);
}
static void
qemu_cfg_select(u16 f)
{
    outw(f, PORT_QEMU_CFG_CTL);
}

static void
qemu_cfg_read(u8 *buf, int len)
{
    insb(PORT_QEMU_CFG_DATA, buf, len);
}

PORT_QEMU_CFG_CTL番号のI/OポートにQEMU_CFG_MAX_CPUSを書きこんだ後、PORT_QEMU_CFG_DATA番号のI/Oポートを読みだしています。どうやら、コントロールポートに欲しいデータの識別番号を書きこみ、その後データポートから当該データを(決められたサイズ分)読み出す、といったプロトコルqemuからデータを受け取っているようです*2

I/Oポート番号はioport.hに定義されています。

#define PORT_QEMU_CFG_CTL      0x0510
#define PORT_QEMU_CFG_DATA     0x0511

このポート番号をヒントにqemu側のコードを見つけられそうです。

qemu側のコード

qemu-kvmのコードをgrepしてみます。(0x0510では見つかりませんでした。。。)

$ grep -r 0x510 hw/
hw/fw_cfg.h:    uint16_t  select;      /* write this to 0x510 to read it */
hw/musicpal.c:#define MP_GPIO_IN_HI           0x510
hw/pc.c:#define BIOS_CFG_IOPORT 0x510
hw/omap2.c:    [ 32] = { 0x51000, 0x1000, 32 | 16 | 8 }, /* L4TA10 */
hw/sun4u.c:#define BIOS_CFG_IOPORT      0x510
hw/pl061.c:    case 0x510: /* Pull-up */
hw/pl061.c:    case 0x510: /* Pull-up */

hw/pc.cのマクロ定義が当たりのようです。

このポート番号はbochs_bios_init関数内*3で、fw_cfg_init関数に渡されています

    fw_cfg = fw_cfg_init(BIOS_CFG_IOPORT, BIOS_CFG_IOPORT + 1, 0, 0);

BIOS_CFG_IOPORT + 1でコントロールポート番号も渡していますね。

fw_cfg_init()@hw/fw_cfg.cの中身を見てみましょう。

    dev = qdev_create(NULL, "fw_cfg");
    qdev_prop_set_uint32(dev, "ctl_iobase", ctl_port);
    qdev_prop_set_uint32(dev, "data_iobase", data_port);
    qdev_init_nofail(dev);
    d = sysbus_from_qdev(dev);

    s = DO_UPCAST(FWCfgState, busdev.qdev, dev);
    // 省略

    fw_cfg_add_bytes(s, FW_CFG_SIGNATURE, (uint8_t *)"QEMU", 4);
    fw_cfg_add_bytes(s, FW_CFG_UUID, qemu_uuid, 16);
    fw_cfg_add_i16(s, FW_CFG_NOGRAPHIC, (uint16_t)(display_type == DT_NOGRAPHIC));
    fw_cfg_add_i16(s, FW_CFG_NB_CPUS, (uint16_t)smp_cpus);
    fw_cfg_add_i16(s, FW_CFG_MAX_CPUS, (uint16_t)max_cpus);
    fw_cfg_add_i16(s, FW_CFG_BOOT_MENU, (uint16_t)boot_menu);

FW_CFG_MAX_CPUSをキーにmax_cpusで最大CPU数が設定されているのがわかります。

fw_cfg_add_i16関数を追ってみます。

int fw_cfg_add_i16(FWCfgState *s, uint16_t key, uint16_t value)
{
    uint16_t *copy;

    copy = qemu_malloc(sizeof(value));
    *copy = cpu_to_le16(value);
    return fw_cfg_add_bytes(s, key, (uint8_t *)copy, sizeof(value));
}

int fw_cfg_add_bytes(FWCfgState *s, uint16_t key, uint8_t *data, uint32_t len)
{
    int arch = !!(key & FW_CFG_ARCH_LOCAL);

    key &= FW_CFG_ENTRY_MASK;

    if (key >= FW_CFG_MAX_ENTRY)
        return 0;

    s->entries[arch][key].data = data;
    s->entries[arch][key].len = len;

    return 1;
}

FWCfgState#entriesにI/Oポート番号とデータ、データサイズが設定されています。

このentriesにアクセスしているのは、fw_cfg_read関数です。(他にもありますが、関係ないので省略。)

static uint8_t fw_cfg_read(FWCfgState *s)
{
    int arch = !!(s->cur_entry & FW_CFG_ARCH_LOCAL);
    FWCfgEntry *e = &s->entries[arch][s->cur_entry & FW_CFG_ENTRY_MASK];
    uint8_t ret;

    if (s->cur_entry == FW_CFG_INVALID || !e->data || s->cur_offset >= e->len)
        ret = 0;
    else
        ret = e->data[s->cur_offset++];

    FW_CFG_DPRINTF("read %d\n", ret);

    return ret;
}

s->cur_entryが指すエントリのデータを取っているようです*4。s->cur_entryはfw_cfg_select関数で設定されています。

static int fw_cfg_select(FWCfgState *s, uint16_t key)
{
    int ret;

    s->cur_offset = 0;
    if ((key & FW_CFG_ENTRY_MASK) >= FW_CFG_MAX_ENTRY) {
        s->cur_entry = FW_CFG_INVALID;
        ret = 0;
    } else {
        s->cur_entry = key;
        ret = 1;
    }

    FW_CFG_DPRINTF("select key %d (%sfound)\n", key, ret ? "" : "not ");

    return ret;
}

fw_cfg_select関数はfw_cfg_io_writew関数から呼ばれています。SeaBIOSがoutwでコントロールポートにアクセスしていたので、どうやら合ってそうです。

ちなみにfw_cfg_read関数の方はfw_cfg_io_readbから呼ばれています。

この2つの関数はfw_cfg_init1関数でコールバックとして登録されています

    if (s->ctl_iobase) {
        register_ioport_write(s->ctl_iobase, 2, 2, fw_cfg_io_writew, s);
    }
    if (s->data_iobase) {
        register_ioport_read(s->data_iobase, 1, 1, fw_cfg_io_readb, s);
        register_ioport_write(s->data_iobase, 1, 1, fw_cfg_io_writeb, s);
    }

register_*がSeaBIOS(やゲストOS)でin/out命令が発行されたときに呼び出されるコールバックを登録する関数なのでしょう。

これでSeaBIOSとqemuの動作が繋がりました。

最後に、max_cpusについて調べてみます。qemu-options.hxの定義がわかり易そうです。

DEF("smp", HAS_ARG, QEMU_OPTION_smp,
"-smp n[,maxcpus=cpus][,cores=cores][,threads=threads][,sockets=sockets]\n"
" set the number of CPUs to 'n' [default=1]\n"
" maxcpus= maximum number of total cpus, including\n"
" offline CPUs for hotplug, etc\n"
" cores= number of CPU cores on one socket\n"
" threads= number of threads on one CPU core\n"
" sockets= number of discrete sockets in the system\n",
QEMU_ARCH_ALL)
STEXI
@item -smp @var{n}[,cores=@var{cores}][,threads=@var{threads}][,sockets=@var{sockets}][,maxcpus=@var{maxcpus}]
@findex -smp
Simulate an SMP system with @var{n} CPUs. On the PC target, up to 255
CPUs are supported. On Sparc32 target, Linux limits the number of usable CPUs
to 4.
For the PC target, the number of @var{cores} per socket, the number
of @var{threads} per cores and the total number of @var{sockets} can be
specified. Missing values will be computed. If any on the three values is
given, the total number of CPUs @var{n} can be omitted. @var{maxcpus}
specifies the maximum number of hotpluggable CPUs.
ETEXI

qemu -smp 2,maxcpus=4などとコマンドライン引数に指定すると、最大CPU数を設定できるようです。その数値が今まで追ってきた仕組みでSeaBIOSに渡されているのでしょう。

おわりに

というわけで、SeaBIOSがqemuからデータを受け取るコードを見てきました。分かってしまえば、やっていることは難しく無かったと思います。(むしろ受け取ったデータからACPIテーブルなどを生成する方が面倒そうです。)

ちなみに、このようなデータ受け渡しは、昔qemuが採用していたBochsOSやXenでもやられているようです。調べていないですが、VirtualBoxVMwareも同じようなことをやっているのではないかと推測します(違ってたら教えてください)。

*1:OSにはACPIテーブル経由で情報を渡します。

*2:ちなみにinsb関数のlen引数はinsb命令につけるrep接頭辞に渡すもので、指定回数命令を繰り返して指定サイズのデータを取り出しています。

*3:BochsBIOSを使っていた名残りでしょうか。

*4:cur_offset++しているので、(I/Oポートのデータバスサイズより)大きなデータを複数回のin命令で取り出せることがわかります。