外部記憶装置

外付け記憶装置

Mewz on libkrun - その3 Mewz 追加実装(Linux zeropage, kernel cmd params)

Mewz (WebAssembly x Unikernel) を libkrun で動かしてみた - 外部記憶装置 の詳細を記すシリーズ

目次

ビルドスクリプトの整備

Mewz kernel の libkrunfw 互換な共有ライブラリへの変換

その2 で、libkrun は libkrunfw を利用することを確認した。 libkrun で Mewz を動かすためには、Mewz kernel のビルド後、適切に libkrunfw 互換な共有ライブラリへ変換する必要がある。

公式の手順に従いビルドされた kernel (mewz.qemu.elf) を libkrunfw が提供する bin2cbundle.py で変換する形で対応した。

実装としては、以下のコミットに対応する。

github.com

zig のビルドスクリプト(build.zig) では、zig build libkrunfw と実行すると WebAssembly と kernel のリンク後、libkrunfw 互換の共有ライブラリへ変換するように処理を追加した。 実行すると、libkrunfw-mewz.so という名前で共有ライブラリが生成される。

const rewrite_kernel_cmd = b.addSystemCommand(&[_][]const u8{"./scripts/rewrite-kernel.sh"});
rewrite_kernel_cmd.step.dependOn(b.getInstallStep());
...    
const libkrunfw_cmd = b.addSystemCommand(&[_][]const u8{"./scripts/gen-libkrunfw.sh"});
libkrunfw_cmd.step.dependOn(&rewrite_kernel_cmd.step);
const libkrunfw_step = b.step("libkrunfw", "build libkrunfw compatible shared library");
libkrunfw_step.dependOn(&libkrunfw_cmd.step);

libkrun への対応

libkrun 側では、起動用の kernel の取得に libkrunfw-mewz.so を利用するようにする必要がある。 また、標準では Port IO 経由で利用するシリアルデバイスは接続されないため、Mewz を利用するときにはシリアルデバイスを接続する必要がある。

これらについては、libkrun の feature として mewz を追加し、mewz feature が有効になっているときには libkrunfw-mewz と Port IO 経由のシリアルデバイスを利用するようにした。 また、examples の chroot_vm についても同様に libkrunfw-mewz を利用するようにした。 これらの改変は以下の mewz-on-libkrun ブランチにある。

github.com

Mewz への追加実装

Linux zeropage への対応

zeropage への対応としては、主に利用可能なメモリ領域の取得と cmd params のポインタの取得を行う。 実装としては、0x7000 に存在する zeropage を読み取れば良い。 対応するコミットは以下である。

github.com

メモリの初期化において、zeropage が持つ E820 を順番に読み取り、利用可能な領域(E820 RAM) を multiboot プロトコルで起動したときと同様の手順で登録する。

pub fn initWithZeroPage(info: zeropage.ZeroPageInfo) void {
    const image_end_addr = @intFromPtr(&image_end);
    {
        // disable alignment checks for mmaps
        @setRuntimeSafety(false);
        var entry_idx: u8 = 0;
        while (entry_idx < info.e820_entry_num) {
            entry_idx += 1;
            const entry = info.e820_entries[entry_idx - 1];
            log.info.printf("E820 Entry [{}] addr=0x{x} size=0x{x} type={}\n", .{ entry_idx, entry.addr, entry.size, entry.type_ });
            if (entry.type_ == 1) { // E820 RAM
                // exclude the kernel image from available memory, because it's already used
                const base = @max(image_end_addr, entry.addr);
                const end = entry.addr + entry.size;
                if (end <= base) {
                    continue;
                }
                log.info.printf("available memory: {x} - {x}\n", .{ base, end });
                // align the range to BLOCK_SIZE
                const aligned_base = util.roundUp(usize, base, BLOCK_SIZE);
                const aligned_end = util.roundDown(usize, end, BLOCK_SIZE);
                const length = aligned_end - aligned_base;
                if (length >= MIN_BOOTTIME_ALLOCATOR_SIZE) {
                    // create a new allocator for the boot time
                    const buf = @as([*]u8, @ptrFromInt(aligned_base))[0..length];
                    boottime_fba = FixedBufferAllocator.init(buf);
                    boottime_allocator = boottime_fba.?.allocator();
                } else {
                    // add the range to the free list
                    initRange(aligned_base, length);
                }
            }
        }
    }
}

環境によっては multiboot プロトコルを使う可能性もあるため、Magic value を読み取り共存する形とした。

    if (boot_magic == 0x2badb002) {
        log.info.print("booted with multiboot1\n");
        const bootinfo = @as(*multiboot.BootInfo, @ptrFromInt(boot_params));
        printBootinfo(boot_magic, bootinfo);
        mem.init(bootinfo);
        param.parseFromArgs(util.getString(bootinfo.cmdline));
    } else {
        log.info.print("booted with linux zero page\n");
        const info = zeropage.parseZeroPageInfo(0x7000);
        mem.initWithZeroPage(info);
    }

kernel command-line parameters への対応

Virtio MMIO への対応をするために、cmd params の読み取り部分を拡張した。 cmd params のアドレスや長さは zeropage の setup_header に含まれるため、それを読み取りパースすればよい。 対応するコミットは以下である。

github.com

以下のように、zeropage の setup_header から文字列を切り出し、param.parseFromArgs 関数で処理する。

const cmd_params = @as([*]u8, @ptrFromInt(info.setup_header.cmd_line_ptr))[0..info.setup_header.cmdline_size];
param.parseFromArgs(cmd_params);

