Русский
Русский
English
Статистика
Реклама

Перевод - recovery mode Хост KVM в паре строчек кода

Привет!

Сегодня публикуем статью о том, как написать хост KVM. Мы увидели ее в блоге Serge Zaitsev, перевели и дополнили собственными примерами на Python для тех, кто не работает с языком С++.

KVM (Kernel-based Virtual Machine) это технология виртуализации, которая поставляется с ядром Linux. Другими словами, KVM позволяет запускать несколько виртуальных машин (VM) на одном виртуальном хосте Linux. Виртуальные машины в этом случае называются гостевыми (guests). Если вы когда-нибудь использовали QEMU или VirtualBox на Linux, вы знаете, на что способен KVM.

Но как это работает под капотом?

IOCTL


KVM предоставляет API через специальный файл устройства /dev/kvm. Запуская устройство, вы обращаетесь к подсистеме KVM, а затем выполняете системные вызовы ioctl для распределения ресурсов и запуска виртуальных машин. Некоторые вызовы ioctl возвращают файловые дескрипторы, которыми также можно управлять с помощью ioctl. И так до бесконечности? На самом деле, нет. В KVM всего несколько уровней API:

  • уровень /dev/kvm, используемый для управления всей подсистемой KVM и для создания новых виртуальных машин,
  • уровень VM, используемый для управления отдельной виртуальной машиной,
  • уровень VCPU, используемый для управления работой одного виртуального процессора (одна виртуальная машина может работать на нескольких виртуальных процессорах) VCPU.

Кроме того, существуют API для устройств ввода-вывода.

Посмотрим, как это выглядит на практике.

// KVM layerint kvm_fd = open("/dev/kvm", O_RDWR);int version = ioctl(kvm_fd, KVM_GET_API_VERSION, 0);printf("KVM version: %d\n", version);// Create VMint vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);// Create VM Memory#define RAM_SIZE 0x10000void *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 VCPUint vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);

Пример на 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);

На этом этапе мы создали новую виртуальную машину, выделили для нее память и назначили один виртуальный ЦП. Чтобы наша виртуальная машина действительно запускала что-то, нам нужно загрузить образ виртуальной машины и правильно настроить регистры процессора.

Загрузка виртуальной машины


Это достаточно легко! Просто прочтите файл и скопируйте его содержимое в память виртуальной машины. Конечно, mmap тоже неплохой вариант.

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);

Пример на Python:

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

Предполагается, что guest.bin содержит валидный байт-код для текущей архитектуры ЦП, потому что KVM не интерпретирует инструкции ЦП одну за другой, как это делали старые виртуальные машины. KVM отдает вычисления настоящему ЦП и только перехватывает ввод-вывод. Вот почему современные виртуальные машины работают с высокой производительностью, близкой к голому железу, если только вы не выполняете операции с большим количеством ввода-вывода (I/O heavy operations).

Вот крошечное ядро гостевой виртуальной машины, которое мы попробуем запустить в первую очередь:

#
# 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


Если вы не знакомы с ассемблером, то пример выше это крошечный 16-разрядный исполняемый файл, который увеличивает регистр в цикле и выводит значение в порт 0x10.

Мы сознательно скомпилировали его как архаичное 16-битное приложение, потому что запускаемый виртуальный процессор KVM может работать в нескольких режимах, как настоящий процессор x86. Самый простой режим это реальный режим (real mode), который использовался для запуска 16-битного кода с прошлого века. Реальный режим отличается адресацией памяти, она прямая вместо использования дескрипторных таблиц было бы проще инициализировать наш регистр для реального режима:

struct kvm_sregs sregs;ioctl(vcpu_fd, KVM_GET_SREGS, &sregs);// Initialize selector and base with zerossregs.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 registersioctl(vcpu_fd, KVM_SET_SREGS, &sregs);// Initialize and save normal registersstruct kvm_regs regs;regs.rflags = 2; // bit 1 must always be set to 1 in EFLAGS and RFLAGSregs.rip = 0; // our code runs from address 0ioctl(vcpu_fd, KVM_SET_REGS, &regs);

