پیاده‌سازی واحد فروشنده pKVM

این صفحه نحوه پیاده‌سازی ماژول فروشنده ماشین مجازی مبتنی بر هسته محافظت‌شده (pKVM) را توضیح می‌دهد.

برای android16-6.12 و نسخه‌های جدیدتر، وقتی این مراحل را انجام دادید، باید درخت دایرکتوری مشابه این داشته باشید:

BUILD.bazel
el1.c
hyp/
    BUILD.bazel
    el2.c

برای دیدن نمونه کامل، به ساختن واحد pKVM با DDK مراجعه کنید.

برای android15-6.6 و نسخه‌های قدیمی‌تر:

Makefile
el1.c
hyp/
    Makefile
    el2.c
  1. کد فراسرپرست EL2 (el2.c) را اضافه کنید. حداقل، این کد باید تابع init را که مرجعی به ساختار pkvm_module_ops را می‌پذیرد اعلام کند:

    #include <asm/kvm_pkvm_module.h>
    
    int pkvm_driver_hyp_init(const struct pkvm_module_ops *ops)
    {
      /* Init the EL2 code */
    
      return 0;
    }
    

    میانای برنامه‌سازی کاربردی واحد فروشنده pKVM ساختاری است که تماس‌های برگشتی را به سرپرست pKVM کپسوله می‌کند. این ساختار از همان قوانین ABI پیروی می‌کند که میاناهای GKI پیروی می‌کنند.

  2. برای ساختن کد سرپرست ماشین مجازی، hyp/Makefile را ایجاد کنید:

    hyp-obj-y := el2.o
    include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.module
    
  3. کد هسته EL1 (el1.c) را اضافه کنید. بخش init این کد باید حاوی فراخوانی برای pkvm_load_el2 module باشد تا کد سرپرست EL2 را از مرحله ۱ بار کند.

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <asm/kvm_pkvm_module.h>
    
    int __kvm_nvhe_pkvm_driver_hyp_init(const struct pkvm_module_ops *ops);
    
    static int __init pkvm_driver_init(void)
    {
        unsigned long token;
    
        return pkvm_load_el2_module(__kvm_nvhe_pkvm_driver_hyp_init, &token);
    }
    module_init(pkvm_driver_init);
    
  4. قوانین ساخت را ایجاد کنید.

    برای android16-6.12 و نسخه‌های جدیدتر، برای ایجاد ddk_library() برای EL2 و ddk_module() برای EL1، به ساختن واحد pKVM با DDK مراجعه کنید.

    برای android15-6.6 و نسخه‌های قدیمی‌تر، فایل ساخت ریشه را ایجاد کنید تا کد EL1 و EL2 را به‌هم پیوند دهد:

    ifneq ($(KERNELRELEASE),)
    clean-files := hyp/hyp.lds hyp/hyp-reloc.S
    
    obj-m := pkvm_module.o
    pkvm_module-y := el1.o hyp/kvm_nvhe.o
    
    $(PWD)/hyp/kvm_nvhe.o: FORCE
             $(Q)$(MAKE) $(build)=$(obj)/hyp $(obj)/hyp/kvm_nvhe.o
    else
    all:
            make -C $(KDIR) M=$(PWD) modules
    clean:
            make -C $(KDIR) M=$(PWD) clean
    endif
    

بار کردن واحد pKVM

همانند واحدهای فروشنده GKI، واحدهای فروشنده pKVM را می‌توان بااستفاده از modprobe بار کرد. بااین‌حال، به دلایل امنیتی، بارگذاری باید قبل‌از حذف امتیازات انجام شود. برای بار کردن ماژول pKVM، باید مطمئن شوید ماژول‌هایتان در سیستم فایل ریشه (initramfs) گنجانده شده باشد و باید موارد زیر را به خط فرمان هسته اضافه کنید:

kvm-arm.protected_modules=mod1,mod2,mod3,...

واحدهای فروشنده pKVM ذخیره‌شده در initramfs امضا و محافظت initramfs را به‌ارث می‌برند.

اگر یکی از پیمانه‌های فروشنده pKVM بار نشود، سیستم ناامن درنظر گرفته می‌شود و امکان راه‌اندازی ماشین مجازی محافظت‌شده وجود نخواهد داشت.

فراخوانی تابع EL2 (سرپرست) از EL1 (واحد هسته)

فراخوانی سرپرست (HVC) دستورالعملی است که به هسته اجازه می‌دهد سرپرست را فراخوانی کند. با معرفی واحدهای فروشنده pKVM، می‌توان از HVC برای فراخوانی تابع برای اجرا در EL2 (در واحد سرپرست) از EL1 (واحد هسته) استفاده کرد:

  1. در کد EL2 (el2.c)، گرداننده EL2 را اعلام کنید:

Android 14

   void pkvm_driver_hyp_hvc(struct kvm_cpu_context *ctx)
   {
     /* Handle the call */

     cpu_reg(ctx, 1) = 0;
   }

‫Android 15 یا بالاتر

   void pkvm_driver_hyp_hvc(struct user_pt_regs *regs)
   {
     /* Handle the call */

     regs->regs[0] = SMCCC_RET_SUCCESS;
     regs->regs[1] = 0;
   }
  1. در کد EL1 (el1.c)، کارگزار EL2 را در واحد فروشنده pKVM ثبت کنید:

    int __kvm_nvhe_pkvm_driver_hyp_init(const struct pkvm_module_ops *ops);
    void __kvm_nvhe_pkvm_driver_hyp_hvc(struct kvm_cpu_context *ctx); // Android14
    void __kvm_nvhe_pkvm_driver_hyp_hvc(struct user_pt_regs *regs);   // Android15
    
    static int hvc_number;
    
    static int __init pkvm_driver_init(void)
    {
      long token;
      int ret;
    
      ret = pkvm_load_el2_module(__kvm_nvhe_pkvm_driver_hyp_init,token);
      if (ret)
        return ret;
    
      ret = pkvm_register_el2_mod_call(__kvm_nvhe_pkvm_driver_hyp_hvc, token)
      if (ret < 0)
        return ret;
    
      hvc_number = ret;
    
      return 0;
    }
    module_init(pkvm_driver_init);
    
  2. در کد EL1 خود (el1.c)، با HVC تماس بگیرید:

    pkvm_el2_mod_call(hvc_number);
    

اشکال‌زدایی و نمایه کردن کد EL2

این بخش شامل چندین گزینه برای اشکال‌زدایی کد EL2 واحد pKVM است.

فرستادن و خواندن رویدادهای ردیابی فراناظر

‫Tracefs از ابرناظر pKVM پشتیبانی می‌کند. کاربر ریشه به این واسط دسترسی دارد، که در /sys/kernel/tracing/hypervisor/ قرار دارد:

  • tracing_on: ردیابی را روشن یا خاموش می‌کند.
  • trace: نوشتن در این فایل باعث بازنشانی ردیابی می‌شود.
  • trace_pipe: خواندن این فایل رویدادهای «ابرسرپرست» را چاپ می‌کند.
  • ‫buffer_size_kb: اندازه میان‌گیر هر CPU که رویدادها را نگه می‌دارد. اگر رویدادها ازدست می‌روند، این مقدار را افزایش دهید.

به‌طور پیش‌فرض، رویدادها خاموش هستند. برای فعال کردن رویدادها، از فایل /sys/kernel/tracing/hypervisor/events/my_event/enable مربوطه در Tracefs استفاده کنید. همچنین می‌توانید هر رویداد فراناظری را در زمان راه‌اندازی با خط فرمان هسته hyp_event=event1,event2 فعال کنید.

پیش‌از اعلام رویداد، کد EL2 واحد باید متن کلیشه‌ای زیر را اعلام کند، که در آن pkvm_ops همان struct pkvm_module_ops * ارائه‌شده به تابع واحد init است:

  #include "events.h"
  #define HYP_EVENT_FILE ../../../../relative/path/to/hyp/events.h
  #include <nvhe/define_events.h>

  #ifdef CONFIG_TRACING
  void *tracing_reserve_entry(unsigned long length)
  {
      return pkvm_ops->tracing_reserve_entry(length);
  }

  void tracing_commit_entry(void)
  {
      pkvm_ops->tracing_commit_entry();
  }
  #endif

اعلام رویدادها

رویدادها را در فایل .h خودشان اعلام کنید:

  $ cat hyp/events.h
  #if !defined(__PKVM_DRIVER_HYPEVENTS_H_) || defined(HYP_EVENT_MULTI_READ)
  #define __PKVM_DRIVER_HYPEVENTS_H_

  #ifdef __KVM_NVHE_HYPERVISOR__
  #include <nvhe/trace.h>
  #endif

  HYP_EVENT(pkvm_driver_event,
          HE_PROTO(u64 id),
          HE_STRUCT(
                  he_field(u64, id)
          ),
          HE_ASSIGN(
                  __entry->id = id;
          ),
          HE_PRINTK("id=0x%08llx", __entry->id)
  );
  #endif

