6.1 mmio设备模拟

同物理机器上的物理地址空间一样,虚拟机的 IPA 地址空间也包含了用于访问内存和外围设备的区域,如下图所示:

img

Hypervior 可以通过将 IPA 与实际物理外设地址映射,从而使 虚拟机可以直接通过二阶段的地址映射来访问物理设备,比如我们之前实现的虚拟机上的 PL011 支持,就是直接将 IPA 映射到物理地址上,实现设备直通.

img

  • 上述左边的设备叫做分配的外设,是已经分配给 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 设备通过该单链表串联起来

img

6.2 mmio设备注册

/*
注册新的 MMIO 设备到虚拟机的 vmmios 链表。
*/
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设备处理

下图为虚拟设备访问全过程:

img

  • 虚拟机中的软件访问虚拟设备,在上图中访问一个虚拟 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;
/* which vcpu has been trapped into EL2 */
read_sysreg(vcpu, tpidr_el2);

u64 esr, elr, far;
/* Exception Syndrome Register */
read_sysreg(esr, esr_el2);
/* Exception Link Register */
read_sysreg(elr, elr_el2);
/* Holds the faulting Virtual Address */
read_sysreg(far, far_el2);

/* Exception Class 取esr_el2寄存器的 26-31位,即EC字段*/
u64 esr_ec = (esr >> 26) & 0x3F;
/* Instruction Specific Syndrome 取esr_el2寄存器0-24位,即ISS字段*/
u64 esr_iss = esr & 0x1FFFFFF;
/* Instruction Length 取esr_el2寄存器的25位,即IL字段 */
u64 esr_il = (esr >> 25) & 0x1;

switch(esr_ec) {

/* HVC instruction execution in AArch64 state, when HVC is not disabled. */
/* 64位环境下执行HVC(Hypervisor Call)指令触发的异常。用于虚拟机监控模式(EL2),虚拟机通过此指令与Hypervisor交互。*/
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);
}
/* hvc from EL1 will set the preferred exception return address to pc+4 */
vcpu->regs.elr += 0;
break;
/* 64位环境下执行SMC(Secure Monitor Call)指令触发的异常。用于安全监控模式(EL3),实现安全世界与非安全世界的切换*/
case 0x17:
LOG_INFO("\033[32m[el1_sync_proc] smc trap from EL1\033[0m\n");
/* on smc call, iss is the imm of a smc */
if(hvc_smc_handler(vcpu, esr_iss) != 0) {
abort("Unknown SMC call #%d", esr_iss);
}
/* smc trapped from EL1 will set preferred execption return address to pc
* so we need to +4 return to the next instruction.
*/
vcpu->regs.elr += 4;
break;
/* data abort 内存访问异常*/
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; /* write or read */
int sas = (esr_iss >> 22) & 0x3; /* Syndrome Access Size */
int srt = (esr_iss >> 16) & 0x1f; /* The register number of the Wt/Xt/Rt */
int fnv = (esr_iss >> 10) & 0x1; /* far is valid or not valid */

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_issESR_EL2iss字段的值,wnr、sas、srt、fnv代表了不同属性,通过access_size这个枚举变量来存储
  • 最后调用vmmio_handler函数继续处理

vmmio_handler函数的定义如下:

int vmmio_handler(struct vcpu *vcpu, int reg_num, struct vmmio_access *vmmio)
{
/* The header of all the vmmios */
struct vmmio_info *vmmios = vcpu->vm->vmmios;
if(vmmios == NULL) {
return -1;
}

u64 ipa = vmmio->ipa;
u64 *reg = NULL;
u64 val;
/* 根据故障地址(vmmio->ipa)查找匹配的 MMIO 设备,
调用相应的 vmmio_read 或 vmmio_write 函数。*/
/* when use WZR or XZR, the srt is 31 */
if(reg_num == 31) {
val = 0;
} else {
/* use other common registers */
reg = &vcpu->regs.x[reg_num];
val = *reg;
}

/* 遍历MMIO设备链表 */
for(struct vmmio_info *m = vmmios; m != NULL; m = m->next) {
/* the fault ipa belong to this vmmio */
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函数中

 //vm.c

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

/* TODO: test */
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();

/* test code for wakeup vcore 1 */
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;

测试结果如下:

img