Пример на 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)

Запуск


Код загружен, регистры готовы. Приступим? Чтобы запустить виртуальную машину, нам нужно получить указатель на состояние выполнения (run state) для каждого виртуального ЦП, а затем войти в цикл, в котором виртуальная машина будет работать до тех пор, пока она не будет прервана операциями ввода-вывода или другими операциями, где управление будет передано обратно хосту.

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;}}

Пример на 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

Теперь, если мы запустим приложение, мы увидим:

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
...


Работает! Полные исходные коды доступны по следующему адресу (если вы заметили ошибку, комментарии приветствуются!).

Вы называете это ядром?


Скорее всего, всё это не очень впечатляет. Как насчет того, чтобы вместо этого запустить ядро Linux?

Начало будет таким же: откройте /dev/kvm, создайте виртуальную машину и т. д. Однако нам понадобится еще несколько вызовов ioctl на уровне виртуальной машины, чтобы добавить периодический интервальный таймер, инициализировать TSS (требуется для чипов Intel) и добавить контроллер прерываний:

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);

Также нам нужно будет изменить способ инициализации регистров. Ядру Linux требуется защищенный режим, поэтому мы включаем его во флагах регистра и инициализируем базу, селектор, степень детализации для каждого специального регистра:

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 moderegs.rflags = 2;regs.rip = 0x100000; // This is where our kernel code startsregs.rsi = 0x10000; // This is where our boot parameters start

Каковы параметры загрузки и почему нельзя просто загрузить ядро по нулевому адресу? Пришло время узнать больше о формате bzImage.

Образ ядра следует специальному протоколу загрузки, где есть фиксированный заголовок с параметрами загрузки, за которым следует фактический байт-код ядра. Здесь описан формат загрузочного заголовка.

Загрузка образа ядра


Чтобы правильно загрузить образ ядра в виртуальную машину, нам нужно сначала прочитать весь файл bzImage. Мы смотрим на смещение 0x1f1 и получаем оттуда количество секторов настройки. Мы пропустим их, чтобы узнать, где начинается код ядра. Кроме того, мы скопируем параметры загрузки из начала bzImage в область памяти для параметров загрузки виртуальной машины (0x10000).

Но даже этого будет недостаточно. Нам нужно будет исправить параметры загрузки для нашей виртуальной машины, чтобы принудительно перейти в режим VGA и инициализировать указатель командной строки.

Наше ядро должно выводить логи на ttyS0, чтобы мы могли перехватить ввод-вывод и наш виртуальный компьютер распечатал его на stdout. Для этого нам нужно добавить console = ttyS0 в командную строку ядра.

