前言
对于type1的hypervisor(例如xen),虚拟机是如何访问IO的?下面分成原理部分和实现部分阐述。
原理部分
Stage 2 translation
hypervisor通过阶段2转换(Stage 2 translation)控制虚拟机(VM)中的内存视图。它允许hypervisor控制虚拟机可以访问哪些内存映射的系统资源,以及这些资源在虚拟机的地址空间中出现在哪里。
这种控制内存访问的能力对于隔离和沙盒来说非常重要。阶段2转换可用于确保VM只能看到分配给它的资源,而不能看到分配给其他VM或系统管理程序的资源。
对于存储器地址转换,阶段2转换是转换的第二阶段。为了支持这一点,需要一组新的转换表,称为阶段2表,如下所示:
操作系统(OS)控制一组转换表,这些转换表从虚拟地址空间映射到它认为的物理地址空间。然而,该过程经历到真实物理地址空间的第二次转换。第二阶段由hypervisor控制。
操作系统控制的翻译称为阶段1翻译,而hypervisor控制的翻译则称为阶段2翻译。OS认为是物理存储器的地址空间被称为中间物理地址(IPA)空间。
Emulating Memory-mapped Input/Output (MMIO)
与物理机器上的物理地址空间一样,虚拟机中的IPA空间包含用于访问内存和外围设备的区域,如下所示:
虚拟机可以使用外围区域来访问真实的物理外围设备(通常称为直接分配的外围设备)和虚拟外围设备。虚拟外围设备完全由hypervisor在软件中模拟,如下图所示:
分配的外围设备(assigned peripheral)是已分配给VM并映射到其IPA空间的真实物理设备。这允许在VM内运行的软件直接与外围设备交互。
虚拟外围设备(irtual peripheral)是hypervisor要在软件中模拟的外围设备。相应的第2阶段表条目将被标记为故障。虚拟机中的软件认为它可以直接与外围设备对话,但每次访问都会触发第2阶段的故障,hypervisor会在异常处理程序中模拟外围设备访问。
要模拟外围设备,hypervisor不仅需要知道哪个外围设备被访问,还需要知道该外围设备中的哪个寄存器被访问,访问是读还是写,访问的大小,以及用于传输数据的寄存器。
arm的手册《Exception Model》指南介绍了FAR_ELx寄存器。在处理阶段1故障时,这些寄存器会报告触发异常的虚拟地址。虚拟地址对hypervisor没有帮助,因为hypervisor通常不知道来宾操作系统是如何配置其虚拟地址空间的。对于第2阶段故障,有一个额外的寄存器HPFAR_EL2,它报告中止的地址的IPA。由于IPA空间由hypervisor控制,因此它可以使用这些信息来确定需要模拟的寄存器。
对于触发第2阶段故障的单个通用寄存器加载或存储,提供了额外的综合征信息。该信息包括访问的大小以及源或目的地寄存器,并允许hypervisor确定对虚拟外围设备的访问类型。
下图说明捕获模拟访问的过程:
此过程在以下步骤中进行了描述:
1.虚拟机中的软件试图访问虚拟外围设备。在本例中,这是一个虚拟UART的接收FIFO。
2.此访问在第2阶段转换时被阻止,导致路由到EL2的中止。
- 中止使用有关异常的信息填充ESR_EL2,包括访问的字节数、目标寄存器以及它是加载还是存储。
- 中止还会用中止访问的IPA填充HPFAR_EL2。
3.hypervisor使用来自ESR_EL2和HPFAR_EL2的信息来识别所访问的虚拟外围寄存器。此信息允许hypervisor模拟操作。然后,它通过ERET返回到vCPU。
- 在LDR之后重新开始执行指令。
实现部分
我们看看Xen是如何实现对MMIO的处理的。
结构体
domain结构体上有vmmio,vmmio负责管理domain的MMIO。其中num_entries代表注册的mmio_handler个数,max_num_entries代表注册的mmio_handler的最多个数,handlers是mmio_handler数组的指针。
struct domain
{
struct arch_domain arch;
}
struct arch_domain
{
struct vmmio vmmio;
}
struct vmmio {
int num_entries;
int max_num_entries;
rwlock_t lock;
struct mmio_handler *handlers;
};
mmio_handler如下,定义了起始地址(虚拟机的IPA),size,ops以及priv指针。ops会处理MMIO的读写,priv根据模拟的需要各个驱动自定义。
struct mmio_handler_ops {
mmio_read_t read;
mmio_write_t write;
};
struct mmio_handler {
paddr_t addr;
paddr_t size;
const struct mmio_handler_ops *ops;
void *priv;
};
初始化
创建相应domain的时候会,初始化vmmio。
domain_create
—>arch_domain_create
----->domain_io_init
domain_io_init的实现如下,主要申请了一个数组,用来管理mmio_handler。
int domain_io_init(struct domain *d, int max_count)
{
rwlock_init(&d->arch.vmmio.lock);
d->arch.vmmio.num_entries = 0;
d->arch.vmmio.max_num_entries = max_count;
d->arch.vmmio.handlers = xzalloc_array(struct mmio_handler, max_count);
if ( !d->arch.vmmio.handlers )
return -ENOMEM;
return 0;
}
通过register_mmio_handler注册的handler来实现 xen在异常处理程序中模拟外围设备访问。
register_mmio_handler函数实现如下,第一个参数是domian结构体,该结构体上的vmmio用来管理需要模拟的IO。第二个参数是模拟IO实现的mmio_handler_ops。第三个参数是虚拟机的IO的IPA,第四个参数是虚拟机的IO的size,第五个参数是private。
void register_mmio_handler(struct domain *d,
const struct mmio_handler_ops *ops,
paddr_t addr, paddr_t size, void *priv)
{
struct vmmio *vmmio = &d->arch.vmmio;
struct mmio_handler *handler;
BUG_ON(vmmio->num_entries >= vmmio->max_num_entries);
write_lock(&vmmio->lock);
handler = &vmmio->handlers[vmmio->num_entries];//vmmio中空闲的handler
handler->ops = ops;
handler->addr = addr;
handler->size = size;
handler->priv = priv;
vmmio->num_entries++;
/* Sort mmio handlers in ascending order based on base address */
sort(vmmio->handlers, vmmio->num_entries, sizeof(struct mmio_handler),//根据IPA进行排序。
cmp_mmio_handler, swap_mmio_handler);
write_unlock(&vmmio->lock);
}
首先取vmmio中空闲的handler。其次将ops,addr,size和priv依次赋值给handler。再次根据IPA,handler对进行排序。
这里虚拟机的IPA的值,应该同xen中的PA是值地址相同,xen中模拟虚拟机的IPA的读写。
中断处理
虚拟机对于的IO访问,由于只建立了阶段1转换(Stage 1 translation),所以会产生同步异常,这个异常会被xen捕获(参考原理部分)。调用do_trap_guest_sync处理来自虚拟机的同步异常。do_trap_guest_sync主要根据ESR_EL2的EC(错误类型->Exception Class.)来调用不同的函数处理ISS(Instruction Specific Syndrome)。
void do_trap_guest_sync(struct cpu_user_regs *regs)
{
const union hsr hsr = { .bits = regs->hsr }; // 读取ESR_EL2寄存器,以获取同步异常产生的类型
switch ( hsr.ec )
{
...
case HSR_EC_DATA_ABORT_LOWER_EL:
perfc_incr(trap_dabt);
do_trap_stage2_abort_guest(regs, hsr);
break;
...
}
}
#define HSR_EC_INSTR_ABORT_LOWER_EL 0x20
#define HSR_EC_INSTR_ABORT_CURR_EL 0x21
#define HSR_EC_DATA_ABORT_LOWER_EL 0x24
#define HSR_EC_DATA_ABORT_CURR_EL 0x25
ESR_EL2的EC值,0b100100代表Data Abort from a lower Exception level。
同步异常会读取ESR_EL2寄存器,以获取同步异常产生的类型,即EC值如上图arm手册。根据EC值的不同调用不同的函数处理异常。这里调用do_trap_stage2_abort_guest,去解析ESR_EL2的ISS字段。
static void do_trap_stage2_abort_guest(struct cpu_user_regs *regs,
const union hsr hsr)
{
/*
* The encoding of hsr_iabt is a subset of hsr_dabt. So use
* hsr_dabt to represent an abort fault.
*/
const struct hsr_xabt xabt = hsr.xabt;//ISS的DABT和IABT状态标志位。
int rc;
vaddr_t gva; //触发同步异常的VA
paddr_t gpa; //触发同步异常的IPA
uint8_t fsc = xabt.fsc & ~FSC_LL_MASK;//获取FSC,低两位清空了。
bool is_data = (hsr.ec == HSR_EC_DATA_ABORT_LOWER_EL);
mmio_info_t info;
enum io_state state;
/*
* If this bit has been set, it means that this stage-2 abort is caused
* by a guest external abort. We treat this stage-2 abort as guest SError.
*/
if ( xabt.eat )
return __do_trap_serror(regs, true);
gva = get_hfar(is_data);//FAR_EL2寄存器中读取触发同步异常的VA
if ( hpfar_is_valid(xabt.s1ptw, fsc) )
gpa = get_faulting_ipa(gva);//HPFAR_EL2寄存器中读取触发同步异常的IPA
else
{
/*
* Flush the TLB to make sure the DTLB is clear before
* doing GVA->IPA translation. If we got here because of
* an entry only present in the ITLB, this translation may
* still be inaccurate.
*/
if ( !is_data )
flush_guest_tlb_local();
rc = gva_to_ipa(gva, &gpa, GV2M_READ);
/*
* We may not be able to translate because someone is
* playing with the Stage-2 page table of the domain.
* Return to the guest.
*/
if ( rc == -EFAULT )
return; /* Try again */
}
.....
switch ( fsc )
case FSC_FLT_TRANS:
{
info.gpa = gpa;
info.dabt = hsr.dabt;//参考hsr_dabt结构体,ISS的DABT的标志位。
state = try_handle_mmio(regs, &info);//cpu_user_regs,IPA和dabt做参数
......
}
do_trap_stage2_abort_guest通过读取ESR_EL2的ISS,确认引起Data Abort from a lower Exception level的同步异常的具体原因。然后根据具体异常在调用对应的处理函数。通过FAR_EL2寄存器中读取触发同步异常的VA,通过HPFAR_EL2寄存器中读取触发同步异常的IPA,传递给try_handle_mmio函数。
#define FSC_FLT_TRANS (0x04)
/* Contain the common bits between DABT and IABT */
struct hsr_xabt {
unsigned long fsc:6; /* Fault status code */
unsigned long pad1:1; /* Not common */
unsigned long s1ptw:1; /* Stage 2 fault during stage 1 translation */
unsigned long pad2:1; /* Not common */
unsigned long eat:1; /* External abort type */
unsigned long fnv:1; /* FAR not Valid */
unsigned long pad3:14; /* Not common */
unsigned long len:1; /* Instruction length */
unsigned long ec:6; /* Exception Class */
} xabt;
struct hsr_dabt {
unsigned long dfsc:6; /* Data Fault Status Code */
unsigned long write:1; /* Write / not Read */
unsigned long s1ptw:1; /* Stage 2 fault during stage 1 translation */
unsigned long cache:1; /* Cache Maintenance */
unsigned long eat:1; /* External Abort Type */
unsigned long fnv:1; /* FAR not Valid */
#ifdef CONFIG_ARM_32
unsigned long sbzp0:5;
#else
unsigned long sbzp0:3;
unsigned long ar:1; /* Acquire Release */
unsigned long sf:1; /* Sixty Four bit register */
#endif
unsigned long reg:5; /* Register */
unsigned long sign:1; /* Sign extend */
unsigned long size:2; /* Access Size */
unsigned long valid:1; /* Syndrome Valid */
unsigned long len:1; /* Instruction length */
unsigned long ec:6; /* Exception Class */
} dabt; /* HSR_EC_DATA_ABORT_* */
Data Abort 的ISS标志位。xen代码中用hsr_dabt结构体表示。
其中bit位描述如下:
ISV:表示ISS包含的指令异常的解析是有效的。
SAS:异常发升的时候访问内存的大小。
SSE:数据是否需要符号扩展。
SRT:目标寄存器Rt。
SF:是32位还是64位load/store指令.
AR:指令是否包含acquire/release屏障语义
VNCR:是否来自嵌套虚拟化的访问。
SET:同步异常类型。
FnV:FAR寄存器是否有效。
EA:外部中止类型。
CM:Cache maintenance产生的数据异常。
S1PTW:对于stage 2 映射,指示错误发生在stage 1阶段还是stage 2阶段。
WnR:读或者写操作除法的异常。
DFSC:数据故障状态码
对于DFSC产生的具体原因。在获取FSC,低两位清空了,所以FSC_FLT_TRANS (0x04)对应Translation fault, level 0-level 3.
try_handle_mmio的实现如下:
enum io_state try_handle_mmio(struct cpu_user_regs *regs,
mmio_info_t *info)
{
struct vcpu *v = current;
const struct mmio_handler *handler = NULL;
int rc;
ASSERT(info->dabt.ec == HSR_EC_DATA_ABORT_LOWER_EL);
if ( !(info->dabt.valid || (info->dabt_instr.state == INSTR_CACHE)) )
{
ASSERT_UNREACHABLE();
return IO_ABORT;
}
handler = find_mmio_handler(v->domain, info->gpa);//根据gpa找到domain的vmmio上注册的handler,参考register_mmio_handler函数。
if ( !handler )
{
rc = try_fwd_ioserv(regs, v, info);
if ( rc == IO_HANDLED )
return handle_ioserv(regs, v);
return rc;
}
/*
* When the data abort is caused due to cache maintenance and the address
* belongs to an emulated region, Xen should ignore this instruction.
*/
if ( info->dabt_instr.state == INSTR_CACHE )
return IO_HANDLED;
/*
* At this point, we know that the instruction is either valid or has been
* decoded successfully. Thus, Xen should be allowed to execute the
* instruction on the emulated MMIO region.
*/
if ( info->dabt.write ) //写操作还是读操作?
return handle_write(handler, v, info);
else
return handle_read(handler, v, info);
}
find_mmio_handler根据gpa找到domain的vmmio上注册的handler,根据操作的类型调用handle_write和handle_read函数。
最后handle_read和handle_write会调用handler的ops来时先读写。参数vcpu,mmio_info_t和handler->priv会传递给ops。
static enum io_state handle_read(const struct mmio_handler *handler,
struct vcpu *v,
mmio_info_t *info)
{
const struct hsr_dabt dabt = info->dabt;
struct cpu_user_regs *regs = guest_cpu_user_regs();//获取压栈的VCPU寄存器的cpu_user_regs
/*
* Initialize to zero to avoid leaking data if there is an
* implementation error in the emulation (such as not correctly
* setting r).
*/
register_t r = 0;
if ( !handler->ops->read(v, info, &r, handler->priv) )//调用注册handler中的read函数,读取的值存到变量r中
return IO_ABORT;
r = sign_extend(dabt, r);
//获取ESR_EL2的ISS的SRT:目标寄存器Rt比特位,即hsr_dabt结构体的reg。最为写回的寄存器。
//由于当前进入xen的异常处理,guest os的VCPU寄存器的保存在cpu_user_regs,修改cpu_user_regs中对应的寄存器,返回guest os就会生效。
set_user_reg(regs, dabt.reg, r);
return IO_HANDLED;
}
static enum io_state handle_write(const struct mmio_handler *handler,
struct vcpu *v,
mmio_info_t *info)
{
const struct hsr_dabt dabt = info->dabt;
struct cpu_user_regs *regs = guest_cpu_user_regs();
int ret;
//调用注册handler中的write函数,将hsr_dabt结构体的reg写入到对应的PA
ret = handler->ops->write(v, info, get_user_reg(regs, dabt.reg),
handler->priv);
return ret ? IO_HANDLED : IO_ABORT;
}
参考资料
learn_the_architecture_-_aarch64_virtualization_102142_0100_03_en.pdf