رویدادهای انتشار

با فراخوانی تابع C تولیدشده می‌توانید رویدادها را در کد EL2 ثبت کنید:

  trace_pkvm_driver_event(id);

افزودن ثبت اضافی (Android 15 یا نسخه‌های پایین‌تر)

برای Android 15 و نسخه‌های پایین‌تر، ثبت اضافی را درطول راه‌اندازی واحد اضافه کنید. این مورد در Android 16 و بالاتر الزامی نیست.

  #ifdef CONFIG_TRACING
  extern char __hyp_event_ids_start[];
  extern char __hyp_event_ids_end[];
  #endif

  int pkvm_driver_hyp_init(const struct pkvm_module_ops *ops)
  {
  #ifdef CONFIG_TRACING
      ops->register_hyp_event_ids((unsigned long)__hyp_event_ids_start,
                                        (unsigned long)__hyp_event_ids_end);
  #endif

      /* init module ... */

      return 0;
  }

انتشار رویدادها بدون اعلام قبلی (Android 16 و نسخه‌های بالاتر)

اعلام رویدادها می‌تواند برای اشکال‌زدایی سریع دشوار باشد. trace_hyp_printk() به تماس‌گیرنده اجازه می‌دهد تا چهار آرگومان را بدون هیچ‌گونه اعلام رویدادی به رشته قالب ارسال کند:

  trace_hyp_printk("This is my debug");
  trace_hyp_printk("This is my variable: %d", (int)foo);
  trace_hyp_printk("This is my address: 0x%llx", phys);

کد تکراری در کد EL2 نیز لازم است. trace_hyp_printk() ماکرویی است که تابع trace___hyp_printk() را فراخوانی می‌کند:

  #include <nvhe/trace.h>

  #ifdef CONFIG_TRACING
  void trace___hyp_printk(u8 fmt_id, u64 a, u64 b, u64 c, u64 d)
  {
          pkvm_ops->tracing_mod_hyp_printk(fmt_id, a, b, c, d);
  }
  #endif

رویداد __hyp_printk را در /sys/kernel/tracing/hypervisor/events/ یا در زمان راه‌اندازی با خط فرمان هسته hyp_event=__hyp_printk فعال کنید.

هدایت رویدادها به dmesg

پارامتر خط فرمان هسته hyp_trace_printk=1 باعث می‌شود رابط ردیابی ابرناظر هر رویداد ثبت‌شده را به dmesg هسته ارسال کند. این کار برای خواندن رویدادها زمانی که trace_pipe دردسترس نیست مفید است.

رویدادهای تخلیه درطول یک هسته بحرانی (Android 16 و بالاتر)

رویدادهای «سرپرست ماشین مجازی» نظرسنجی می‌شوند. بنابراین، بین آخرین نظرسنجی و خطای هسته، پنجره‌ای وجود دارد که در آن رویدادها منتشر شده‌اند اما در کنسول تخلیه نشده‌اند. اگر hyp_trace_printk فعال شده باشد، گزینه پیکربندی هسته CONFIG_PKVM_DUMP_TRACE_ON_PANIC تلاش می‌کند جدیدترین رویدادها را در کنسول تخلیه کند.

این گزینه به‌طور پیش‌فرض برای GKI غیرفعال است.

از Ftrace برای ردیابی فراخوانی تابع و برگشت (Android 16 و بالاتر) استفاده کنید

Ftrace یکی از ویژگی‌های هسته است که به شما امکان می‌دهد هر فراخوانی و بازگشت تابع را ردیابی کنید. به‌همین ترتیب، مدیر اجرایی pKVM دو رویداد func و func_ret را ارائه می‌دهد.

می‌توانید توابع ردیابی‌شده را با خط فرمان هسته hyp_ftrace_filter= یا با یکی از فایل‌های tracefs انتخاب کنید:

  • /sys/kernel/tracing/hypervisor/set_ftrace_filter
  • /sys/kernel/tracing/hypervisor/set_ftrace_notrace

فیلترها از تطبیق سراسری سبک پوسته استفاده می‌کنند.

فیلتر زیر توابعی را که با pkvm_hyp_driver شروع می‌شوند ردیابی می‌کند:

  echo "__kvm_nvhe_pkvm_hyp_driver*" > /sys/kernel/tracing/hypervisor/set_ftrace_filter

رویدادهای func و func_ret فقط با CONFIG_PKVM_FTRACE=y دردسترس است. این گزینه به‌طور پیش‌فرض برای GKI غیرفعال است.