Но даже после этого мы не получим никакого результата. Мне пришлось установить поддельный идентификатор процессора для нашего ядра (http://personeltest.ru/aways/www.kernel.org/doc/Documentation/virtual/kvm/cpuid.txt). Скорее всего, ядро, которое я собрал, полагалось на эту информацию, чтобы определить, работает ли оно внутри гипервизора или на голом железе.

Я использовал ядро, скомпилированное с крошечной конфигурацией, и настроил несколько флагов конфигурации для поддержки терминала и virtio (фреймворк виртуализации ввода-вывода для Linux).

Полный код модифицированного хоста KVM и тестового образа ядра доступны здесь.

Если этот образ не запустился, можно использовать другой образ, доступный по данной ссылке.

Если мы скомпилируем его и запустим, мы получим следующий результат:

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 2020Command line: console=ttyS0Intel Spectre v2 broken microcode detected; disabling Speculation ControlDisabled fast string operationsx86/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]:  256x86/fpu: Enabled xstate features 0x7, context size is 832 bytes, using 'standard' format.BIOS-provided physical RAM map:BIOS-88: [mem 0x0000000000000000-0x000000000009efff] usableBIOS-88: [mem 0x0000000000100000-0x00000000030fffff] usableNX (Execute Disable) protection: activetsc: Fast TSC calibration using PITtsc: Detected 2594.055 MHz processorlast_pfn = 0x3100 max_arch_pfn = 0x400000000x86/PAT: Configuration [0-7]: WB  WT  UC- UC  WB  WT  UC- UCUsing GB pages for direct mappingZone ranges:  DMA32    [mem 0x0000000000001000-0x00000000030fffff]  Normal   emptyMovable zone start for each nodeEarly memory node ranges  node   0: [mem 0x0000000000001000-0x000000000009efff]  node   0: [mem 0x0000000000100000-0x00000000030fffff]Zeroed struct page in unavailable ranges: 20322 pagesInitmem setup node 0 [mem 0x0000000000001000-0x00000000030fffff][mem 0x03100000-0xffffffff] available for PCI devicesclocksource: refined-jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645519600211568 nsBuilt 1 zonelists, mobility grouping on.  Total pages: 12253Kernel command line: console=ttyS0Dentry 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:offMemory: 37216K/49784K available (4097K kernel code, 292K rwdata, 244K rodata, 832K init, 916K bss, 12568K reserved, 0K cma-reserved)Kernel/User page tables isolation: enabledNR_IRQS: 4352, nr_irqs: 24, preallocated irqs: 16Console: colour VGA+ 142x228printk: console [ttyS0] enabledAPIC: ACPI MADT or MP tables are not detectedAPIC: Switch to virtual wire mode setup with no configurationNot enabling interrupt remapping due to skipped IO-APIC setupclocksource: tsc-early: mask: 0xffffffffffffffff max_cycles: 0x25644bd94a2, max_idle_ns: 440795207645 nsCalibrating delay loop (skipped), value calculated using timer frequency.. 5188.11 BogoMIPS (lpj=10376220)pid_max: default: 4096 minimum: 301Mount-cache hash table entries: 512 (order: 0, 4096 bytes, linear)Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes, linear)Disabled fast string operationsLast level iTLB entries: 4KB 64, 2MB 8, 4MB 8Last level dTLB entries: 4KB 64, 2MB 0, 4MB 0, 1GB 4CPU: Intel 06/3d (family: 0x6, model: 0x3d, stepping: 0x4)Spectre V1 : Mitigation: usercopy/swapgs barriers and __user pointer sanitizationSpectre V2 : Spectre mitigation: kernel not compiled with retpoline; no mitigation available!Speculative Store Bypass: VulnerableTAA: Mitigation: Clear CPU buffersMDS: Mitigation: Clear CPU buffersPerformance Events: Broadwell events, 16-deep LBR, Intel PMU driver....

Очевидно, это по-прежнему довольно бесполезный результат: нет initrd или корневого раздела, нет реальных приложений, которые могли бы работать в этом ядре, но все же это доказывает, что KVM не такой уж страшный и довольно мощный инструмент.

Вывод


Чтобы запустить полноценный Linux, хост виртуальной машины должен быть намного более продвинутым нам нужно смоделировать несколько драйверов ввода-вывода для дисков, клавиатуры, графики. Но общий подход останется прежним, например, нам потребуется настроить параметры командной строки для initrd аналогичным образом. Для дисков нужно будет перехватывать ввод-вывод и отвечать должным образом.

Однако никто не заставляет вас использовать KVM напрямую. Существует libvirt, приятная дружественная библиотека для технологий низкоуровневой виртуализации, таких как KVM или BHyve.

Если вам интересно узнать больше о KVM, я предлагаю посмотреть исходники kvmtool. Их намного легче читать, чем QEMU, а весь проект намного меньше и проще.

Надеюсь, вам понравилась статья.

Вы можете следить за новостями на Github, в Twitter или подписываться через rss.

Ссылки на GitHub Gist с примерами на Python от эксперта Timeweb: (1) и (2).
Источник: habr.com
К списку статей
Опубликовано: 06.11.2020 20:04:29
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании timeweb

Виртуализация

Разработка под linux

Kvm

Linux

Vm

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru