تنفيذ وحدة مورّد وحدة التحكّم عن بُعد في شاشة الفيديو (pKVM)

توضّح هذه الصفحة كيفية تنفيذ وحدة بائع تستند إلى آلة افتراضية محمية بنواة (pKVM).

في الإصدارات android16-6.12 والإصدارات الأحدث، بعد الانتهاء من هذه الخطوات، من المفترض أن تظهر شجرة دليل مشابهة لما يلي:

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

للاطّلاع على مثال كامل، يُرجى الرجوع إلى إنشاء وحدة pKVM باستخدام DDK .

في الإصدارات 15-6.6 من Android والإصدارات الأقدم، اتّبِع الخطوات التالية:

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 هي بنية تتضمّن عمليات رد الاتصال إلى برنامج Hypervisor الخاص بـ pKVM. تتّبع هذه البنية قواعد واجهة التطبيق الثنائية نفسها التي تتّبعها واجهات GKI.

  2. أنشئ hyp/Makefile لإنشاء رمز برنامج المشرف:

    hyp-obj-y := el2.o
    include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.module
    
  3. أضِف رمز نواة EL1 (el1.c). يجب أن يحتوي قسم الإعداد الأوّلي لهذا الرمز على استدعاء pkvm_load_el2 module لتحميل رمز برنامج الإشراف EL2 من الخطوة 1.

    #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 والإصدارات الأحدث، يُرجى الرجوع إلى إنشاء وحدة pKVM باستخدام DDK لإنشاء ddk_library() لـ EL2 وddk_module() لـ EL1.

    بالنسبة إلى الإصدارات android15-6.6 والإصدارات الأقدم، أنشئ ملف makefile الجذر لربط الرمزين البرمجيين 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;
   }

الإصدار 15 من نظام التشغيل Android أو إصدار أحدث

   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.

إصدار أحداث تتبُّع Hypervisor وقراءتها

يتوافق Tracefs مع برنامج pKVM للإشراف. يمكن لمستخدم الجذر الوصول إلى الواجهة، الموجودة في /sys/kernel/tracing/hypervisor/:

  • tracing_on: لتفعيل التتبُّع أو إيقافه
  • trace: تؤدي الكتابة إلى هذا الملف إلى إعادة ضبط التتبُّع.
  • trace_pipe: تؤدي قراءة هذا الملف إلى طباعة أحداث برنامج Hypervisor.
  • buffer_size_kb: حجم ذاكرة التخزين المؤقت لكل وحدة معالجة مركزية التي تحتوي على الأحداث يمكنك زيادة هذه القيمة إذا تم فقدان الأحداث.

يتم إيقاف الأحداث تلقائيًا. لتفعيل الأحداث، استخدِم ملف /sys/kernel/tracing/hypervisor/events/my_event/enable المناسب في Tracefs. يمكنك أيضًا تفعيل أي حدث من أحداث برنامج Hypervisor في وقت بدء التشغيل باستخدام سطر أوامر النواة 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

إرسال الأحداث

يمكنك تسجيل الأحداث في رمز EL2 عن طريق استدعاء دالة C التي تم إنشاؤها:

  trace_pkvm_driver_event(id);

إضافة تسجيل إضافي (Android 15 أو الإصدارات الأقدم)

في الإصدار 15 من نظام التشغيل Android والإصدارات الأقدم، عليك تضمين عملية تسجيل إضافية أثناء تهيئة الوحدة. هذا الإذن غير مطلوب في الإصدار 16 من نظام التشغيل Android والإصدارات الأحدث.

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

إصدار الأحداث بدون تعريف مسبق (الإصدار 16 من نظام التشغيل Android والإصدارات الأحدث)

قد يكون الإعلان عن الأحداث أمرًا مرهقًا لتصحيح الأخطاء بسرعة. تتيح الدالة 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 متاحًا.

تفريغ الأحداث أثناء حدوث خطأ فادح في النواة (الإصدار 16 من نظام التشغيل Android والإصدارات الأحدث)

يتم استطلاع أحداث المشرف. لذلك، هناك فترة زمنية بين آخر عملية استطلاع وخطأ kernel panic، حيث تم إصدار الأحداث ولكن لم يتم تفريغها إلى وحدة التحكّم. يحاول خيار إعدادات النواة CONFIG_PKVM_DUMP_TRACE_ON_PANIC تفريغ أحدث الأحداث في وحدة التحكّم إذا تم تفعيل hyp_trace_printk.

يتم إيقاف هذا الخيار تلقائيًا في GKI.

استخدام Ftrace لتتبُّع استدعاء الدالة وعودتها (الإصدار 16 من نظام التشغيل Android والإصدارات الأحدث)

‫Ftrace هي ميزة في النواة تتيح لك تتبُّع كل استدعاء للدالة وعودتها. وبطريقة مماثلة، يوفّر برنامج pKVM الخارق حدثَين هما func وfunc_ret.

يمكنك اختيار الدوال التي تم تتبُّعها باستخدام سطر أوامر النواة hyp_ftrace_filter= أو باستخدام أحد ملفات tracefs:

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

تستخدم الفلاتر مطابقة الأنماط العامة بنمط shell.

تتتبّع الفلترة التالية الدوال التي تبدأ بـ pkvm_hyp_driver:

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

لا تتوفّر أحداث func وfunc_ret إلا مع CONFIG_PKVM_FTRACE=y. يتم إيقاف هذا الخيار تلقائيًا في GKI.