6.1 mmio设备模拟 同物理机器上的物理地址空间一样,虚拟机的 IPA 地址空间也包含了用于访问内存和外围设备的区域,如下图所示:
Hypervior 可以通过将 IPA 与实际物理外设地址映射,从而使 虚拟机可以直接通过二阶段的地址映射来访问物理设备,比如我们之前实现的虚拟机上的 PL011 支持,就是直接将 IPA 映射到物理地址上,实现设备直通.
上述左边的设备叫做分配的外设,是已经分配给 Guest VM 并映射到其 IPA 空间的真实物理设备,允许 Guest VM 在运行时直接与外围设备交互。
上述右边的设备叫做虚拟外设,是 hypervisor 要在其软件中模拟的外设,在我们的 X-Hyper 实现中,我们让该虚拟设备的 IPA在二阶段表条目中不做物理地址映射,或者如果已经映射的,那么我们就给他解除映射。此时如果 Guest VM 访问该 IPA地址空间,则会触发 Data Abort 异常,从而陷入 EL2,并在 EL2 中实现软件模拟。
在 Hypervisor 的软件模拟中,我们需要知道哪个外设被访问了,还需要知道该外设中的哪个寄存器被访问了,是读访问还是写访问,访问的字节大小,以及传输数据的寄存器是哪个。
在Xhyper中使用vmmio_info来代表一个vmmio设备:
enum access_size { VMMIO_BYTE = 1 , VMMIO_HALFWORD = 2 , VMMIO_WORD = 4 , VMMIO_DOUBLEWORD = 8 , }; struct vmmio_info { struct vmmio_info *next ; u64 base; u64 size; int (*vmmio_read)(struct vcpu *, u64, u64 *, struct vmmio_access *); int (*vmmio_write)(struct vcpu *, u64, u64, struct vmmio_access *); };
vmmio info 定义了虚拟设备的起始地址(虚拟机的 IPA),size 大小,读写的操作函数以及指向下个 mmio 虚拟设备的指针,这些同属于一个虚拟机的 mmio 设备通过该单链表串联起来
6.2 mmio设备注册 int vmmio_handler_register (struct vm *vm, u64 ipa, u64 size, int (*vmmio_read)(struct vcpu *, u64, u64 *, struct vmmio_access *), int (*vmmio_write)(struct vcpu *, u64, u64, struct vmmio_access *)) { if (size == 0 || vm == NULL ) { return -1 ; } arch_spin_lock (&vm->vm_lock); struct vmmio_info *new = (struct vmmio_info *)xmalloc (sizeof (struct vmmio_info)); new ->next = vm->vmmios; vm->vmmios = new ; arch_spin_unlock (&vm->vm_lock); new ->base = ipa; new ->size = size; new ->vmmio_read = vmmio_read; new ->vmmio_write = vmmio_write; return 0 ; } void create_mmio_trap (struct vm *vm, u64 ipa, u64 size, int (*vmmio_read)(struct vcpu *, u64, u64 *, struct vmmio_access *), int (*vmmio_write)(struct vcpu *, u64, u64, struct vmmio_access *)) { u64 *vttbr = vm->vttbr; if (page_walk (vttbr, ipa, 0 )) { page_unmap (vttbr, ipa, size); } vmmio_handler_register (vm, ipa, size, vmmio_read, vmmio_write); flush_tlb (); return ; }
注册的操作在于如下两步操作:
new->next = vm->vmmios; vm->vmmios = new;
在vm结构体中定义了一个vmmio_info的指针用于保存mmio链表的头部
typedef struct vm { char name[32 ]; int nvcpu; u64 *vttbr; spinlock_t vm_lock; struct vcpu *vcpus[NCPU]; struct vmmio_info *vmmios; } vm_t ;
create_mmio_trap会调用vmmio_handler_register函数
6.3 mmio设备处理 下图为虚拟设备访问全过程:
虚拟机中的软件访问虚拟设备,在上图中访问一个虚拟 GIC 的 GICD 寄存器内容;此访问在二阶段转换时被阻止,并路由到 EL2 的异常
异常处理中硬件填充 ESR_EL2 寄存器,包括异常原因,访问的字节数、目标寄存器以及它是加载还是存储
异常处理中将访问的 IPA 填充到 HPFAR_EL2 寄存器中
Hypervisor 的异常处理软件中使用 ESR_EL2 和 HPFAR_EL2 来识别所访问的虚拟外设寄存器并进行设备的模拟操作,完成后返回到 VCPU
Guest VM 继续执行 LDR 之后的指令
HPFAR_EL2 寄存器用于在 EL2 模式(Hypervisor 模式)下记录 Stage-2 地址转换过程中发生的页面错误(Page Fault)或权限错误的中间物理地址(Intermediate Physical Address, IPA)
数据异常捕获, ESR_EL2寄存器的ec字段的值为0x24时代表产生了内存访问异常,此时会去调用data_abort_handler函数处理内存访问异常
void el1_sync_proc () { vcpu_t *vcpu; read_sysreg (vcpu, tpidr_el2); u64 esr, elr, far; read_sysreg (esr, esr_el2); read_sysreg (elr, elr_el2); read_sysreg (far, far_el2); u64 esr_ec = (esr >> 26 ) & 0x3F ; u64 esr_iss = esr & 0x1FFFFFF ; u64 esr_il = (esr >> 25 ) & 0x1 ; switch (esr_ec) { case 0x16 : LOG_INFO ("\033[32m [el1_sync_proc] hvc trap from EL1\033[0m\n" ); if (hvc_smc_handler (vcpu, esr_iss) != 0 ) { abort ("Unknown HVC call #%d" , esr_iss); } vcpu->regs.elr += 0 ; break ; case 0x17 : LOG_INFO ("\033[32m[el1_sync_proc] smc trap from EL1\033[0m\n" ); if (hvc_smc_handler (vcpu, esr_iss) != 0 ) { abort ("Unknown SMC call #%d" , esr_iss); } vcpu->regs.elr += 4 ; break ; case 0x24 : LOG_INFO ("\033[32m[el1_sync_proc] data abort from EL0/1\033[0m\n" ); data_abort_handler (vcpu, esr_iss, far); vcpu->regs.elr += 4 ; break ; default : abort ("Unknown el1 sync: esr_ec %p, esr_iss %p, elr %p, far %p" , esr_ec, esr_iss, elr, far); break ; } return ; }
data_abort_handler函数定义如下:
static int data_abort_handler (vcpu_t *vcpu, u64 esr_iss, u64 far) { u64 ipa = get_fault_ipa (far); int wnr = (esr_iss >> 6 ) & 0x1 ; int sas = (esr_iss >> 22 ) & 0x3 ; int srt = (esr_iss >> 16 ) & 0x1f ; int fnv = (esr_iss >> 10 ) & 0x1 ; if (fnv != 0 ) { abort ("FAR is not valid" ); } enum access_size accesz; switch (sas) { case 0 : accesz = VMMIO_BYTE; break ; case 1 : accesz = VMMIO_HALFWORD; break ; case 2 : accesz = VMMIO_WORD; break ; case 3 : accesz = VMMIO_DOUBLEWORD; break ; default : abort ("Unknow SAS from esr_el2" ); } struct vmmio_access vmmio_acs; vmmio_acs.ipa = ipa; vmmio_acs.accsize = accesz; vmmio_acs.wnr = wnr; if (vmmio_handler (vcpu, srt, &vmmio_acs) < 0 ) { LOG_WARN ("VMMIO handler failed: ipa %p, va %p\n" ); return -1 ; } return 0 ; }
传入的参数far即为FAR_EL2寄存器的值,存储了产生内存访问异常的地址
esr_iss为ESR_EL2的iss字段的值,wnr、sas、srt、fnv代表了不同属性,通过access_size这个枚举变量来存储
最后调用vmmio_handler函数继续处理
vmmio_handler函数的定义如下:
int vmmio_handler (struct vcpu *vcpu, int reg_num, struct vmmio_access *vmmio) { struct vmmio_info *vmmios = vcpu->vm->vmmios; if (vmmios == NULL ) { return -1 ; } u64 ipa = vmmio->ipa; u64 *reg = NULL ; u64 val; if (reg_num == 31 ) { val = 0 ; } else { reg = &vcpu->regs.x[reg_num]; val = *reg; } for (struct vmmio_info *m = vmmios; m != NULL ; m = m->next) { if (m->base <= ipa && ipa < m->base + m->size) { if (vmmio->wnr) { if (m->vmmio_write) { LOG_INFO ("[VMMIO WRITE]: device base: %p, offset is %p, write value %p, to reg %p\n" , m->base, ipa - m->base, val, reg_num); return m->vmmio_write (vcpu, ipa - m->base, val, vmmio); } } else { if (m->vmmio_read) { LOG_INFO ("[VMMIO READ]: device base: %p, offset is %p, read to reg %p\n" , m->base, ipa - m->base, reg_num); return m->vmmio_read (vcpu, ipa - m->base, reg, vmmio); } } } } return 0 ; }
根据故障地址查找对应的mmio设备,然后调用对应的读写函数
6.4 mmio映射测试 首先在Hypervisor中注册,在create_guest_vm函数中
static int test_mmio_read (struct vcpu *vcpu, u64 offset, u64 *val, struct vmmio_access *vmmio) { return 0 ; } static int test_mmio_write (struct vcpu *vcpu, u64 offset, u64 val, struct vmmio_access *vmmio) { return 0 ; } create_mmio_trap (vm, GICD_BASE, 0x10000 , test_mmio_read, test_mmio_write);create_mmio_trap (vm, GICR_BASE, 0x10000 , NULL , NULL );
然后在guest_vm中来进行触发:
int vm_primary_init () { pl011_init (); print_logo (); smc_call ((u64)0xc4000003 , (u64)1 , (u64)_start); volatile unsigned int *GICD_BASE = (volatile unsigned int *)(0x8000100 ); *GICD_BASE = 100 ; unsigned int y = *GICD_BASE; printf ("y is %d\n" , y); while (1 ) { printf ("I am vm 1 on core 0\n" ); for (int i=0 ; i < 100000000 ; i++); } return 0 ; }
先对GICD_BASE地址进行写操作:*GICD_BASE = 100;
然后对GICD_BASE地址进行读操作:unsigned int y = *GICD_BASE;
测试结果如下: