مرحبا!
ننشر اليوم مقالًا حول كيفية كتابة مضيف KVM. لقد رأينا ذلك في مدونة Serge Zaitsev ، وقمنا بترجمتها واستكمالها بأمثلة Python الخاصة بنا لأولئك الذين لا يعملون مع C ++.
KVM (الآلة الافتراضية المستندة إلى Kernel) هي تقنية افتراضية تأتي مع Linux kernel. بمعنى آخر ، يسمح لك KVM بتشغيل عدة أجهزة افتراضية (VMs) على مضيف افتراضي واحد على نظام Linux. تسمى الأجهزة الافتراضية في هذه الحالة الضيوف. إذا سبق لك استخدام QEMU أو VirtualBox على Linux ، فأنت تعلم ما يمكن لـ KVM القيام به.
لكن كيف تعمل تحت الغطاء؟
IOCTL
يعرض KVM API من خلال ملف جهاز خاص / dev / kvm . عند بدء تشغيل جهاز ، يمكنك الوصول إلى النظام الفرعي KVM ثم إجراء مكالمات نظام ioctl لتخصيص الموارد وبدء تشغيل الأجهزة الافتراضية. تؤدي العديد من استدعاءات ioctl إلى إرجاع واصفات الملفات ، والتي يمكن معالجتها أيضًا باستخدام ioctls. وهكذا إلى ما لا نهاية؟ ليس صحيحا. لا يوجد سوى عدد قليل من مستويات API في KVM:
- مستوى / dev / kvm المستخدم لإدارة النظام الفرعي KVM بأكمله وإنشاء أجهزة افتراضية جديدة ،
- طبقة VM المستخدمة لإدارة جهاز افتراضي فردي ،
- يستخدم مستوى VCPU للتحكم في تشغيل معالج افتراضي واحد (يمكن تشغيل جهاز افتراضي واحد على عدة معالجات افتراضية) - VCPU.
بالإضافة إلى ذلك ، هناك واجهات برمجة تطبيقات لأجهزة الإدخال / الإخراج.
دعونا نرى كيف يبدو في الممارسة.
// 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);
مثال بايثون:
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);
مثال بايثون:
# Read guest.bin
guest_bin = load_guestbin('guest.bin')
mem[:len(guest_bin)] = guest_bin
من المفترض أن يحتوي guest.bin على رمز بايت صالح لبنية وحدة المعالجة المركزية الحالية ، لأن KVM لا يفسر تعليمات وحدة المعالجة المركزية ، واحدة تلو الأخرى ، كما فعل الجهاز الظاهري القديم. يعطي KVM الحساب لوحدة المعالجة المركزية الحقيقية ويعترض فقط I / O. هذا هو السبب في أن الأجهزة الافتراضية الحديثة تعمل بأداء عالٍ ، بالقرب من المعدن ، إلا إذا كنت تقوم بعمليات إدخال / إخراج ثقيلة.
إليك نواة الجهاز الظاهري للضيف الصغير الذي سنحاول تشغيله أولاً: إذا لم تكن معتادًا على المُجمِّع ، فإن المثال أعلاه عبارة عن ملف تنفيذي صغير بحجم 16 بت يزيد من التسجيل في حلقة ويخرج قيمة إلى المنفذ 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
لقد قمنا بتجميعه عن عمد باعتباره تطبيقًا قديمًا من 16 بت ، لأن المعالج الظاهري KVM الذي تم إطلاقه يمكن أن يعمل في عدة أوضاع ، تمامًا مثل معالج x86 الحقيقي. أبسط وضع هو الوضع "الحقيقي" ، والذي تم استخدامه لتشغيل كود 16 بت منذ القرن الماضي. يختلف الوضع الحقيقي في معالجة الذاكرة ، فهو مباشر بدلاً من استخدام جداول الوصف - سيكون من الأسهل تهيئة السجل الخاص بنا للوضع الحقيقي:
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, ®s);
مثال بايثون:
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)
ادارة
تم تحميل الكود والسجلات جاهزة. هيا بنا نبدأ؟ لبدء تشغيل جهاز افتراضي ، نحتاج إلى الحصول على مؤشر إلى "حالة التشغيل" لكل وحدة معالجة مركزية افتراضية ثم إدخال حلقة يتم فيها تشغيل الجهاز الظاهري حتى يتم مقاطعته بواسطة الإدخال / الإخراج أو غيره العمليات حيث سيتم نقل التحكم مرة أخرى إلى المضيف.
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;
}
}
مثال بايثون:
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
الآن إذا قمنا بتشغيل التطبيق ، فسنرى: Works! كود المصدر الكامل متاح على العنوان التالي (إذا لاحظت خطأ ، التعليقات مرحب بها!).
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 kernel بدلاً من ذلك؟
ستكون البداية هي نفسها: open / 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 kernel إلى الوضع المحمي ، لذلك نقوم بتمكينه في علامات التسجيل وتهيئة القاعدة والمحدد والدقة لكل حالة خاصة:
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
ما هي معلمات التمهيد ولماذا لا يمكنك فقط تمهيد النواة على العنوان صفر؟ حان الوقت لمعرفة المزيد عن تنسيق bzImage.
تتبع صورة kernel "بروتوكول تمهيد" خاص حيث يوجد رأس ثابت مع معلمات تمهيد متبوعة برمز kernel bytecode الفعلي. يتم وصف تنسيق رأس التمهيد هنا .
تحميل صورة النواة
لتحميل صورة kernel بشكل صحيح في الجهاز الظاهري ، نحتاج إلى قراءة ملف bzImage بأكمله أولاً. ننظر إلى الإزاحة 0x1f1 ونحصل على عدد قطاعات الإعداد من هناك. سنتخطىهم لنرى من أين يبدأ كود النواة. بالإضافة إلى ذلك ، سنقوم بنسخ معلمات التمهيد من بداية bzImage إلى منطقة الذاكرة لمعلمات التمهيد للجهاز الظاهري (0x10000).
لكن حتى هذا لن يكون كافيًا. سنحتاج إلى تصحيح معلمات التمهيد لجهازنا الظاهري لفرضه على وضع VGA وتهيئة مؤشر سطر الأوامر.
تحتاج النواة الخاصة بنا إلى تسجيل الدخول إلى ttyS0 حتى نتمكن من اعتراض الإدخال / الإخراج وطباعتها الآلة الافتراضية على stdout. للقيام بذلك ، نحتاج إلى إضافة "console = ttyS0" إلى سطر أوامر kernel.
لكن حتى بعد ذلك ، لن نحصل على أي نتيجة. اضطررت إلى تعيين معرف وحدة المعالجة المركزية وهمية لنواة لدينا (https://www.kernel.org/doc/Documentation/virtual/kvm/cpuid.txt). على الأرجح ، اعتمدت النواة التي جمعتها معًا على هذه المعلومات لتحديد ما إذا كانت تعمل داخل جهاز مراقبة أو على معدن مكشوف.
لقد استخدمت نواة تم تجميعها بتكوين "صغير" وقمت بإعداد عدد قليل من علامات التكوين لدعم Terminal و Virtio (إطار عمل المحاكاة الافتراضية I / O لنظام 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 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.
...
من الواضح أن هذه لا تزال نتيجة غير مفيدة إلى حد ما: لا يوجد قسم initrd أو قسم جذر ، ولا توجد تطبيقات حقيقية يمكن تشغيلها في هذه النواة ، لكنها لا تزال تثبت أن KVM ليست أداة رهيبة وقوية للغاية.
خاتمة
لتشغيل Linux كامل ، يجب أن يكون مضيف الجهاز الظاهري أكثر تقدمًا - نحتاج إلى تصميم العديد من برامج تشغيل الإدخال / الإخراج للأقراص ولوحة المفاتيح والرسومات. لكن النهج العام يظل كما هو ، على سبيل المثال ، نحتاج إلى تكوين معلمات سطر الأوامر لـ initrd بنفس الطريقة. ستحتاج الأقراص إلى اعتراض الإدخال / الإخراج والاستجابة بشكل مناسب.
ومع ذلك ، لا أحد يجبرك على استخدام KVM مباشرة. توجد مكتبة libvirt ، وهي مكتبة لطيفة ودودة لتقنيات المحاكاة الافتراضية منخفضة المستوى مثل KVM أو BHyve.
إذا كنت مهتمًا بمعرفة المزيد عن KVM ، أقترح البحث في مصدر kvmtool . إنها أسهل في القراءة من QEMU والمشروع بأكمله أصغر بكثير وأبسط.
ارجو ان تكون قد استمتعت بالمقال.
يمكنك متابعة الأخبار على جيثب ، تويتر أو الاشتراك عن طريق آر إس إس .
روابط إلى GitHub Gist مع أمثلة Python من خبير Timeweb: (1) و (2) .