בדף הזה מוסבר איך מטמיעים מודול ספק של מכונה וירטואלית מוגנת מבוססת-ליבה (pKVM).
ב-Android 16-6.12 ואילך, אחרי שמסיימים את השלבים האלה, אמורה להיות לכם היררכיית ספריות שדומה לזו:
BUILD.bazel
el1.c
hyp/
BUILD.bazel
el2.c
דוגמה מלאה זמינה במאמר איך בונים מודול pKVM באמצעות DDK.
בגרסה android15-6.6 ובגרסאות קודמות:
Makefile
el1.c
hyp/
Makefile
el2.c
מוסיפים את קוד ההיפר-ויזור 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; }ה-API של מודול הספק pKVM הוא מבנה שמכיל קריאות חוזרות (callbacks) אל ההיפרווייזר pKVM. המבנה הזה פועל לפי אותם כללי ABI כמו ממשקי GKI.
יוצרים את
hyp/Makefileכדי לבנות את קוד ההיפר-ויזור:hyp-obj-y := el2.o include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.moduleמוסיפים את קוד הליבה EL1 (
el1.c). קטע ה-init של הקוד הזה חייב להכיל קריאה ל-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);יוצרים את כללי הבנייה.
ב-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 (מודול ליבה)
קריאה ל-hypervisor (HVC) היא הוראה שמאפשרת לליבה לקרוא ל-hypervisor. עם ההשקה של מודולי ספקים של pKVM, אפשר להשתמש ב-HVC כדי לקרוא לפונקציה שתפעל ב-EL2 (במודול ההיפר-ויזור) מ-EL1 (מודול הליבה):
- בקוד EL2 (
el2.c), מצהירים על ה-handler של 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;
}
בקוד EL1 (
el1.c), רושמים את ה-handler של 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);בקוד EL1 (
el1.c), קוראים ל-HVC:pkvm_el2_mod_call(hvc_number);
ניפוי באגים ופרופילים של קוד EL2
בקטע הזה מפורטות כמה אפשרויות לניפוי באגים בקוד של מודול pKVM EL2.
פליטה וקריאה של אירועי מעקב של Hypervisor
Tracefs תומך בהיפרוויזור pKVM. למשתמש Root יש גישה לממשק, שנמצא ב-/sys/kernel/tracing/hypervisor/:
-
tracing_on: הפעלה או השבתה של המעקב. -
trace: כתיבה לקובץ הזה מאפסת את המעקב. -
trace_pipe: קריאת הקובץ הזה מדפיסה את האירועים של ההיפר-ויז'ר. -
buffer_size_kb: גודל המאגר לכל CPU שמכיל אירועים. צריך להגדיל את הערך הזה אם יש אירועים שלא נרשמים.
כברירת מחדל, האירועים מושבתים. כדי להפעיל אירועים, משתמשים בקובץ /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 ומטה)
ב-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 ואילך)
מתבצעת שליפה של אירועים מההיפרויזור. לכן יש חלון זמן בין הסקר האחרון לבין תגובה לשגיאת ליבה קריטית, שבו האירועים הופקו אבל לא הועברו מידע למסוף.
אפשרות ההגדרה של ליבת המערכת CONFIG_PKVM_DUMP_TRACE_ON_PANIC מנסה להעביר מידע על האירועים האחרונים במסוף אם hyp_trace_printk הופעלה.
האפשרות הזו מושבתת כברירת מחדל ב-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
המסננים משתמשים בהתאמה של תבניות glob בסגנון 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.