Mewz (WebAssembly x Unikernel) を libkrun で動かしてみた - 外部記憶装置 の詳細を記すシリーズ
- その1 libkrun を試す
- その2 libkrun の構造
- その3 Mewz 追加実装(Linux zeropage, kernel cmd params) ← この記事
- その4 Mewz 追加実装(Virtio MMIO)
- その5 Mewz on libkrun してみた
- その6 Mewz 追加実装(virtio-console)
- その7 virtio-vsock について
- その8 Mewz 追加実装(virtio-vsock)
- その9 TSI の仕組み
目次
ビルドスクリプトの整備
Mewz kernel の libkrunfw 互換な共有ライブラリへの変換
その2 で、libkrun は libkrunfw を利用することを確認した。 libkrun で Mewz を動かすためには、Mewz kernel のビルド後、適切に libkrunfw 互換な共有ライブラリへ変換する必要がある。
公式の手順に従いビルドされた kernel (mewz.qemu.elf
) を libkrunfw が提供する bin2cbundle.py
で変換する形で対応した。
実装としては、以下のコミットに対応する。
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
ブランチにある。
Mewz への追加実装
Linux zeropage への対応
zeropage への対応としては、主に利用可能なメモリ領域の取得と cmd params のポインタの取得を行う。
実装としては、0x7000
に存在する zeropage を読み取れば良い。
対応するコミットは以下である。
メモリの初期化において、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 に含まれるため、それを読み取りパースすればよい。 対応するコミットは以下である。
以下のように、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 デバイスのサポートを無効化するオプションを追加した。
その他の対応: シャットダウン
Mewz のシャットダウンでは、IO Port 0x501 に対して書き込みを行う。
これは QEMU の起動時に isa-debug-exit,iobase=0x501,iosize=2
のオプションを指定することで、該当ポートへの書き込み時にシャットダウンを行う設定となっているためである。
しかし、libkrun では同様の機能は提供されず、代わりに i8042 の IO Port (0x64) への書き込みをもって自身を停止させるようになっている。
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 を書き込みことで終了させるようにした。
ここまでの動作確認
一旦ここまでの動作を確認する。
$ 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
について正しく実行できていることが分かる。