KVM menjadi tuan rumah dalam beberapa baris kode

Halo!



Hari ini kami menerbitkan artikel tentang cara menulis host KVM. Kami melihatnya di blog Serge Zaitsev , menerjemahkan dan melengkapinya dengan contoh kami sendiri dengan Python untuk mereka yang tidak bekerja dengan C ++.


KVM (Mesin Virtual berbasis Kernel) adalah teknologi virtualisasi yang disertakan dengan kernel Linux. Dengan kata lain, KVM memungkinkan Anda menjalankan beberapa mesin virtual (VM) pada satu host virtual Linux. Mesin virtual dalam hal ini disebut tamu. Jika Anda pernah menggunakan QEMU atau VirtualBox di Linux, Anda tahu apa yang mampu dilakukan KVM.



Tapi bagaimana cara kerjanya di bawah tenda?



IOCTL



KVM mengekspos API melalui file perangkat khusus / dev / kvm . Saat Anda memulai perangkat, Anda mengakses subsistem KVM dan kemudian membuat panggilan sistem ioctl untuk mengalokasikan sumber daya dan memulai mesin virtual. Beberapa panggilan ioctl mengembalikan deskriptor file, yang juga dapat dimanipulasi dengan ioctl. Dan seterusnya ad infinitum? Tidak juga. Hanya ada beberapa level API di KVM:



  • tingkat / dev / kvm yang digunakan untuk mengelola seluruh subsistem KVM dan untuk membuat mesin virtual baru,
  • lapisan VM yang digunakan untuk mengelola mesin virtual individu,
  • tingkat VCPU yang digunakan untuk mengontrol pengoperasian satu prosesor virtual (satu mesin virtual dapat berjalan pada beberapa prosesor virtual) - VCPU.


Selain itu, ada API untuk perangkat I / O.



Mari kita lihat bagaimana tampilannya dalam praktiknya.



// KVM layer
int kvm_fd = open("/dev/kvm", O_RDWR);
int version = ioctl(kvm_fd, KVM_GET_API_VERSION, 0);
printf("KVM version: %d\n", version);

// Create VM
int vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);

// Create VM Memory
#define RAM_SIZE 0x10000
void *mem = mmap(NULL, RAM_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
struct kvm_userspace_memory_region mem = {
	.slot = 0,
	.guest_phys_addr = 0,
	.memory_size = RAM_SIZE,
	.userspace_addr = (uintptr_t) mem,
};
ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &mem);

// Create VCPU
int vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);


Contoh Python:



with open('/dev/kvm', 'wb+') as kvm_fd:
    # KVM layer
    version = ioctl(kvm_fd, KVM_GET_API_VERSION, 0)
    if version != 12:
        print(f'Unsupported version: {version}')
        sys.exit(1)

    # Create VM
    vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0)

    # Create VM Memory
    mem = mmap(-1, RAM_SIZE, MAP_PRIVATE | MAP_ANONYMOUS, PROT_READ | PROT_WRITE)
    pmem = ctypes.c_uint.from_buffer(mem)
    mem_region = UserspaceMemoryRegion(slot=0, flags=0,
                                       guest_phys_addr=0, memory_size=RAM_SIZE,
                                       userspace_addr=ctypes.addressof(pmem))
    ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, mem_region)

    # Create VCPU
    vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);


Pada langkah ini, kami telah membuat mesin virtual baru, mengalokasikan memori untuknya, dan menetapkan satu vCPU. Agar mesin virtual kita benar-benar menjalankan sesuatu, kita perlu memuat gambar mesin virtual dan mengkonfigurasi register prosesor dengan benar.



Memuat mesin virtual



Cukup mudah! Cukup baca file dan salin isinya ke memori mesin virtual. Tentu saja, mmap juga merupakan pilihan yang bagus.



int bin_fd = open("guest.bin", O_RDONLY);
if (bin_fd < 0) {
	fprintf(stderr, "can not open binary file: %d\n", errno);
	return 1;
}
char *p = (char *)ram_start;
for (;;) {
	int r = read(bin_fd, p, 4096);
	if (r <= 0) {
		break;
	}
	p += r;
}
close(bin_fd);


Contoh Python:



    # Read guest.bin
    guest_bin = load_guestbin('guest.bin')
    mem[:len(guest_bin)] = guest_bin


Diasumsikan bahwa guest.bin berisi kode byte yang valid untuk arsitektur CPU saat ini, karena KVM tidak menafsirkan instruksi CPU, satu per satu, seperti yang dilakukan mesin virtual lama. KVM memberikan kalkulasi ke CPU sebenarnya dan hanya memotong I / O. Inilah sebabnya mengapa mesin virtual modern bekerja dengan kinerja tinggi, mendekati logam biasa, kecuali Anda menjalankan operasi berat I / O.



Berikut adalah kernel mesin virtual tamu kecil yang akan kami coba jalankan terlebih dahulu: Jika Anda tidak terbiasa dengan assembler, contoh di atas adalah executable 16-bit kecil yang menambah register dalam satu loop dan mengeluarkan nilai ke port 0x10.



#

# Build it:

#

# as -32 guest.S -o guest.o

# ld -m elf_i386 --oformat binary -N -e _start -Ttext 0x10000 -o guest guest.o

#

.globl _start

.code16

_start:

xorw %ax, %ax

loop:

out %ax, $0x10

inc %ax

jmp loop








Kami sengaja menyusunnya sebagai aplikasi kuno 16-bit, karena prosesor virtual KVM yang diluncurkan dapat beroperasi dalam beberapa mode, sama seperti prosesor x86 asli. Mode paling sederhana adalah mode "nyata", yang telah digunakan untuk menjalankan kode 16-bit sejak abad terakhir. Mode nyata berbeda dalam pengalamatan memori, ini langsung daripada menggunakan tabel deskriptor - akan lebih mudah untuk menginisialisasi register kami untuk mode nyata:



struct kvm_sregs sregs;
ioctl(vcpu_fd, KVM_GET_SREGS, &sregs);
// Initialize selector and base with zeros
sregs.cs.selector = sregs.cs.base = sregs.ss.selector = sregs.ss.base = sregs.ds.selector = sregs.ds.base = sregs.es.selector = sregs.es.base = sregs.fs.selector = sregs.fs.base = sregs.gs.selector = 0;
// Save special registers
ioctl(vcpu_fd, KVM_SET_SREGS, &sregs);

// Initialize and save normal registers
struct kvm_regs regs;
regs.rflags = 2; // bit 1 must always be set to 1 in EFLAGS and RFLAGS
regs.rip = 0; // our code runs from address 0
ioctl(vcpu_fd, KVM_SET_REGS, &regs);


Contoh Python:



    sregs = Sregs()
    ioctl(vcpu_fd, KVM_GET_SREGS, sregs)
    # Initialize selector and base with zeros
    sregs.cs.selector = sregs.cs.base = sregs.ss.selector = sregs.ss.base = sregs.ds.selector = sregs.ds.base = sregs.es.selector = sregs.es.base = sregs.fs.selector = sregs.fs.base = sregs.gs.selector = 0
    # Save special registers
    ioctl(vcpu_fd, KVM_SET_SREGS, sregs)

    # Initialize and save normal registers
    regs = Regs()
    regs.rflags = 2  # bit 1 must always be set to 1 in EFLAGS and RFLAGS
    regs.rip = 0  # our code runs from address 0
    ioctl(vcpu_fd, KVM_SET_REGS, regs)


Lari



Kode dimuat, register siap. Mari kita mulai? Untuk memulai mesin virtual, kita perlu mendapatkan pointer ke "run state" untuk setiap vCPU dan kemudian memasuki loop di mana mesin virtual akan berjalan hingga diinterupsi oleh I / O atau lainnya operasi di mana kontrol akan ditransfer kembali ke host.



int runsz = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
struct kvm_run *run = (struct kvm_run *) mmap(NULL, runsz, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu_fd, 0);

for (;;) {
	ioctl(vcpu_fd, KVM_RUN, 0);
	switch (run->exit_reason) {
	case KVM_EXIT_IO:
		printf("IO port: %x, data: %x\n", run->io.port, *(int *)((char *)(run) + run->io.data_offset));
		break;
	case KVM_EXIT_SHUTDOWN:
		return;
	}
}


Contoh Python:



    runsz = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, 0)
    run_buf = mmap(vcpu_fd, runsz, MAP_SHARED, PROT_READ | PROT_WRITE)
    run = Run.from_buffer(run_buf)

    try:
        while True:
            ret = ioctl(vcpu_fd, KVM_RUN, 0)
            if ret < 0:
                print('KVM_RUN failed')
                return
             if run.exit_reason == KVM_EXIT_IO:
                print(f'IO port: {run.io.port}, data: {run_buf[run.io.data_offset]}')
             elif run.exit_reason == KVM_EXIT_SHUTDOWN:
                return
              time.sleep(1)
    except KeyboardInterrupt:
        pass


Sekarang jika kita menjalankan aplikasinya, kita akan melihat: Bekerja! Kode sumber lengkap tersedia di alamat berikut (jika Anda melihat kesalahan, komentar dipersilakan!).



IO port: 10, data: 0

IO port: 10, data: 1

IO port: 10, data: 2

IO port: 10, data: 3

IO port: 10, data: 4

...








Apakah Anda menyebutnya inti?



Kemungkinan besar, semua ini tidak terlalu mengesankan. Bagaimana jika menjalankan kernel Linux?



Awalnya akan sama: buka / dev / kvm , buat mesin virtual, dll. Namun, kami memerlukan beberapa panggilan ioctl lagi di level mesin virtual untuk menambahkan timer interval berkala, menginisialisasi TSS (diperlukan untuk chip Intel) dan menambahkan pengontrol interupsi:



ioctl(vm_fd, KVM_SET_TSS_ADDR, 0xffffd000);
uint64_t map_addr = 0xffffc000;
ioctl(vm_fd, KVM_SET_IDENTITY_MAP_ADDR, &map_addr);
ioctl(vm_fd, KVM_CREATE_IRQCHIP, 0);
struct kvm_pit_config pit = { .flags = 0 };
ioctl(vm_fd, KVM_CREATE_PIT2, &pit);


Kami juga perlu mengubah cara kami menginisialisasi register. Kernel Linux membutuhkan mode terlindungi, jadi kami mengaktifkannya di tanda register dan menginisialisasi basis, selektor, perincian untuk setiap kasus khusus:



sregs.cs.base = 0;
sregs.cs.limit = ~0;
sregs.cs.g = 1;

sregs.ds.base = 0;
sregs.ds.limit = ~0;
sregs.ds.g = 1;

sregs.fs.base = 0;
sregs.fs.limit = ~0;
sregs.fs.g = 1;

sregs.gs.base = 0;
sregs.gs.limit = ~0;
sregs.gs.g = 1;

sregs.es.base = 0;
sregs.es.limit = ~0;
sregs.es.g = 1;

sregs.ss.base = 0;
sregs.ss.limit = ~0;
sregs.ss.g = 1;

sregs.cs.db = 1;
sregs.ss.db = 1;
sregs.cr0 |= 1; // enable protected mode

regs.rflags = 2;
regs.rip = 0x100000; // This is where our kernel code starts
regs.rsi = 0x10000; // This is where our boot parameters start


Apa saja parameter boot dan mengapa Anda tidak bisa mem-boot kernel di alamat nol? Saatnya mempelajari lebih lanjut tentang format bzImage.



Image kernel mengikuti "protokol boot" khusus di mana ada header tetap dengan opsi boot diikuti oleh bytecode kernel yang sebenarnya. Format dari boot header dijelaskan di sini .



Memuat gambar kernel



Untuk memuat image kernel dengan benar ke mesin virtual, kita perlu membaca seluruh file bzImage terlebih dahulu. Kami melihat offset 0x1f1 dan mendapatkan jumlah sektor penyiapan dari sana. Kami akan melewatkannya untuk melihat di mana kode kernel dimulai. Selain itu, kami akan menyalin parameter boot dari awal bzImage ke area memori untuk parameter boot mesin virtual (0x10000).



Tapi itu pun tidak akan cukup. Kita perlu memperbaiki parameter boot untuk VM kita untuk memaksanya ke mode VGA dan menginisialisasi penunjuk baris perintah.



Kernel kita perlu log ke ttyS0 agar kita dapat mencegat I / O dan mesin virtual kita mencetaknya ke stdout. Untuk melakukan ini, kita perlu menambahkan "console = ttyS0" ke baris perintah kernel.



Tetapi bahkan setelah itu, kami tidak akan mendapatkan hasil apa pun. Saya harus menetapkan id cpu palsu untuk kernel kami (https://www.kernel.org/doc/Documentation/virtual/kvm/cpuid.txt). Kemungkinan besar, kernel yang saya kumpulkan mengandalkan informasi ini untuk menentukan apakah itu berjalan di dalam hypervisor atau pada logam kosong.



Saya menggunakan kernel yang dikompilasi dengan konfigurasi "kecil" dan menyiapkan beberapa flag konfigurasi untuk mendukung terminal dan virtio (kerangka kerja virtualisasi I / O untuk Linux).



Kode lengkap untuk host KVM yang dimodifikasi dan image kernel uji tersedia di sini .



Jika gambar ini tidak dimulai, Anda dapat menggunakan gambar lain yang tersedia di tautan ini .


Jika kita mengkompilasinya dan menjalankannya, kita mendapatkan output berikut:



Linux version 5.4.39 (serge@melete) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~16.04~ppa1)) #12 Fri May 8 16:04:00 CEST 2020
Command line: console=ttyS0
Intel Spectre v2 broken microcode detected; disabling Speculation Control
Disabled fast string operations
x86/fpu: Supporting XSAVE feature 0x001: 'x87 floating point registers'
x86/fpu: Supporting XSAVE feature 0x002: 'SSE registers'
x86/fpu: Supporting XSAVE feature 0x004: 'AVX registers'
x86/fpu: xstate_offset[2]:  576, xstate_sizes[2]:  256
x86/fpu: Enabled xstate features 0x7, context size is 832 bytes, using 'standard' format.
BIOS-provided physical RAM map:
BIOS-88: [mem 0x0000000000000000-0x000000000009efff] usable
BIOS-88: [mem 0x0000000000100000-0x00000000030fffff] usable
NX (Execute Disable) protection: active
tsc: Fast TSC calibration using PIT
tsc: Detected 2594.055 MHz processor
last_pfn = 0x3100 max_arch_pfn = 0x400000000
x86/PAT: Configuration [0-7]: WB  WT  UC- UC  WB  WT  UC- UC
Using GB pages for direct mapping
Zone ranges:
  DMA32    [mem 0x0000000000001000-0x00000000030fffff]
  Normal   empty
Movable zone start for each node
Early memory node ranges
  node   0: [mem 0x0000000000001000-0x000000000009efff]
  node   0: [mem 0x0000000000100000-0x00000000030fffff]
Zeroed struct page in unavailable ranges: 20322 pages
Initmem setup node 0 [mem 0x0000000000001000-0x00000000030fffff]
[mem 0x03100000-0xffffffff] available for PCI devices
clocksource: refined-jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645519600211568 ns
Built 1 zonelists, mobility grouping on.  Total pages: 12253
Kernel command line: console=ttyS0
Dentry cache hash table entries: 8192 (order: 4, 65536 bytes, linear)
Inode-cache hash table entries: 4096 (order: 3, 32768 bytes, linear)
mem auto-init: stack:off, heap alloc:off, heap free:off
Memory: 37216K/49784K available (4097K kernel code, 292K rwdata, 244K rodata, 832K init, 916K bss, 12568K reserved, 0K cma-reserved)
Kernel/User page tables isolation: enabled
NR_IRQS: 4352, nr_irqs: 24, preallocated irqs: 16
Console: colour VGA+ 142x228
printk: console [ttyS0] enabled
APIC: ACPI MADT or MP tables are not detected
APIC: Switch to virtual wire mode setup with no configuration
Not enabling interrupt remapping due to skipped IO-APIC setup
clocksource: tsc-early: mask: 0xffffffffffffffff max_cycles: 0x25644bd94a2, max_idle_ns: 440795207645 ns
Calibrating delay loop (skipped), value calculated using timer frequency.. 5188.11 BogoMIPS (lpj=10376220)
pid_max: default: 4096 minimum: 301
Mount-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
Disabled fast string operations
Last level iTLB entries: 4KB 64, 2MB 8, 4MB 8
Last level dTLB entries: 4KB 64, 2MB 0, 4MB 0, 1GB 4
CPU: Intel 06/3d (family: 0x6, model: 0x3d, stepping: 0x4)
Spectre V1 : Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Spectre V2 : Spectre mitigation: kernel not compiled with retpoline; no mitigation available!
Speculative Store Bypass: Vulnerable
TAA: Mitigation: Clear CPU buffers
MDS: Mitigation: Clear CPU buffers
Performance Events: Broadwell events, 16-deep LBR, Intel PMU driver.
...


Jelas, ini masih merupakan hasil yang tidak berguna: tidak ada partisi initrd atau root, tidak ada aplikasi nyata yang dapat berjalan di kernel ini, tetapi ini masih membuktikan bahwa KVM bukanlah alat yang buruk dan cukup kuat.



Kesimpulan



Untuk menjalankan Linux yang lengkap, host mesin virtual harus jauh lebih canggih - kita perlu mensimulasikan beberapa driver I / O untuk disk, keyboard, grafik. Tetapi pendekatan umumnya tetap sama, misalnya kita perlu mengkonfigurasi parameter baris perintah untuk initrd dengan cara yang sama. Disk tersebut perlu mencegat I / O dan merespons dengan tepat.



Namun, tidak ada yang memaksa Anda untuk menggunakan KVM secara langsung. Ada libvirt , perpustakaan ramah yang bagus untuk teknologi virtualisasi tingkat rendah seperti KVM atau BHyve.



Jika Anda tertarik untuk mempelajari lebih lanjut tentang KVM, saya sarankan untuk melihat sumber kvmtool . Mereka jauh lebih mudah dibaca daripada QEMU dan keseluruhan proyek jauh lebih kecil dan sederhana.



Semoga Anda menikmati artikelnya.



Anda dapat mengikuti berita di Github , Twitter, atau berlangganan melalui rss .



Tautan ke GitHub Gist dengan contoh Python dari pakar Timeweb: (1) dan (2) .



All Articles