param.parseFromArgs 関数内では、virtio_mmio.device へ対応した。

        } else if (std.mem.eql(u8, k, "virtio_mmio.device")) {
            if (params.mmio_device_num >= params.mmio_devices.len) {
                log.warn.printf("too many virtio_mmio devices. '{s}' is ignored\n", .{v});
                continue;
            }
            // TODO: error handle
            // 4K@0xd0004000:9 SIZE@ADDR:IRQ
            var iter = std.mem.splitScalar(u8, v, '@');
            const size_str = iter.next() orelse continue;
            // TODO: support non 4KiB device
            if (!std.mem.eql(u8, size_str, "4K")) {
                log.warn.printf("vrtio_mmio: size {s} is not supported\n", .{size_str});
                continue;
            }
            // addr_with_irq = 0xd0004000:9
            const addr_with_irq = iter.next() orelse continue;
            // addr_str = 0xd0004000
            iter = std.mem.splitScalar(u8, addr_with_irq, ':');
            const addr_str = iter.next() orelse continue;
            const irq_str = iter.next() orelse continue;
            // addr_str contains prefix '0x', so base=0 is specified.
            const addr = std.fmt.parseInt(usize, addr_str, 0) catch |err| {
                log.warn.printf("failed to parse {s}: {}\n", .{ addr_str, err });
                continue;
            };
            const irq_line = std.fmt.parseInt(usize, irq_str, 10) catch |err| {
                log.warn.printf("failed to parse {s}: {}\n", .{ irq_str, err });
                continue;
            };
            params.mmio_devices[params.mmio_device_num] = virtio_mmio.MMIODeviceParam{
                .addr = addr,
                .size = 4096,
                .irq = irq_line,
            };
            params.mmio_device_num += 1;
        }

文字列としては virtio_mmio.device=4K@0xd0004000:9 のようなサイズ@アドレス:IRQ line 番号 という形で受け渡される。 これをパースし、Virtio MMIOバイスとして登録する。 実際のデバイスの認識処理については次回記述する。

その他の対応: PCIバイスの無効化

libkrun では PCIバイスを利用しないため、PCI バスのスキャンを行う必要はない。 また、本来は無効なデバイスについては VendorID を 0xFFFF とすべきところ、libkrun では 0 となっているため大量のデバイスが存在する判定となってしまう。 デバイスの過多による panic が発生するため、ビルド時に PCIバイスのサポートを無効化するオプションを追加した。

github.com

その他の対応: シャットダウン

Mewz のシャットダウンでは、IO Port 0x501 に対して書き込みを行う。 これは QEMU の起動時に isa-debug-exit,iobase=0x501,iosize=2 のオプションを指定することで、該当ポートへの書き込み時にシャットダウンを行う設定となっているためである。 しかし、libkrun では同様の機能は提供されず、代わりに i8042 の IO Port (0x64) への書き込みをもって自身を停止させるようになっている。

libkrun/src/devices/src/legacy/i8042.rs at b67a6a06a2c7b4534e9d67fcd9dba8626e4834d9 · containers/libkrun · GitHub

            OFS_STATUS if data[0] == CMD_RESET_CPU => {
                // The guest wants to assert the CPU reset line. We handle that by triggering
                // our exit event fd. Meaning Firecracker will be exiting as soon as the VMM
                // thread wakes up to handle this event.
                if let Err(e) = self.reset_evt.write(1) {
                    error!("Failed to trigger i8042 reset event: {:?}", e);
                }
            }

IO Port (0x501) への書き込み後終了しなかった場合については、 IO Port (0x64) へ 0xFE を書き込みことで終了させるようにした。

github.com

ここまでの動作確認

一旦ここまでの動作を確認する。

$ git clone https://github.com/containers/libkrunfw
$ git clone -b mewz-on-libkrun https://github.com/naoki9911/libkrun
$ git clone -b mewz-on-libkrun --recursive https://github.com/naoki9911/mewz
$ cd mewz
$ curl -o helloworld.wat https://raw.githubusercontent.com/Mewz-project/Wasker/main/helloworld.wat
$ wasker helloworld.wat
$ zig build -Dapp-obj=wasm.o -Dlog-level=info -Denable-pci=false libkrunfw
$ cd ../libkrun
$ make MEWZ=1
$ mv target/release/libkrun-mewz.so* lib/.
$ cd examples
$ make
$ sudo LD_LIBRARY_PATH=../lib ./chroot_vm dummy dummy
[LOG INFO]: booted with linux zero page
[LOG INFO]: zeropage: e820_entries=2
[LOG INFO]: E820 Entry [1] addr=0x0 size=0x9fc00 type=1
[LOG INFO]: E820 Entry [2] addr=0x100000 size=0x200a0001 type=1
[LOG INFO]: available memory: 144d7000 - 201a0001
[LOG INFO]: virtio_mmio device detected: addr=0xd0000000 size=0x1000 IRQ=5
[LOG INFO]: virtio_mmio device detected: addr=0xd0001000 size=0x1000 IRQ=6
[LOG INFO]: virtio_mmio device detected: addr=0xd0002000 size=0x1000 IRQ=7
[LOG INFO]: virtio_mmio device detected: addr=0xd0003000 size=0x1000 IRQ=8
[LOG INFO]: virtio_mmio device detected: addr=0xd0004000 size=0x1000 IRQ=9

Hello, wasker

このように、helloworld.wat について正しく実行できていることが分かる。