├── PPT ├── README.md ├── 唐诗美-AARCH64 Hypercraft虚拟化实现.pdf ├── 李宇-QEMU-and-KVM基本实现.pdf ├── 李宇-RISC-V-Hypervisor-Extension基本设定.pdf ├── 胡柯洋-Rust-Shyper-MonitorVM设计.pdf ├── 胡柯洋-Rust-Shyper多平台兼容和移植经验.pdf ├── 胡柯洋-基于Rust的嵌入式虚拟机监视器及热更新技术.pdf ├── 莫策-ARMv8体系结构与硬件虚拟化.pdf ├── 莫策-Rust-Shyper代码结构与设计实现.pdf ├── 陈岳-hcHyper项目架构与实现.pdf ├── 陈岳-x86虚拟化简述.pdf └── 齐呈祥-hypercraft设计理念与架构.pdf ├── README.md ├── book ├── README.md └── 系统虚拟化原理与实现.pdf ├── hypervisor ├── RISCV │ ├── README.md │ ├── ans.md │ ├── exercises.md │ └── img │ │ └── privilege_modes.jpg ├── aarch64 │ ├── README.md │ ├── ans.md │ ├── ch0-env-prepare.md │ ├── ch1-el-architecture-runcode.md │ ├── ch2-cpu-vcpu-vm.md │ ├── ch3-exception-hvc.md │ ├── ch4-memory-virtualization.md │ ├── ch5-boot-process.md │ ├── img │ │ ├── boot.png │ │ ├── changeEL.png │ │ ├── el.png │ │ ├── hvcflow.png │ │ ├── page_table.png │ │ ├── stage2translation.png │ │ └── translationstage.png │ ├── multi-core.md │ └── multi-vm.md ├── branch.md └── x86_64 │ ├── 00-env.md │ ├── 01-vmx.md │ ├── 02-vmcs.md │ ├── 03-vmlaunch.md │ ├── 04-ept.md │ ├── 05-nimbos.md │ ├── 06-io-int.md │ ├── README.md │ ├── aa-answer.md │ └── figures │ ├── 1-flow.svg │ ├── 2-vmclear.svg │ ├── 3-context-switch.svg │ ├── 4-ept-entries.svg │ ├── 4-ept.svg │ ├── 4-gpm-layout.svg │ ├── 4-nested.svg │ ├── 4-shadow1.svg │ ├── 4-shadow2.svg │ ├── 5-cpuid-failed.png │ ├── 5-gpm-layout.svg │ ├── 5-wrmsr-failed.png │ ├── 6-apic-timer.png │ ├── 6-ints.svg │ ├── 6-iommu.svg │ ├── 6-prob2.png │ └── 6-serial-io.svg └── tasks ├── README.md ├── device_and_interrupt_virtualization.md ├── dynamic_memory_management.md ├── multi_core_support.md ├── multi_vm_support.md ├── real_time_vm.md └── vm_migration.md /PPT/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PPT/唐诗美-AARCH64 Hypercraft虚拟化实现.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/唐诗美-AARCH64 Hypercraft虚拟化实现.pdf -------------------------------------------------------------------------------- /PPT/李宇-QEMU-and-KVM基本实现.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/李宇-QEMU-and-KVM基本实现.pdf -------------------------------------------------------------------------------- /PPT/李宇-RISC-V-Hypervisor-Extension基本设定.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/李宇-RISC-V-Hypervisor-Extension基本设定.pdf -------------------------------------------------------------------------------- /PPT/胡柯洋-Rust-Shyper-MonitorVM设计.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/胡柯洋-Rust-Shyper-MonitorVM设计.pdf -------------------------------------------------------------------------------- /PPT/胡柯洋-Rust-Shyper多平台兼容和移植经验.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/胡柯洋-Rust-Shyper多平台兼容和移植经验.pdf -------------------------------------------------------------------------------- /PPT/胡柯洋-基于Rust的嵌入式虚拟机监视器及热更新技术.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/胡柯洋-基于Rust的嵌入式虚拟机监视器及热更新技术.pdf -------------------------------------------------------------------------------- /PPT/莫策-ARMv8体系结构与硬件虚拟化.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/莫策-ARMv8体系结构与硬件虚拟化.pdf -------------------------------------------------------------------------------- /PPT/莫策-Rust-Shyper代码结构与设计实现.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/莫策-Rust-Shyper代码结构与设计实现.pdf -------------------------------------------------------------------------------- /PPT/陈岳-hcHyper项目架构与实现.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/陈岳-hcHyper项目架构与实现.pdf -------------------------------------------------------------------------------- /PPT/陈岳-x86虚拟化简述.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/陈岳-x86虚拟化简述.pdf -------------------------------------------------------------------------------- /PPT/齐呈祥-hypercraft设计理念与架构.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/PPT/齐呈祥-hypercraft设计理念与架构.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2023秋冬季训练营第三阶段-虚拟化方向 2 | 3 | > 往期视频讲座链接:https://os2edu.cn/course/120 4 | 5 | ## 前置技能 6 | 7 | - 熟悉 x86、arm、risc-v 任一架构 8 | - 函数调用规范 9 | - 中断异常处理流程 10 | - 页表机制实现过程 11 | 12 | ## 资料阅读建议 13 | 14 | - 了解虚拟化相关基础概念 15 | - 推荐[Docker,K8s,KVM,Hypervisor和微服务有什么区别联系吗?](https://www.zhihu.com/question/307537564/answer/583653317) 16 | - 各体系结构的实现方案介绍 17 | - [莫策-ARMv8体系结构与硬件虚拟化](./PPT/莫策-ARMv8体系结构与硬件虚拟化.pdf) 18 | - [齐呈祥-hypercraft设计理念与架构](./PPT/齐呈祥-hypercraft设计理念与架构.pdf) 19 | - [陈岳-x86虚拟化简述](./PPT/陈岳-x86虚拟化简述.pdf) 20 | - 熟悉 ArceOS 21 | - 推荐课程:[项目一:ArceOS单内核Unikernel](https://os2edu.cn/course/157) (不做实验,只听石磊老师讲感觉也可 22 | - [ArceOS 设计&实现 - 组件化OS基础 - 1](https://www.bilibili.com/video/BV1th4y1b7e4/?spm_id_from=333.337.search-card.all.click&vd_source=1d65ea6deb9458981dfc8bd282f7a495) 23 | 24 | ## Hypervisor讲解 25 | 26 | * [x86-64 Hypervisor介绍](./hypervisor/x86_64/README.md) 27 | * [ARM Hypervisor介绍](./hypervisor/aarch64/README.md) 28 | * [RISCV练习](./hypervisor/RISCV/exercises.md) 29 | 30 | ## 任务列表 31 | 32 | * [多核支持](./tasks/multi_core_support.md) 33 | * [多VM支持](./tasks/multi_vm_support.md) 34 | * [内存动态管理](./tasks/dynamic_memory_management.md) 35 | * [设备和中断虚拟化](./tasks/device_and_interrupt_virtualization.md) 36 | * [虚拟机迁移](./tasks/vm_migration.md) 37 | * [实时虚拟机](./tasks/real_time_vm.md) 38 | 39 | ## 课程PPT 40 | 41 | 以下ppt对应的课程视频链接:https://os2edu.cn/course/120 42 | 43 | * [莫策-ARMv8体系结构与硬件虚拟化](./PPT/莫策-ARMv8体系结构与硬件虚拟化.pdf) 44 | * [莫策-Rust-Shyper代码结构与设计实现](./PPT/莫策-Rust-Shyper代码结构与设计实现.pdf) 45 | * [齐呈祥-hypercraft设计理念与架构](./PPT/齐呈祥-hypercraft设计理念与架构.pdf) 46 | * [李宇-QEMU/KVM基本实现](./PPT/李宇-QEMU-and-KVM基本实现.pdf) 47 | * [李宇-RISC-V-Hypervisor-Extension基本设定](./PPT/李宇-RISC-V-Hypervisor-Extension基本设定.pdf) 48 | * [胡柯洋-Rust-Shyper Monitor VM设计](./PPT/胡柯洋-Rust-Shyper-MonitorVM设计.pdf) 49 | * [胡柯洋-Rust-Shyper多平台兼容和移植经验](./PPT/胡柯洋-Rust-Shyper多平台兼容和移植经验.pdf) 50 | * [胡柯洋-基于Rust的嵌入式虚拟机监视器及热更新技术](./PPT/胡柯洋-基于Rust的嵌入式虚拟机监视器及热更新技术.pdf) 51 | * [季朋-virtio基本原理和驱动/设备交互](https://zhuanlan.zhihu.com/p/639301753?utm_psn=1704906158266068992) 52 | * [陈岳-x86虚拟化简述](./PPT/陈岳-x86虚拟化简述.pdf) 53 | * [陈岳-hcHyper项目架构与实现](./PPT/陈岳-hcHyper项目架构与实现.pdf) 54 | * [唐诗美-AARCH64 Hypercraft虚拟化实现](./PPT/唐诗美-AARCH64%20Hypercraft虚拟化实现.pdf) 55 | 56 | ## 参考书籍 57 | * [系统虚拟化原理与实现](./book/系统虚拟化原理与实现.pdf) 58 | 59 | ## 参考资料 60 | ### aarch64 61 | - [Arm® Architecture Reference Manual for A-profile architecture](https://developer.arm.com/documentation/ddi0487/latest/) 62 | - [Arm® Architecture Registers for A-profile architecture](https://developer.arm.com/documentation/ddi0601/latest/) 63 | - [Armv8-A virtualization](https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Learn%20the%20Architecture/Armv8-A%20virtualization.pdf?revision=a765a7df-1a00-434d-b241-357bfda2dd31) 64 | - [Arm® Exception model](https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Learn%20the%20Architecture/Exception%20model.pdf?revision=a62f2bf2-b08a-4a4f-8cbe-38c67ddf4434) 65 | 66 | ### x86 67 | - [Intel® 64 and IA-32 Architectures Software Developer Manuals](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) 68 | 69 | ### RISCV 70 | - [RISC-V Technical Specifications](https://wiki.riscv.org/display/HOME/RISC-V+Technical+Specifications) 71 | - [The RISC-V Instruction Set Manual Volume I: Unprivileged ISA](https://drive.google.com/file/d/1s0lZxUZaa7eV_O0_WsZzaurFLLww7ou5/view) 72 | - [The RISC-V Instruction Set Manual Volume II: Privileged Architecture](https://drive.google.com/file/d/1EMip5dZlnypTk7pt4WWUKmtjUKTOkBqh/view) 73 | - [The RISC-V Hypervisor Extension](https://riscv.org/wp-content/uploads/2017/12/Tue0942-riscv-hypervisor-waterman.pdf) 74 | -------------------------------------------------------------------------------- /book/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /book/系统虚拟化原理与实现.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/book/系统虚拟化原理与实现.pdf -------------------------------------------------------------------------------- /hypervisor/RISCV/README.md: -------------------------------------------------------------------------------- 1 | # arceos RISCV hypervisor 2 | # RISCV Hypercraft教程 3 | 4 | ## [Exercises ](./exercises.md) 5 | -------------------------------------------------------------------------------- /hypervisor/RISCV/ans.md: -------------------------------------------------------------------------------- 1 | # RISCV练习答案 2 | ## 练习1: 3 | 1.略 4 | 2. 5 | (1)引入虚拟化扩展后,增加了HS模式(是S模式的一种扩展),Hypervisor位于该模式,可以托管客户机操作系统。VS模式和VU模式,VS模式是客户机内核所处的模式,VU是客户机用户程序处于的模式。具体的可以看下图: 6 | ![特权模式图](./img/privilege_modes.jpg) 7 | 8 | (2)二型虚拟机,因为是运行于arceos中,并不是直接运行在裸机上。 9 | 10 | ## 练习2: 11 | 主要涉及以下寄存器: 12 | - hstatus 寄存器(Hypervisor Status):是HS模式下用于控制和存储的一些状态信息的寄存器,用于控制中断使能、虚拟内存设置以及一些其他的特权级别控制。 13 | - sstatus寄存器(Supervisor Status):用于控制和存储Supervisor模式下的状态信息。包括位字段,用于控制中断使能、虚拟内存设置等。 14 | - sepc寄存器(Supervisor Exception Program Counter):存储导致最近一次异常或中断的指令地址。 15 | - hgatp寄存器(Hypervisor Guest Address Translation and Protection):用于控制和配置HS模式下的虚拟地址转换和保护机制。 16 | 17 | ## 练习3: 18 | 在Hypercraft中产生Trap主要的起因是Interrupt和Exception,Interrupt和Exception所包含的具体类型定义如下面代码显示: 19 | 20 | ```rust 21 | ///Trap 22 | pub enum Trap { 23 | Interrupt(Interrupt), 24 | Exception(Exception), 25 | } 26 | 27 | /// Interrupt 28 | pub enum Interrupt { 29 | UserSoft, 30 | VirtualSupervisorSoft, 31 | SupervisorSoft, 32 | UserTimer, 33 | VirtualSupervisorTimer, 34 | SupervisorTimer, 35 | UserExternal, 36 | VirtualSupervisorExternal, 37 | SupervisorExternal, 38 | Unknown, 39 | } 40 | 41 | /// Exception 42 | pub enum Exception { 43 | InstructionMisaligned, 44 | InstructionFault, 45 | IllegalInstruction, 46 | Breakpoint, 47 | LoadFault, 48 | StoreMisaligned, 49 | StoreFault, 50 | UserEnvCall, 51 | VirtualSupervisorEnvCall, 52 | InstructionPageFault, 53 | LoadPageFault, 54 | StorePageFault, 55 | InstructionGuestPageFault, 56 | LoadGuestPageFault, 57 | VirtualInstruction, 58 | StoreGuestPageFault, 59 | Unknown, 60 | } 61 | ``` 62 | 63 | 可能存在如下特权级别切换:客户机用户程序(VU模式)—>客户机内核(VS模式)—>Hypervisor(HS模式) 64 | 65 | 主要涉及下面一些寄存器: 66 | - scause:S模式下保存产生异常的原因 67 | - stval:S模式下存储导致异常的指令或数据的值 68 | - htval:HS模式下记录异常导致的指令或数据值 69 | - htinst:HS模式下储导致异常的指令的副本 70 | 71 | 72 | ## 练习4: 73 | guest OS 中启用了 3 级页,在gva->gpa的过程中,由于还涉及guest页表gpa->hpa的转换,所以需要处理3×3=9次。最后得到了gpa,gpa->hpa还需要进行第二阶段的3次访存,所以是9+3=12次。 74 | 75 | ## 练习5: 76 | 分别修改apps/hv/src/main.rs处理dtb与kerel entry point的相关内容、apps/hv/guest/inux/linux.dts中memory节点的reg并利用dtc重新编译为dtb、以及srcipts/make/gemu.mk 51、52行内容。 77 | -------------------------------------------------------------------------------- /hypervisor/RISCV/exercises.md: -------------------------------------------------------------------------------- 1 | 2 | ## 练习1 3 | 1. 在arceos中运行linux 4 | 2. (1)阅读[The RISC-V Instruction Set Manual Volume II: Privileged Architecture](https://drive.google.com/file/d/1EMip5dZlnypTk7pt4WWUKmtjUKTOkBqh/view) Chapter 8(8.1)Privilege Modes,回答RISCV引入虚拟化扩展后,其特权模式有哪些新的变化,各模式之间存在什么关系与区别。(2)请问Hypercraft是哪一种hypervisor。 5 | 6 | ## 练习2 7 | 1. 在合适的地方修改代码,打印出在vcpu初始化前后VmCpuRegisters各个部分寄存器的值。比较它们哪些值发生了变化。并根据本章节介绍的内容,对照[The RISC-V Instruction Set Manual Volume II: Privileged Architecture](https://drive.google.com/file/d/1EMip5dZlnypTk7pt4WWUKmtjUKTOkBqh/view),说明值变化的寄存器的作用。 8 | 9 | ## 练习3 10 | 1. 参考[The RISC-V Instruction Set Manual Volume II: Privileged Architecture](https://drive.google.com/file/d/1EMip5dZlnypTk7pt4WWUKmtjUKTOkBqh/view) Chapter 8(8.6) Traps,分析RISCV版本的Hypercraft存在哪些异常类型,这些异常从触发到处理完成分别经过怎样的特权级别的切换,会涉及到哪些寄存器的设置。 11 | 12 | ## 练习4 13 | 1. 假设在 guest OS 中启用了 3 级页表,如果 guest 的一次访存 (使用 guest 虚拟地址) 发生了 TLB 缺失,请问以riscv64 two-stage address translation的方法实现内存虚拟化时,最多会导致多少次内存访问? 14 | 15 | ## 练习5 16 | 1. 把kernel和dtb放到其他的内存地址后启动。 17 | 提示:除了修改apps/hv/src/main.rs中载入kernel和dtb的地址,还有哪些地方需要修改?关注scripts/make/qemu.mk、apps/hv/guest/linux/linux.dts,注意dts如何编译成dtb。 18 | -------------------------------------------------------------------------------- /hypervisor/RISCV/img/privilege_modes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/RISCV/img/privilege_modes.jpg -------------------------------------------------------------------------------- /hypervisor/aarch64/README.md: -------------------------------------------------------------------------------- 1 | # arceos aarch64 hypervisor 2 | # AARCH64 Hypercraft教程 3 | 4 | ## [CH.0 环境准备](./ch0-env-prepare.md) 5 | ## [CH.1 AARCH64特权等级(异常级别EL)简介与代码整体及运行方法说明](./ch1-el-architecture-runcode.md) 6 | ## [CH.2 CPU、VCpu与VM结构](./ch2-cpu-vcpu-vm.md) 7 | ## [CH.3 AARCH64异常处理与HVC指令介绍及其Handler实现](./ch3-exception-hvc.md) 8 | ## [CH.4 内存虚拟化介绍与实现](./ch4-memory-virtualization.md) 9 | ## [CH.5 启动流程详解](./ch5-boot-process.md) -------------------------------------------------------------------------------- /hypervisor/aarch64/ans.md: -------------------------------------------------------------------------------- 1 | # 解答 2 | ## 1.5.2 3 | 二型虚拟机,因为是运行于arceos中,并不是直接运行在裸机上。 4 | ## 2.4.1 5 | 变化的寄存器 6 | - sctlr_el1:系统控制寄存器,用于进行一些系统设置,包括内存系统 7 | - nTWE, bit [18] 8 | - 0:如果在 EL0 执行的 WFE 指令会导致执行被暂停,例如事件寄存器未设置且没有待处理的 WFE 唤醒事件,则会使用 0x1 ESR 代码将其视为对EL1 的异常。 9 | - 1:WFE指令正常运行。 10 | - nTWI, bit [16] 11 | - 0:如果在 EL0 执行的 WFI 指令会导致执行被暂停,例如事件寄存器未设置且没有待处理的 WFE 唤醒事件,则会使用 0x1 ESR 代码将其视为对EL1 的异常。 12 | - 1:WFI指令正常运行。 13 | - CP15BEN, bit [5] 14 | - 0:aarch32 CP15 barrier指令禁用,编码为UNDEFINED。 15 | - 1:aarch32 CP15 barrier指令启用。 16 | - SA0, bit [4] 17 | - 当该bit启用时,EL0 的加载/存储指令中堆栈指针作为基地址的使用必须对齐到 16 字节边界,否则将引发堆栈对齐故障异常。 18 | - vtcr_el2:用于设置二阶段页表第二阶段翻译的相关参数 19 | - PS, bits [18:16]:第二阶段翻译的物理地址大小 20 | - TG0, bits [15:14]:VTTBR_EL2的页面粒度大小 21 | - SH0, bits [13:12]:用VTTBR_EL2或VSTTBR_EL2进行table walks时关联的内存的共享属性 22 | - ORGN0, bits [11:10]:用VTTBR_EL2或VSTTBR_EL2进行table walks时关联的内存的外部缓存属性 23 | - IRGN0, bits [9:8]:用VTTBR_EL2或VSTTBR_EL2进行table walks时关联的内存的内部缓存属性 24 | - SL0, bits [7:6]:第二阶段翻译查询的开始级别 25 | - T0SZ, bits [5:0]:VTTBR_EL2 寻址的内存区域的大小偏移量,区域大小为2^(64-T0SZ)字节。 26 | - hcr_el2:用于进行进行虚拟化的配置,比如设置哪些操作会被trap到EL2。由于目前hypervisor的实现中断控制器也以直通的方式进行虚拟化,所以未设置异常路由到EL2。 27 | - RW, bit [31]: 28 | - 0b0:Lower EL都为aarch32。 29 | - 0b1:在EL1的执行状态为aarch64,EL0的执行状态由PSTATE.nRW当前的值决定。 30 | - VM, bit [0]: 31 | - 0b0:禁用EL1&0 二阶段地址翻译。 32 | - 0b1:启用EL1&0 二阶段地址翻译。 33 | - vmpidr_el2:用于存储虚拟多处理器ID 34 | - x0:存储dtb在内存中的地址,用于之后启动后系统进行读取 35 | - elr(el2):存储kernel entry point在内存中的地址,使eret后能够直接跳转 36 | - spsr:用于恢复到PSTATE中的信息 37 | - SPSR_EL1::M::EL1h:设置异常级别为EL1,并且设置由EL确定用的SP是哪个EL。 38 | - SPSR_EL1::I::Masked:IRQ被屏蔽。 39 | - SPSR_EL1::F::Masked:FIQ被屏蔽。 40 | - SPSR_EL1::A::Masked:SError中断被屏蔽。 41 | - SPSR_EL1:\:D::Masked:目标在当前EL的Watchpoint、Breakpoint 和 Software Step 异常被屏蔽。 42 | ## 3.4.1 43 | ![流程](./img/hvcflow.png) 44 | ## 3.4.2 45 | esr_el2的ec会显示为000000,查询文档可知可能的原因: 46 | ◦ A read access using a System register pattern that is not allocated for reads or that does not permit reads at the current Exception level and Security state. 47 | ◦ A write access using a System register pattern that is not allocated for writes or that does not permit writes at the current Exception level and Security state. 48 | ## 3.4.3 49 | 略。不知道如何实现的可直接联系老师。 50 | ## 4.3.1 51 | ![translate](./img/page_table.png) 52 | 参考图中所说,aarch64是3级页表,在gva->gpa的过程中,由于还涉及guest页表gpa->hpa的转换,所以需要处理3×3=9次。最后得到了gpa,gpa->hpa还需要进行第二阶段的3次访存,所以是9+3=12次。 53 | ## 4.3.2 54 | 直接使用gpm的translate方法即可,需要注意的是一开始未建立映射时会出现HyperError::Internal error,需要进行错误处理。 55 | ## 5.3.1 56 | 分别修改apps/hv/src/main.rs处理dtb与kernel entry point的相关内容、apps/hv/guest/linux/linux-aarch64.dts中memory节点的reg并利用dtc重新编译为dtb、以及srcipts/make/qemu.mk 56、57行内容。不能放到0x40000000,因为hypervisor的栈的内存是在这块区域。 -------------------------------------------------------------------------------- /hypervisor/aarch64/ch0-env-prepare.md: -------------------------------------------------------------------------------- 1 | # 0. 环境准备 2 | ## 0.1 开发环境 3 | - 软件: 4 | - 建议安装 Ubuntu 等 Linux 操作系统 (不能是虚拟机) 5 | - [QEMU](https://www.qemu.org/download/) >= 7.0.0 6 | - [Rust 工具链](https://www.rust-lang.org)(nightly-2023-03-03) 7 | - 理论上只需指定 nightly channel 即可,但考虑到不同的 nightly 编译器可能出现行为差异,请在遇见奇怪的问题时尝试锁定工具链版本 8 | -------------------------------------------------------------------------------- /hypervisor/aarch64/ch1-el-architecture-runcode.md: -------------------------------------------------------------------------------- 1 | # 1. AARCH64特权等级(异常级别EL)简介与代码整体及运行方法说明 2 | ## 1.1 AARCH64异常级别 3 | > 在Arm架构中,当前特权级别只能在处理器发生异常或从异常返回时更改。因此,这些特权级别被称为“异常级别”(Exception levels)。 4 | 5 | ![el示意图](./img/el.png) 6 | 7 | - 异常级别的切换(系统调用) 8 | ![改变EL](./img/changeEL.png) 9 | - SVC 10 | - 用于触发一个Supervisor Call Exception,使在EL0特权级的用户程序能够请求EL1特权级的操作系统服务。 11 | - HVC 12 | - 用于触发一个Hypervisor Call Exception,使在EL1特权级的操作系统能够请求在EL2特权级的虚拟监控程序提供的服务。 13 | - SMC 14 | - 用于触发一个Secure Monitor Call Exception,使在正常世界(Normal world)能够从EL3特权级的固件请求安全世界(Secure world)提供的服务。 15 | - ERET 16 | - 用于异常返回,从当前异常等级返回触发异常前的异常等级。 17 | ## 1.2 代码组织结构 18 | - crates/hypercraft/src/aarch64: aarch64 hypercraft的大部分代码,包括Vcpu、VM、CPU、EPT结构的定义,以及异常处理函数与HVC的调用。 19 | - apps/hv/src: 定义了虚拟机启动的main函数,以及相关虚拟机的配置。 20 | - apps/hv/guest: 用于存放guest kernel、dtb等内容。 21 | - modules/axhal/src/arch/aarch64: 用于定义异常处理上下文的结构,以及异常向量表的定义。 22 | - modules/axhal/src/platform/aarch64_common: 定义了arceOS入口函数,并在其中完成一些EL2级别的设置。 23 | - modules/axruntime/src/gpm.rs: 定义了guest page table的相关内容。 24 | - crates/page_table_entry与crates/page_table: 定义了页表相关结构与方法,包括页表描述符等。 25 | ## 1.3 建立执行环境 26 | 本文档将对基于arceOS的type-2的hypervisor进行说明,该种类型hypervisor不能直接访问硬件,需要依赖于底层OS。arceOS本身的启动与初始化流程在modules/axhal/src/platform/aarch64_common/boot.rs中。 27 | 1. 初始化EL2的异常向量表 28 | 2. 初始化栈帧 29 | 3. 初始化页表以及EL2与EL1的页表基址寄存器,开启MMU 30 | 4. 切换到EL1执行系统 31 | ## 1.4 运行 32 | 假设我们以下操作都是在$(WORKSPACE)目录下进行操作。 33 | ### 1.4.1 运行nimbos 34 | #### 编译镜像 35 | 1.4.1节运行的OS为[NimbOS](https://github.com/equation314/nimbos)。为了编译这个OS内核,执行以下指令: 36 | ```shell 37 | # set up cross-compile tools 38 | # download 39 | wget https://musl.cc/aarch64-linux-musl-cross.tgz 40 | # install 41 | tar zxf aarch64-linux-musl-cross.tgz 42 | # exec below command in bash 43 | export PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH 44 | # OR add below info in ~/.bashrc 45 | # echo PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH >> ~/.bashrc 46 | 47 | # clone nimbos 48 | git clone https://github.com/equation314/nimbos.git 49 | cd nimbos/kernel 50 | 51 | # set up rust tools 52 | make env 53 | 54 | # build nimbos 55 | cd ../user && make ARCH=aarch64 build 56 | cd ../kernel && make build ARCH=aarch64 LOG=warn 57 | ``` 58 | 此时会在$(WORKSPACE)/nimbos/kernel/target/aarch64/release下看到编译好的nimbos.bin 59 | #### 运行hypervisor 60 | 在qemu中运行arceOS,并在arceOS上起虚拟机。 61 | ```shell 62 | # clone arceos 63 | git clone https://github.com/arceos-hypervisor/arceos.git -b hypervisor --recurse-submodules 64 | cd arceos 65 | 66 | # move nimbos image to arceos hv 67 | cp $(WORKSPACE)/nimbos/kernel/target/aarch64/release/nimbos.bin apps/hv/guest/nimbos/nimbos-aarch64.bin 68 | 69 | # set up rust tools 70 | cargo install cargo-binutils 71 | 72 | # build and run arceos aarch64 hypervisor 73 | make ARCH=aarch64 A=apps/hv HV=y LOG=info GUEST=nimbos run 74 | ``` 75 | 执行上述指令后可以成功看见,能够在命令行输入hello_world,打印出“Hello world from user mode program!” 76 | ```shell 77 | d8888 .d88888b. .d8888b. 78 | d88888 d88P" "Y88b d88P Y88b 79 | d88P888 888 888 Y88b. 80 | d88P 888 888d888 .d8888b .d88b. 888 888 "Y888b. 81 | d88P 888 888P" d88P" d8P Y8b 888 888 "Y88b. 82 | d88P 888 888 888 88888888 888 888 "888 83 | d8888888888 888 Y88b. Y8b. Y88b. .d88P Y88b d88P 84 | d88P 888 888 "Y8888P "Y8888 "Y88888P" "Y8888P" 85 | 86 | arch = aarch64 87 | platform = qemu-virt-aarch64 88 | smp = 1 89 | build_mode = release 90 | log_level = info 91 | 92 | [ 0.001507 0 axruntime:138] Logging is enabled. 93 | [ 0.002006 0 axruntime:139] Primary CPU 0 started, dtb = 0x48000000. 94 | [ 0.002167 0 axruntime:144] Found physcial memory regions: 95 | [ 0.002318 0 axruntime:146] [PA:0x40080000, PA:0x40090000) .text (READ | EXECUTE | RESERVED) 96 | [ 0.002507 0 axruntime:146] [PA:0x40090000, PA:0x40094000) .rodata (READ | RESERVED) 97 | [ 0.002609 0 axruntime:146] [PA:0x40094000, PA:0x40097000) .data (READ | WRITE | RESERVED) 98 | [ 0.002707 0 axruntime:146] [PA:0x40097000, PA:0x40098000) .percpu (READ | WRITE | RESERVED) 99 | [ 0.002800 0 axruntime:146] [PA:0x40098000, PA:0x400d8000) boot stack (READ | WRITE | RESERVED) 100 | [ 0.002894 0 axruntime:146] [PA:0x400d8000, PA:0x400fb000) .bss (READ | WRITE | RESERVED) 101 | [ 0.002987 0 axruntime:146] [PA:0x9000000, PA:0x9001000) mmio (READ | WRITE | DEVICE | RESERVED) 102 | [ 0.003075 0 axruntime:146] [PA:0x8000000, PA:0x8020000) mmio (READ | WRITE | DEVICE | RESERVED) 103 | [ 0.003169 0 axruntime:146] [PA:0xa000000, PA:0xa004000) mmio (READ | WRITE | DEVICE | RESERVED) 104 | [ 0.003263 0 axruntime:146] [PA:0x10000000, PA:0x3eff0000) mmio (READ | WRITE | DEVICE | RESERVED) 105 | [ 0.003356 0 axruntime:146] [PA:0x4010000000, PA:0x4020000000) mmio (READ | WRITE | DEVICE | RESERVED) 106 | [ 0.003463 0 axruntime:146] [PA:0x400fb000, PA:0x48000000) free memory (READ | WRITE | FREE) 107 | [ 0.003573 0 axruntime:157] Initialize global memory allocator... 108 | [ 0.004438 0 axruntime:170] Initialize platform devices... 109 | [ 0.004510 0 axruntime:200] Primary CPU 0 init OK. 110 | Hello, hv! 111 | [ 0.007543 0 arceos_hv:212] physical memory: [0x70000000: 0x78000000) 112 | [ 0.008902 0 arceos_hv:78] vm run cpu0 113 | [ 0.009120 0 hypercraft::arch::exception:167] lower_aarch64_synchronous 114 | 115 | NN NN iii bb OOOOO SSSSS 116 | NNN NN mm mm mmmm bb OO OO SS 117 | NN N NN iii mmm mm mm bbbbbb OO OO SSSSS 118 | NN NNN iii mmm mm mm bb bb OO OO SS 119 | NN NN iii mmm mm mm bbbbbb OOOO0 SSSSS 120 | ___ ____ ___ ___ 121 | |__ \ / __ \ |__ \ |__ \ 122 | __/ / / / / / __/ / __/ / 123 | / __/ / /_/ / / __/ / __/ 124 | /____/ \____/ /____/ /____/ 125 | 126 | arch = aarch64 127 | platform = qemu-virt-arm 128 | build_mode = release 129 | log_level = warn 130 | 131 | Initializing kernel heap at: [0xffff00004010c0e0, 0xffff00004050c0e0) 132 | Initializing frame allocator at: [PA:0x4050d000, PA:0x48000000) 133 | Mapping .text: [0xffff000040080000, 0xffff000040094000) 134 | Mapping .rodata: [0xffff000040094000, 0xffff00004009b000) 135 | Mapping .data: [0xffff00004009b000, 0xffff000040106000) 136 | Mapping .bss: [0xffff00004010a000, 0xffff00004050d000) 137 | Mapping boot stack: [0xffff000040106000, 0xffff00004010a000) 138 | Mapping physical memory: [0xffff00004050d000, 0xffff000048000000) 139 | Mapping MMIO: [0xffff000009000000, 0xffff000009001000) 140 | Mapping MMIO: [0xffff000008000000, 0xffff000008020000) 141 | Initializing drivers... 142 | Initializing task manager... 143 | /**** APPS **** 144 | cyclictest 145 | exit 146 | fantastic_text 147 | forktest 148 | forktest2 149 | forktest_simple 150 | forktest_simple_c 151 | forktree 152 | hello_c 153 | hello_world 154 | matrix 155 | sleep 156 | sleep_simple 157 | stack_overflow 158 | thread_simple 159 | user_shell 160 | usertests 161 | yield 162 | **************/ 163 | Running tasks... 164 | test kernel task: pid = TaskId(2), arg = 0xdead 165 | test kernel task: pid = TaskId(3), arg = 0xbeef 166 | Rust user shell 167 | >> 168 | ``` 169 | ### 1.4.2 运行linux 170 | #### 编译linux镜像 171 | 1.4.2节运行的系统为linux。为了编译这个OS内核,执行以下指令: 172 | ```shell 173 | # set up cross-compile tools 174 | # download 175 | wget https://musl.cc/aarch64-linux-musl-cross.tgz 176 | # install 177 | tar zxf aarch64-linux-musl-cross.tgz 178 | # exec below command in bash 179 | export PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH 180 | # OR add below info in ~/.bashrc 181 | # echo PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH >> ~/.bashrc 182 | 183 | # download and unzip linux kernel 184 | wget https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/snapshot/linux-6.2.15.tar.gz 185 | tar -xvf linux-6.2.15.tar.gz 186 | cd linux-6.2.15 187 | 188 | # build linux 189 | mkdir build 190 | make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-musl- defconfig 191 | make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-musl- #-j4 192 | ``` 193 | #### 构建文件系统 194 | ```shell 195 | # use busybox to build rootfs 196 | # download 197 | wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 198 | tar xvf busybox-1.36.1.tar.bz2 199 | 200 | # compile busybox 201 | cd busybox-1.36.1 202 | mkdir build 203 | make O=build ARCH=arm64 defconfig 204 | make O=build ARCH=arm64 menuconfig 205 | ## select and save the following settings 206 | ## Settings -> [*] Don't use /usr 207 | ## Settings -> [*] Build static binary (no shared libs) 208 | ## Settings -> (aarch64-linux-musl-) Cross compiler prefix 209 | make O=build #-j4 210 | make O=build install 211 | 212 | # build rootfs 213 | cd build/_install && mkdir -pv {etc,proc,sys,dev,usr/{bin,sbin}} 214 | cd .. 215 | ## create a image 216 | dd if=/dev/zero of=rootfs.img bs=1M count=512 217 | ## format filesystem 218 | mkfs.ext4 rootfs.img 219 | ## mount filesystem 220 | mkdir tmp 221 | sudo mount rootfs.img tmp 222 | ## copy and create the content of the filesystem to mount point 223 | sudo cp -r _install/* tmp/ 224 | cd tmp/dev 225 | sudo mknod console c 5 1 226 | sudo mknod null c 1 3 227 | sudo mknod tty1 c 4 1 228 | sudo mknod tty2 c 4 1 229 | sudo mknod tty3 c 4 1 230 | sudo mknod tty4 c 4 1 231 | ## create /etc/fstab 232 | cd ../etc 233 | sudo vim fstab 234 | ## copy following content to fstab 235 | proc /proc proc defaults 0 0 236 | sysfs /sys sysfs defaults 0 0 237 | ## create /etc/init.d/rcS 238 | sudo mkdir init.d && cd init.d 239 | sudo vim rcS 240 | ## copy following content to rcS 241 | #!/bin/sh 242 | echo -e "Welcome to arceos Linux" 243 | mount -a 244 | echo -e "Remounting the root filesystem" 245 | ## create /etc/inittab 246 | cd .. 247 | sudo vim inittab 248 | ## copy following content to inittab 249 | # /etc/inittab 250 | ::sysinit:/etc/init.d/rcS 251 | console::respawn:-/bin/sh 252 | ::ctrlaltdel:/sbin/reboot 253 | ::shutdown:/bin/umount -a -r 254 | ## umount filesystem 255 | cd ../../ 256 | sudo umount tmp 257 | ``` 258 | #### 运行hypervisor 259 | 在qemu中运行arceOS,并在arceOS上起虚拟机。 260 | ```shell 261 | # clone arceos 262 | git clone https://github.com/arceos-hypervisor/arceos.git -b hypervisor --recurse-submodules 263 | cd arceos 264 | 265 | # move linux image and rootfs to arceos hv 266 | cp $(WORKSPACE)/linux-6.2.15/build/arch/arm64/boot/Image $(WORKSPACE)/arceos/apps/hv/guest/linux/linux-aarch64.bin 267 | cp $(WORKSPACE)/busybox-1.36.1/build/rootfs.img $(WORKSPACE)/arceos/apps/hv/guest/linux/rootfs-aarch64.img 268 | 269 | # set up rust tools 270 | cargo install cargo-binutils 271 | 272 | # build and run arceos aarch64 hypervisor 273 | make ARCH=aarch64 A=apps/hv HV=y LOG=info run 274 | ``` 275 | 启动后即可进入linux系统。 276 | ## 1.5 练习 277 | 1. 根据1.4节在arceos中运行nimbos 278 | 2. 阅读[Armv8-A Virtualization](https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Learn%20the%20Architecture/Armv8-A%20virtualization.pdf?revision=a765a7df-1a00-434d-b241-357bfda2dd31) Chapter 2(共4页),回答hypercraft是哪一种hypervisor。 -------------------------------------------------------------------------------- /hypervisor/aarch64/ch2-cpu-vcpu-vm.md: -------------------------------------------------------------------------------- 1 | # 2. CPU、VCpu与VM结构 2 | ## 2.1 CPU (crates/hypercraft/src/arch/aarch64/cpu.rs) 3 | 该结构对应于物理CPU,用于初始化物理CPU虚拟化所需内容,将其保存在栈上,并保存保存对应的VCpu,该部分主要用于后续多CPU体系的实现。 4 | ```rust 5 | pub struct PerCpu{ 6 | /// per cpu id 7 | pub cpu_id: usize, 8 | stack_top_addr: HostVirtAddr, 9 | /// save for correspond vcpus 10 | pub vcpu_queue: Mutex>, 11 | marker: core::marker::PhantomData, 12 | } 13 | ``` 14 | ### 2.1.1 成员变量 15 | - cpu_id: 当前物理CPU的id 16 | - stack_top_addr:存储该物理CPU的boot stack地址 17 | - vcpu_queue:存储当前物理CPU对应的VCpu 18 | ### 2.1.2 方法 19 | - pub fn init(boot_id: usize, stack_size: usize) -> HyperResult\<()\> 20 | - 为每个CPU分配内存页用于存储CPU的结构信息,并将当前运行的CPU id设置为boot CPU 21 | - pub fn setup_this_cpu(cpu_id: usize) -> HyperResult<()> 22 | - 将cpu_id对应的CPU存储在TPIDR_EL1中,作为当前运行的CPU 23 | - pub fn this_cpu() -> &'static mut PerCpu\ 24 | - 通过读取TPIDR_EL1寄存器,得到当前运行的CPU结构体 25 | - pub fn create_vcpu(&mut self, vcpu_id: usize) -> HyperResult> 26 | - 为当前CPU创建一个VCpu,加入到vcpu_queue中 27 | > TPIDR_EL1:EL1 Software Thread ID Register。提供一个用于存储线程标识信息的位置,以供在EL1特权级执行的软件进行操作系统管理。 28 | ## 2.2 VCpu(crates/hypercraft/src/arch/aarch64/vcpu.rs) 29 | 该结构对应于虚拟机运行的虚拟CPU,用于存储进入guest之前或退出guest时guest的Trap Context以及客户机系统寄存器的值,同时存储进入guest时host的Trap Context值。 30 | ```rust 31 | pub struct VCpu { 32 | /// Vcpu id 33 | pub vcpu_id: usize, 34 | /// Vcpu context 35 | pub regs: VmCpuRegisters, 36 | marker: PhantomData, 37 | } 38 | ``` 39 | ### 2.2.1 成员变量 40 | - vcpu_id: vcpu对应的id 41 | - regs:存储的相关寄存器状态 42 | ```rust 43 | pub struct VmCpuRegisters { 44 | /// guest trap context 45 | pub guest_trap_context_regs: ContextFrame, 46 | /// arceos context 47 | pub save_for_os_context_regs: ContextFrame, 48 | /// virtual machine system regs setting 49 | pub vm_system_regs: VmContext, 50 | } 51 | ``` 52 | - guest_trap_context_regs & save_for_os_context_regs 53 | - guest_trap_context_regs存储了进入guest之前或退出guest时guest的trap context,save_for_os_context_regs存储了进入guest之前arceos本身的trap context。 54 | - trap context具体内容见下,Aarch64ContextFrame实现于crates/hypercraft/src/arch/aarch64/context_frame.rs。 55 | ```rust 56 | pub struct Aarch64ContextFrame { 57 | pub gpr: [u64; 31], // 通用寄存器 58 | pub sp: u64, // 栈指针 59 | pub elr: u64, // 异常返回的地址。如果异常处理在EL1,则为ELR_EL1,如果异常处理在EL2,则为ELR_EL2 60 | pub spsr: u64, // 存储程序状态的寄存器 61 | } 62 | ``` 63 | - vm_system_regs 64 | - 用于存储guest的系统寄存器状态,VmContext实现于crates/hypercraft/src/arch/aarch64/context_frame.rs。 65 | ```rust 66 | pub struct VmContext { 67 | // generic timer 68 | pub cntvoff_el2: u64, 69 | cntp_cval_el0: u64, 70 | cntv_cval_el0: u64, 71 | pub cntkctl_el1: u32, 72 | pub cntvct_el0: u64, 73 | cntp_ctl_el0: u32, 74 | cntv_ctl_el0: u32, 75 | cntp_tval_el0: u32, 76 | cntv_tval_el0: u32, 77 | 78 | // vpidr and vmpidr 79 | vpidr_el2: u32, 80 | pub vmpidr_el2: u64, 81 | 82 | // 64bit EL1/EL0 register 83 | sp_el0: u64, 84 | sp_el1: u64, 85 | elr_el1: u64, 86 | spsr_el1: u32, 87 | pub sctlr_el1: u32, 88 | actlr_el1: u64, 89 | cpacr_el1: u32, 90 | ttbr0_el1: u64, 91 | ttbr1_el1: u64, 92 | tcr_el1: u64, 93 | esr_el1: u32, 94 | far_el1: u64, 95 | par_el1: u64, 96 | mair_el1: u64, 97 | amair_el1: u64, 98 | vbar_el1: u64, 99 | contextidr_el1: u32, 100 | tpidr_el0: u64, 101 | tpidr_el1: u64, 102 | tpidrro_el0: u64, 103 | 104 | // hypervisor context 105 | pub hcr_el2: u64, 106 | cptr_el2: u64, 107 | hstr_el2: u64, 108 | pub pmcr_el0: u64, 109 | pub vtcr_el2: u64, 110 | 111 | // exception 112 | far_el2: u64, 113 | hpfar_el2: u64, 114 | fpsimd: VmCtxFpsimd, 115 | pub gic_state: GicState, 116 | } 117 | ``` 118 | - 具体可分为以下几大类别,在进入guest之前需要设置的寄存器会单独进行说明。 119 | - 时钟相关的寄存器 120 | - 多处理器相关的寄存器 121 | - vmpidr_el2:用于存储虚拟多处理器ID 122 | - guest内部需要自用的系统寄存器 123 | - sctlr_el1:系统控制寄存器,用于进行一些系统设置,包括内存系统 124 | - nTWE, bit [18] 125 | - 0:如果在 EL0 执行的 WFE 指令会导致执行被暂停,例如事件寄存器未设置且没有待处理的 WFE 唤醒事件,则会使用 0x1 ESR 代码将其视为对EL1 的异常。 126 | - 1:WFE指令正常运行。 127 | - nTWI, bit [16] 128 | - 0:如果在 EL0 执行的 WFI 指令会导致执行被暂停,例如事件寄存器未设置且没有待处理的 WFE 唤醒事件,则会使用 0x1 ESR 代码将其视为对EL1 的异常。 129 | - 1:WFI指令正常运行。 130 | - CP15BEN, bit [5] 131 | - 0:aarch32 CP15 barrier指令禁用,编码为UNDEFINED。 132 | - 1:aarch32 CP15 barrier指令启用。 133 | - SA0, bit [4] 134 | - 当该bit启用时,EL0 的加载/存储指令中堆栈指针作为基地址的使用必须对齐到 16 字节边界,否则将引发堆栈对齐故障异常。 135 | - hypervisor设置相关的寄存器 136 | - vtcr_el2:用于设置二阶段页表第二阶段翻译的相关参数 137 | - PS, bits [18:16]:第二阶段翻译的物理地址大小 138 | - TG0, bits [15:14]:VTTBR_EL2的页面粒度大小 139 | - SH0, bits [13:12]:用VTTBR_EL2或VSTTBR_EL2进行table walks时关联的内存的共享属性 140 | - ORGN0, bits [11:10]:用VTTBR_EL2或VSTTBR_EL2进行table walks时关联的内存的外部缓存属性 141 | - IRGN0, bits [9:8]:用VTTBR_EL2或VSTTBR_EL2进行table walks时关联的内存的内部缓存属性 142 | - SL0, bits [7:6]:第二阶段翻译查询的开始级别 143 | - T0SZ, bits [5:0]:VTTBR_EL2 寻址的内存区域的大小偏移量,区域大小为2^(64-T0SZ)字节。 144 | - hcr_el2:用于进行进行虚拟化的配置,比如设置哪些操作会被trap到EL2。由于目前hypervisor的实现中断控制器也以直通的方式进行虚拟化,所以未设置异常路由到EL2。 145 | - RW, bit [31]: 146 | - 0b0:Lower EL都为aarch32。 147 | - 0b1:在EL1的执行状态为aarch64,EL0的执行状态由PSTATE.nRW当前的值决定。 148 | - VM, bit [0]: 149 | - 0b0:禁用EL1&0 二阶段地址翻译。 150 | - 0b1:启用EL1&0 二阶段地址翻译。 151 | - 异常处理相关的寄存器 152 | 153 | ### 2.2.2 方法 154 | - pub fn new(id: usize) -> Self 155 | - 创建一个新的VCpu。这个方法用于CPU的create_vcpu方法进行调用。 156 | - pub fn init(&mut self, kernel_entry_point: usize, device_tree_ipa: usize):初始化VCpu的相关寄存器 157 | - fn vcpu_arch_init(&mut self, kernel_entry_point: usize, device_tree_ipa: usize) 158 | - 设置guest trap context,将x0寄存器设置为device_tree_ipa,将elr(返回地址)设置为guest内核入口地址。 159 | - 设置spsr 160 | - SPSR_EL1::M::EL1h:设置异常级别为EL1,并且设置由EL确定用的SP是哪个EL。 161 | - SPSR_EL1::I::Masked:IRQ被屏蔽。 162 | - SPSR_EL1::F::Masked:FIQ被屏蔽。 163 | - SPSR_EL1::A::Masked:SError中断被屏蔽。 164 | - SPSR_EL1::D::Masked:目标在当前EL的Watchpoint、Breakpoint 和 Software Step 异常被屏蔽。 165 | - fn init_vm_context(&mut self) 166 | - 对sctlr_el1、vtcr_el2、hcr_el2、vmpidr_el2寄存器进行设置。(详见vm_system_regs处解释) 167 | - pub fn vcpu_ctx_addr(&self) -> usize 168 | - 获取当前vcpu regs成员的地址 169 | - pub fn run(&self, vttbr_token: usize) -> ! 170 | - 利用hvc call进入EL2进行相关寄存器设置后,返回guest kernel entry point开始执行。详情可见[第五章](./ch5-boot-process.md)。 171 | ## 2.3 VM(crates/hypercraft/src/arch/aarch64/vm.rs) 172 | 该结构对应于guest VM运行所需的一些信息,用于存储VM底层对应的VCpus以及VM对应的页表信息。 173 | ### 2.3.1 成员变量 174 | ```rust 175 | pub struct VM { 176 | /// The vcpus belong to VM 177 | vcpus: VmCpus, 178 | /// The guest page table of VM 179 | gpt: G, 180 | /// VM id 181 | vm_id: usize, 182 | } 183 | ``` 184 | - vcpus: VmCpus\:VM对应的VCpus。 185 | ```rust 186 | pub struct VmCpus { 187 | inner: [Once>; VM_CPUS_MAX], 188 | marker: core::marker::PhantomData, 189 | } 190 | ``` 191 | - VCpus实现于crates/hypercraft/src/vcpus.rs,定义了包含多个VCpu的数组。目前尚未实现多VCpu的支持。 192 | - gpt: G:VM的Guest Page Table,即二阶段翻译中第二阶段(ipa → hpa)对应的页表。具体详情会在[第四章](./ch4-memory-virtualization.md)中介绍。 193 | - vm_id: usize:VM对应的id。 194 | ### 2.3.2 方法 195 | - pub fn new() -> Self 196 | - 创建一个新的VM结构。 197 | - pub fn init_vm_vcpu(&mut self, vcpu_id:usize, kernel_entry_point: usize, device_tree_ipa: usize) 198 | - 找到对应vcpu_id的vcpu,调用vcpu的init方法,将guest kernel入口点以及dtb的ipa作为参数传入,初始化vcpu的相关寄存器设置。 199 | - pub fn run(&mut self, vcpu_id: usize) 200 | - 运行该VM。获取对应vcpu_id的vcpu,并且找到guest page table的基址,作为参数传给vcpu的run方法,启动该虚拟机。具体在[第五章](./ch5-boot-process.md)中进行介绍。 201 | ## 2.4 练习 202 | 1. 在合适的地方修改代码,打印出在vcpu初始化前后regs中guest_trap_context_regs与vm_system_regs各个寄存器的值。比较它们哪些值发生了变化。并根据本章节介绍的内容,对照[Arm A-profile Architecture Registers](https://developer.arm.com/documentation/ddi0601/latest/),说明值变化的寄存器的作用。 -------------------------------------------------------------------------------- /hypervisor/aarch64/ch3-exception-hvc.md: -------------------------------------------------------------------------------- 1 | # 3. AARCH64异常处理与HVC指令介绍及其Handler实现 2 | arceos运行在EL1,然而我们需要设置EL2的一些寄存器,所以需要从el1通过hvc陷入el2进行虚拟机相关配置后,再利用eret进入guest执行guest kernel的内容。这部分将介绍aarch64的异常处理与hvc指令,以及这部分的代码实现。 3 | ## 3.1 AARCH64异常处理 4 | - 硬件处理部分(发生异常时或主动调用触发异常指令,如svc, hvc, smc) 5 | - 更新SPSR_ELn,保存PSTATE信息,需要在异常结束返回时恢复该内容 6 | - 更新PSTATE,反映出当前处理器的状态,比如EL提升 7 | - 异常返回的地址被存储在ELR_ELn中 8 | - 跳转到异常向量表执行对应的异常处理函数 9 | - **注:寄存器的_ELn后缀代表在每个异常等级都有该寄存器的不同拷贝,比如SPSR_EL1和SPSR_EL2时两个不同的物理寄存器** 10 | - 软件处理部分 11 | - 保存现场(上下文) 12 | - 根据异常类型执行对应的异常处理函数 13 | - 恢复现场 (上下文) 14 | - eret 15 | - 硬件处理部分(执行eret) 16 | - 把SPSR_ELn恢复到PSTATE中 17 | - 把PC设置为ELR_ELn,跳转执行 18 | ## 3.2 HVC 19 | - 生成一个目标是EL2的异常。 20 | - HVC #\:其中bits(16) imm = imm16; 21 | - 该imm会存于ESR_EL2的imm16(bits [15:0])。ESR_EL2:用于存储目标EL为EL2发生的异常的相关信息。 22 | ## 3.3 相关代码实现 23 | #### 3.3.1 EL2异常向量表(modules/axhal/src/aarch64/trap_el2.S) 24 | ```rust 25 | .section .text 26 | .p2align 11 27 | .global exception_vector_base_el2 28 | exception_vector_base_el2: 29 | // current EL, with SP_EL0 30 | INVALID_EXCP_EL2 0 0 31 | INVALID_EXCP_EL2 1 0 32 | INVALID_EXCP_EL2 2 0 33 | INVALID_EXCP_EL2 3 0 34 | 35 | // current EL, with SP_ELx 36 | INVALID_EXCP_EL2 1 1 37 | HANDLE_IRQ_EL2 38 | INVALID_EXCP_EL2 2 1 39 | INVALID_EXCP_EL2 3 1 40 | 41 | // lower EL, aarch64 42 | HANDLE_LOWER_SYNC 43 | HANDLE_IRQ_EL2 44 | INVALID_EXCP_EL2 2 2 45 | INVALID_EXCP_EL2 3 2 46 | 47 | // lower EL, aarch32 48 | INVALID_EXCP_EL2 0 3 49 | INVALID_EXCP_EL2 1 3 50 | INVALID_EXCP_EL2 2 3 51 | INVALID_EXCP_EL2 3 3 52 | ``` 53 | - aarch64的异常向量表一共分为四组,每组中又有四个类别的异常处理,具体如下 54 | - 四组 55 | - current el with sp_el0: 没有发生Exception level切换,且SP使用的是SP_EL0 56 | - current el with sp_elx: 没有发生Exception level切换,且SP使用的是SP_ELx(x=1,2,3) 57 | - lower el aarch64: 发生Exception level切换,且target level使用的是aarch64 58 | - lower el aarch32: 发生Exception level切换,且target level使用的是aarch32 59 | - 四个类别 60 | - 同步异常(Synchronous Exception):直接由指令执行触发,返回地址指示了是哪一条指令造成了该异常。如HVC系统调用指令、Data Abort、Instruction Abort等。 61 | - 异步异常(Asynchronous Exception) 62 | - IRQ异常:普通中断,优先级低于FIQ。 63 | - FIQ异常:快速中断,优先级高于IRQ。外部硬件会发出中断请求信号,当当前指令执行完毕时,将引发相应的异常类型。 64 | - SError异常:System Error。如异步数据异常或部分处理器外部引脚触发等。 65 | ##### HANDLE_LOWER_SYNC 66 | - 上述向量表中的INVALID_EXCP_EL2、HANDLE_IRQ_EL2、HANDLE_LOWER_SYNC均为定义在trap_el2.S中的宏。这几个宏的定义流程大致相同,此处详细说明HANDLE_LOWER_SYNC的实现,其余两个类同。 67 | ```rust 68 | .macro HANDLE_LOWER_SYNC 69 | .p2align 7 70 | SAVE_REGS_EL2 71 | mov x0, sp 72 | bl lower_aarch64_synchronous 73 | b .Lexception_return_el2 74 | .endm 75 | ``` 76 | ###### SAVE_REGS_EL2 77 | ```rust 78 | .macro SAVE_REGS_EL2 79 | sub sp, sp, 34 * 8 80 | stp x0, x1, [sp] 81 | stp x2, x3, [sp, 2 * 8] 82 | stp x4, x5, [sp, 4 * 8] 83 | stp x6, x7, [sp, 6 * 8] 84 | stp x8, x9, [sp, 8 * 8] 85 | stp x10, x11, [sp, 10 * 8] 86 | stp x12, x13, [sp, 12 * 8] 87 | stp x14, x15, [sp, 14 * 8] 88 | stp x16, x17, [sp, 16 * 8] 89 | stp x18, x19, [sp, 18 * 8] 90 | stp x20, x21, [sp, 20 * 8] 91 | stp x22, x23, [sp, 22 * 8] 92 | stp x24, x25, [sp, 24 * 8] 93 | stp x26, x27, [sp, 26 * 8] 94 | stp x28, x29, [sp, 28 * 8] 95 | 96 | mov x1, sp 97 | add x1, x1, #(0x110) 98 | stp x30, x1, [sp, 30 * 8] 99 | mrs x10, elr_el2 100 | mrs x11, spsr_el2 101 | stp x10, x11, [sp, 32 * 8] 102 | .endm 103 | ``` 104 | - 首先调用同样定义在trap_el2.S中的宏SAVE_REGS_EL2。这个宏的作用是把当前异常发生的上下文存储在栈上。如代码所示,将栈指针sp减去存放上下文(即为第二章中说明的Aarch64ContextFrame)的大小,把gpr、sp、elr、spsr存放到栈上。 105 | ###### mov x0, sp 106 | - 该语句是将当前sp存放于x0。由于我们之前在栈上已经存储了上下文,所以sp当前指向的刚好是我们保存的上下文内容。此处我们是为了利用x0传递参数,把保存的上下文内容作为参数传给下面的lower_aarch64_synchronous函数. 107 | ###### lower_aarch64_synchronous(crates/hypercraft/src/arch/aarch64/exception.rs) 108 | ```rust 109 | pub extern "C" fn lower_aarch64_synchronous(ctx: &mut ContextFrame) { 110 | info!("lower_aarch64_synchronous"); 111 | match exception_class() { 112 | ... 113 | 0x16 => { 114 | hvc_handler(ctx); 115 | } 116 | ... 117 | } 118 | } 119 | ``` 120 | - lower_aarch64_synchronous函数定义于crates/hypercraft/src/arch/aarch64/exception.rs中。可以通过ESR_EL2寄存器获取当前异常的类别,通过类别匹配不同的handler。此处省略了其他异常类别的情况,主要关注hvc_handler。 121 | - hvc_handler(crates/hypercraft/src/arch/aarch64/sync.rs) 122 | - hvc_handler定义于crates/hypercraft/src/arch/aarch64/sync.rs中。在我们进行hvc调用的时候,会将参数传入x0~x7寄存器,其中x7是作为所有hvc调用的种类和类别,目前只实现了HVC_SYS类别的HVC_SYS_BOOT事件,其中HVC_SYS_BOOT事件包含两个参数,x0为第二阶段翻译的页表基址,x1为对应要运行VM的VCpu的regs。 123 | - hvc_handler定义于crates/hypercraft/src/arch/aarch64/sync.rs中。在我们进行hvc调用的时候,会将参数传入x0~x7寄存器,其中x7是作为所有hvc调用的种类和类别,目前只实现了HVC_SYS类别的HVC_SYS_BOOT事件,其中HVC_SYS_BOOT事件包含两个参数,x0为第二阶段翻译的页表基址,x1为对应要运行VM的VCpu的regs。 124 | ```rust 125 | pub fn hvc_handler(ctx: &mut ContextFrame) { 126 | let x0 = ctx.gpr(0); 127 | let x1 = ctx.gpr(1); 128 | let x2 = ctx.gpr(2); 129 | let x3 = ctx.gpr(3); 130 | let x4 = ctx.gpr(4); 131 | let x5 = ctx.gpr(5); 132 | let x6 = ctx.gpr(6); 133 | let mode = ctx.gpr(7); 134 | 135 | let hvc_type = (mode >> 8) & 0xff; 136 | let event = mode & 0xff; 137 | 138 | match hvc_guest_handler(hvc_type, event, x0, x1, x2, x3, x4, x5, x6) { 139 | Ok(val) => { 140 | ctx.set_gpr(HVC_RETURN_REG, val); 141 | } 142 | Err(_) => { 143 | warn!("Failed to handle hvc request fid 0x{:x} event 0x{:x}", hvc_type, event); 144 | ctx.set_gpr(HVC_RETURN_REG, usize::MAX); 145 | } 146 | } 147 | if hvc_type==HVC_SYS && event== HVC_SYS_BOOT { 148 | unsafe { 149 | let regs: &mut VmCpuRegisters = core::mem::transmute(x1); // x1 is the vm regs context 150 | // save arceos context 151 | regs.save_for_os_context_regs.gpr = ctx.gpr; 152 | regs.save_for_os_context_regs.sp = ctx.sp; 153 | regs.save_for_os_context_regs.elr = ctx.elr; 154 | regs.save_for_os_context_regs.spsr = ctx.spsr; 155 | 156 | ctx.gpr = regs.guest_trap_context_regs.gpr; 157 | ctx.sp = regs.guest_trap_context_regs.sp; 158 | ctx.elr = regs.guest_trap_context_regs.elr; 159 | ctx.spsr = regs.guest_trap_context_regs.spsr; 160 | } 161 | } 162 | } 163 | ``` 164 | - hvc_handler会调用hvc_guest_handler找到对应的类别以及事件执行具体的函数。以当前实现的HVC_SYS_BOOT事件为例,最终会调用实现在crates/hypercraft/src/arch/aarch64/hvc.rs中的init_hv函数。 165 | - init_hv(crates/hypercraft/src/arch/aarch64/hvc.rs) 166 | ```rust 167 | fn init_hv(root_paddr: usize, vm_ctx_addr: usize) { 168 | // cptr_el2: Condtrols trapping to EL2 for accesses to the CPACR, Trace functionality 169 | // an registers associated with floating-point and Advanced SIMD execution. 170 | unsafe { 171 | core::arch::asm!(" 172 | mov x3, xzr // Trap nothing from EL1 to El2. 173 | msr cptr_el2, x3" 174 | ); 175 | } 176 | msr!(VTTBR_EL2, root_paddr); 177 | unsafe { 178 | core::arch::asm!(" 179 | tlbi alle2 // Flush tlb 180 | dsb nsh 181 | isb" 182 | ); 183 | } 184 | 185 | let regs: &VmCpuRegisters = unsafe{core::mem::transmute(vm_ctx_addr)}; 186 | // set vm system related register 187 | regs.vm_system_regs.ext_regs_restore(); 188 | } 189 | ``` 190 | - init_hv函数首先会设置cptr_el2,禁止从el1 trap任何到el2。接着在init_page_table中会设置vttbr_el2为guest page table的root物理地址,而后会刷新TLB。然后通过vm_ctx_addr指针获取到之前设置的vm的相关寄存器,调用VmContext的ext_regs_restore方法把之前设置的和虚拟机相关的系统寄存器真正写入到物理寄存器中。 191 | - 注意:在if hvc_type == HVC_SYS && event == HVC_SYS_BOOT这个条件语句中,会保存目前arceos触发这个异常时的寄存器,同时会把当前栈上的上下文覆盖为guest trap context。因为当异常处理完毕用eret返回时,我们需要直接跳转到guest kernel entry执行,所以此处需要将原本的上下文修改为之前vcpu初始化的guest上下文。 192 | ###### .Lexception_return_el2 193 | ```rust 194 | .Lexception_return_el2: 195 | RESTORE_REGS_EL2 196 | eret 197 | ``` 198 | - .Lexception_return_el2首先会调用同样定义在trap_el2.S的RESTORE_REGS_EL2宏。 199 | - RESTORE_REGS_EL2 200 | ```rust 201 | .macro RESTORE_REGS_EL2 202 | ldp x10, x11, [sp, 32 * 8] 203 | msr elr_el2, x10 204 | msr spsr_el2, x11 205 | 206 | ldp x30, [sp, 30 * 8] 207 | ldp x28, x29, [sp, 28 * 8] 208 | ldp x26, x27, [sp, 26 * 8] 209 | ldp x24, x25, [sp, 24 * 8] 210 | ldp x22, x23, [sp, 22 * 8] 211 | ldp x20, x21, [sp, 20 * 8] 212 | ldp x18, x19, [sp, 18 * 8] 213 | ldp x16, x17, [sp, 16 * 8] 214 | ldp x14, x15, [sp, 14 * 8] 215 | ldp x12, x13, [sp, 12 * 8] 216 | ldp x10, x11, [sp, 10 * 8] 217 | ldp x8, x9, [sp, 8 * 8] 218 | ldp x6, x7, [sp, 6 * 8] 219 | ldp x4, x5, [sp, 4 * 8] 220 | ldp x2, x3, [sp, 2 * 8] 221 | ldp x0, x1, [sp] 222 | add sp, sp, 34 * 8 223 | .endm 224 | ``` 225 | - RESTORE_REGS_EL2与SAVE_REGS_EL2功能相反,利用ldp指令把全部栈上的上下文的内容重新放回到对应的寄存器中。 226 | - 恢复上下文后,调用eret,返回到触发异常前的EL,跳转到ELR_ELx设定的位置开始执行。 227 | #### 3.3.2 异常向量基址寄存器的设定 228 | - 为了触发异常后能够正确地跳转到异常向量表的地址,需要在之前设定异常向量基址寄存器VBAR_ELx,此处的x指的是异常触发的target level。由于上述异常向量表是为了hypervisor需要处理的一些异常实现的,所以我们需要设定VBAR_EL2。具体代码实现于modules/axhal/src/platform/aarch64_common/boot.rs的_start函数中。 229 | ```rust 230 | ldr x8, ={exception_vector_base_el2} // setup vbar_el2 for hypervisor 231 | msr vbar_el2, x8 232 | ``` 233 | ## 3.4 练习 234 | 1. 绘制出在系统中触发hvc异常后程序执行的流程图。(可认为hvc_type为HVC_SYS,hvc_event为HVC_SYS_BOOT) 235 | 236 | 2. 如果在_start函数中,把异常向量基址寄存器的设定(设定VBAR_EL2)放到“bl {switch_to_el1}”后,会发生什么?为什么会这样?(进阶练习,可选) 237 | - 提示:可利用make ARCH=aarch64 A=apps/hv HV=y LOG=debug GUEST=nimbos build`进行编译后,利用以下指令进行qemu debug信息输出: 238 | ```shell 239 | qemu-system-aarch64 -m 3G -smp 1 -cpu cortex-a72 -machine virt -kernel apps/hv/hv_qemu-virt-aarch64.bin -device loader,file=apps/hv/guest/nimbos/nimbos-aarch64.dtb,addr=0x70000000,force-raw=on -device loader,file=apps/hv/guest/nimbos/nimbos-aarch64.bin,addr=0x70200000,force-raw=on -machine virtualization=on,gic-version=2 -nographic -d int,in_asm -D qemu.log 240 | ``` 241 | - 此时代码运行会呈现卡住状态,需要用ctrl+a后按x退出。关注log中第一条异常信息,对照[Arm A-profile Architecture Registers](https://developer.arm.com/documentation/ddi0601/latest/)查阅ESR的错误信息,回答问题。 242 | 243 | 3. 为现有代码添加一个新的hvc_type与hvc_event及其handler,通过x0寄存器作为参数,传递一个数字,在handler中打印出"hello+数字"。并在合适的地方调用这个hvc call作为测试。(挑战练习,可选) -------------------------------------------------------------------------------- /hypervisor/aarch64/ch4-memory-virtualization.md: -------------------------------------------------------------------------------- 1 | # 4. 内存虚拟化介绍与实现 2 | ## 4.1 AARCH64内存虚拟化 3 | ![arm地址翻译](./img/translationstage.png) 4 | - 上图展示了aarch64架构下的各种翻译类型,我们内存虚拟化用到了最后一行的VA→IPA→PA的流程。具体如下图所示。 5 | ![二阶段地址翻译](./img/stage2translation.png) 6 | 处理器在EL1或EL0特权级下运行时,地址转换机制由操作系统(OS)设置和控制。在没有虚拟化的系统中,此机制用于将虚拟地址转换为物理地址。在虚拟化系统中,第一阶段会将Guest Virtual Address(GPA)转换为Intermediate Physical Address(IPA),因为它随后会将IPA转换为Host Physical Address(HPA),即第2阶段(Stage 2)的转换。 7 | - 涉及的相关寄存器: 8 | - TTBR0_EL1,TTBR1_EL1,TCR_EL1:该部分寄存器由guest OS负责设置,guest负责第一阶段的翻译。其中TTBR0_EL1与TTBR1_EL1为页表基址寄存器,TCR_EL1用于对翻译过程进行一些配置,如地址空间大小、页面大小等。 9 | - VTTBR_EL2,VTCR_EL2:该部分寄存器由hypervisor负责设置,hypervisor负责第二阶段的翻译。其中VTTBR_EL2为第二阶段页表基址寄存器,VTCR_EL2用于对翻译过程进行配置,如地址空间大小、页面大小等。 10 | ## 4.2 代码实现 11 | ### 4.2.1 (aarch64) NestedPageTable (crates/hypercraft/src/arch/aarch64/ept.rs) 12 | ```rust 13 | pub struct A64HVPagingMetaData; 14 | 15 | impl PagingMetaData for A64HVPagingMetaData { 16 | const LEVELS: usize = 3; 17 | const PA_MAX_BITS: usize = 48; // In Armv8.0-A, the maximum size for a physical address is 48 bits. 18 | 19 | // The size of the IPA space can be configured in the same way as the 20 | const VA_MAX_BITS: usize = 36; // virtual address space. VTCR_EL2.T0SZ controls the size. 21 | } 22 | /// According to rust shyper, AArch64 translation table. 23 | pub type NestedPageTable = PageTable64; 24 | ``` 25 | - PageTable64 (基于arceos的页表,定义于crates/page_table/src/bi64.rs) 26 | - 成员变量上定义了根物理地址,以及对应的页表映射数组。 27 | - 方法上定义了新建页表、返回根地址、映射、取消映射、根据虚拟地址查询物理地址以及页表权限、遍历。 28 | - A64HVPagingMetaData (实现了PagingMetaData trait,实现于crates/hypercraft/src/arch/aarch64/ept.rs) 29 | - LEVELS:定义有3级页表。 30 | - PA_MAX_BITS:定义物理地址最大有48位。 31 | - VA_MAX_BITS:定义虚拟地址最大有36位。 32 | - A64PTE (基于arceos aarch64的页表项,定义于crates/page_table_entry/src/arch/aarch64.rs) 33 | - 成员变量上定义了PHYS_ADDR_MASK(用于在页表描述符中获取Physical Address) 34 | - 方法上定义了新建页面、新建页表、获取权限Flag等,使其能够在PageTable64进行映射、取消映射、查询、遍历等方法时进行调用。 35 | - **注:在页表描述符中需要注意,第一阶段和第二阶段翻译的部分Flag赋值会不同,例如AP bit,需要根据阶段选择正确的赋值,可在官方文档进行查询。** 36 | - I (PagingIf,在具体执行过程,被实例化为GuestPagingIfImpl,即为PagingIfImpl,该结构定义于modules/axhal/src/paging.rs) 37 | - 定义了分配页帧、回收页帧等方法 38 | ### 4.2.2 GuestPageTable (modules/axruntime/src/gpm.rs) 39 | ```rust 40 | pub struct GuestPageTable(NestedPageTable); 41 | ``` 42 | 利用GuestPagingIfImpl对NestedPageTable进行实例化,主要用于guest页表的创建、映射、查询等功能。 43 | - fn new() -> HyperResult\ 44 | - 调用PageTable64的try_new()方法进行页表内存分配 45 | - fn map( &mut self, gpa: GuestPhysAddr, hpa: hypercraft::HostPhysAddr, flags: MappingFlags) -> HyperResult<()> 46 | - 调用PageTable64的map()方法在gpt建立映射条目 47 | - fn unmap(&mut self, gpa: GuestPhysAddr) -> HyperResult<()> 48 | - 调用PageTable64的unmap()方法取消该区域的映射 49 | - fn translate(&self, gpa: GuestPhysAddr) -> HyperResult\ 50 | - 调用PageTable64的query()方法查询gpa对应的hpa 51 | - fn token(&self) -> usize 52 | - 返回页表基址,用于后续存储至第二阶段页表基址寄存器 53 | ## 4.3 练习 54 | 1. 假设在 guest OS 中启用了 3 级页表,如果 guest 的一次访存 (使用 guest 虚拟地址) 发生了 TLB 缺失,请问以aarch64 stage2 translation的方法实现内存虚拟化时,最多会导致多少次内存访问?(均为 3 级页表) 55 | 2. 在apps/hv/src/main.rs的set_gpm函数中,打印建立地址映射前后查询ipa的结果。(进阶练习,可选) -------------------------------------------------------------------------------- /hypervisor/aarch64/img/boot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/aarch64/img/boot.png -------------------------------------------------------------------------------- /hypervisor/aarch64/img/changeEL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/aarch64/img/changeEL.png -------------------------------------------------------------------------------- /hypervisor/aarch64/img/el.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/aarch64/img/el.png -------------------------------------------------------------------------------- /hypervisor/aarch64/img/hvcflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/aarch64/img/hvcflow.png -------------------------------------------------------------------------------- /hypervisor/aarch64/img/page_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/aarch64/img/page_table.png -------------------------------------------------------------------------------- /hypervisor/aarch64/img/stage2translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/aarch64/img/stage2translation.png -------------------------------------------------------------------------------- /hypervisor/aarch64/img/translationstage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/aarch64/img/translationstage.png -------------------------------------------------------------------------------- /hypervisor/aarch64/multi-core.md: -------------------------------------------------------------------------------- 1 | ## 运行 2 | 假设我们以下操作都是在$(WORKSPACE)目录下进行操作。 3 | ### 1.1 编译linux镜像 4 | 1.4.2节运行的系统为linux。为了编译这个OS内核,执行以下指令: 5 | ```shell 6 | # set up cross-compile tools 7 | # download 8 | wget https://musl.cc/aarch64-linux-musl-cross.tgz 9 | # install 10 | tar zxf aarch64-linux-musl-cross.tgz 11 | # exec below command in bash 12 | export PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH 13 | # OR add below info in ~/.bashrc 14 | # echo PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH >> ~/.bashrc 15 | 16 | # download and unzip linux kernel 17 | wget https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/snapshot/linux-6.2.15.tar.gz 18 | tar -xvf linux-6.2.15.tar.gz 19 | cd linux-6.2.15 20 | 21 | # build linux 22 | mkdir build 23 | make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-musl- defconfig 24 | make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-musl- #-j4 25 | ``` 26 | ### 1.2 构建文件系统 27 | ```shell 28 | # use busybox to build rootfs 29 | # download 30 | wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 31 | tar xvf busybox-1.36.1.tar.bz2 32 | 33 | # compile busybox 34 | cd busybox-1.36.1 35 | mkdir build 36 | make O=build ARCH=arm64 defconfig 37 | make O=build ARCH=arm64 menuconfig 38 | ## select and save the following settings 39 | ## Settings -> [*] Don't use /usr 40 | ## Settings -> [*] Build static binary (no shared libs) 41 | ## Settings -> (aarch64-linux-musl-) Cross compiler prefix 42 | make O=build #-j4 43 | make O=build install 44 | 45 | # build rootfs 46 | cd build/_install && mkdir -pv {etc,proc,sys,dev,usr/{bin,sbin}} 47 | cd .. 48 | ## create a image 49 | dd if=/dev/zero of=rootfs.img bs=1M count=512 50 | ## format filesystem 51 | mkfs.ext4 rootfs.img 52 | ## mount filesystem 53 | mkdir tmp 54 | sudo mount rootfs.img tmp 55 | ## copy and create the content of the filesystem to mount point 56 | sudo cp -r _install/* tmp/ 57 | cd tmp/dev 58 | sudo mknod console c 5 1 59 | sudo mknod null c 1 3 60 | sudo mknod tty1 c 4 1 61 | sudo mknod tty2 c 4 1 62 | sudo mknod tty3 c 4 1 63 | sudo mknod tty4 c 4 1 64 | ## create /etc/fstab 65 | cd ../etc 66 | sudo vim fstab 67 | ## copy following content to fstab 68 | proc /proc proc defaults 0 0 69 | sysfs /sys sysfs defaults 0 0 70 | ## create /etc/init.d/rcS 71 | sudo mkdir init.d && cd init.d 72 | sudo vim rcS 73 | ## copy following content to rcS 74 | #!/bin/sh 75 | echo -e "Welcome to arceos Linux" 76 | mount -a 77 | echo -e "Remounting the root filesystem" 78 | ## create /etc/inittab 79 | cd .. 80 | sudo vim inittab 81 | ## copy following content to inittab 82 | # /etc/inittab 83 | ::sysinit:/etc/init.d/rcS 84 | console::respawn:-/bin/sh 85 | ::ctrlaltdel:/sbin/reboot 86 | ::shutdown:/bin/umount -a -r 87 | ## umount filesystem 88 | cd ../../ 89 | sudo umount tmp 90 | ``` 91 | ### 1.3 编译hypervisor 92 | ```shell 93 | # clone arceos 94 | git clone https://github.com/arceos-hypervisor/arceos.git -b multicore-aarch64 --recurse-submodules 95 | cd arceos 96 | 97 | # set up rust tools 98 | cargo install cargo-binutils 99 | 100 | # build and run arceos aarch64 hypervisor 101 | make A=apps/hv ARCH=aarch64 HV=y LOG=info SMP=2 build 102 | ``` 103 | ### 1.4 运行hypervisor 104 | 在qemu中运行arceOS,并在arceOS上起虚拟机。在0号核(主核)上启动Linux虚拟机,在1号核(副核)上启动Nimbos虚拟机。 105 | ```shell 106 | # move nimbos image to arceos hv 107 | cp $(WORKSPACE)/nimbos/kernel/target/aarch64/release/nimbos.bin apps/hv/guest/nimbos/nimbos-aarch64.bin 108 | 109 | # move linux image and rootfs to arceos hv 110 | cp $(WORKSPACE)/linux-6.2.15/build/arch/arm64/boot/Image $(WORKSPACE)/arceos/apps/hv/guest/linux/linux-aarch64.bin 111 | cp $(WORKSPACE)/busybox-1.36.1/build/rootfs.img $(WORKSPACE)/arceos/apps/hv/guest/linux/rootfs-aarch64.img 112 | 113 | # run linux(virtio) 114 | qemu-system-aarch64 -m 3G -smp 2 -cpu cortex-a72 -machine virt -kernel apps/hv/hv_qemu-virt-aarch64.bin -device loader,file=apps/hv/guest/linux/linux-multi-core.dtb,addr=0x70000000,force-raw=on -device loader,file=apps/hv/guest/linux/linux-aarch64.bin,addr=0x70200000,force-raw=on -machine virtualization=on,gic-version=2 -drive if=none,file=apps/hv/guest/linux/rootfs-aarch64.img,format=raw,id=hd0 -device virtio-blk-device,drive=hd0 -nographic 115 | ``` 116 | 117 | ## 注意 118 | - 目前还未完整编写好启动脚本,所以直接用qemu命令启动,修改完成后重新编译dts到dtb,多核对应的dts文件为aarch64-test.dts。 119 | -------------------------------------------------------------------------------- /hypervisor/aarch64/multi-vm.md: -------------------------------------------------------------------------------- 1 | ## 运行 2 | 假设我们以下操作都是在$(WORKSPACE)目录下进行操作。 3 | ### 1.1 编译nimbos镜像 4 | 1.4.1节运行的OS为[NimbOS](https://github.com/equation314/nimbos)。为了编译这个OS内核,执行以下指令: 5 | ```shell 6 | # set up cross-compile tools 7 | # download 8 | wget https://musl.cc/aarch64-linux-musl-cross.tgz 9 | # install 10 | tar zxf aarch64-linux-musl-cross.tgz 11 | # exec below command in bash 12 | export PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH 13 | # OR add below info in ~/.bashrc 14 | # echo PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH >> ~/.bashrc 15 | 16 | # clone nimbos 17 | git clone https://github.com/equation314/nimbos.git 18 | cd nimbos/kernel 19 | 20 | # set up rust tools 21 | make env 22 | 23 | # build nimbos 24 | cd ../user && make ARCH=aarch64 build 25 | cd ../kernel && make build ARCH=aarch64 LOG=warn 26 | ``` 27 | 此时会在$(WORKSPACE)/nimbos/kernel/target/aarch64/release下看到编译好的nimbos.bin 28 | ### 1.2 编译linux镜像 29 | 1.4.2节运行的系统为linux。为了编译这个OS内核,执行以下指令: 30 | ```shell 31 | # set up cross-compile tools 32 | # download 33 | wget https://musl.cc/aarch64-linux-musl-cross.tgz 34 | # install 35 | tar zxf aarch64-linux-musl-cross.tgz 36 | # exec below command in bash 37 | export PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH 38 | # OR add below info in ~/.bashrc 39 | # echo PATH=`pwd`/aarch64-linux-musl-cross/bin:$PATH >> ~/.bashrc 40 | 41 | # download and unzip linux kernel 42 | wget https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/snapshot/linux-6.2.15.tar.gz 43 | tar -xvf linux-6.2.15.tar.gz 44 | cd linux-6.2.15 45 | 46 | # build linux 47 | mkdir build 48 | make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-musl- defconfig 49 | make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-musl- #-j4 50 | ``` 51 | ### 1.3 构建文件系统 52 | ```shell 53 | # use busybox to build rootfs 54 | # download 55 | wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 56 | tar xvf busybox-1.36.1.tar.bz2 57 | 58 | # compile busybox 59 | cd busybox-1.36.1 60 | mkdir build 61 | make O=build ARCH=arm64 defconfig 62 | make O=build ARCH=arm64 menuconfig 63 | ## select and save the following settings 64 | ## Settings -> [*] Don't use /usr 65 | ## Settings -> [*] Build static binary (no shared libs) 66 | ## Settings -> (aarch64-linux-musl-) Cross compiler prefix 67 | make O=build #-j4 68 | make O=build install 69 | 70 | # build rootfs 71 | cd build/_install && mkdir -pv {etc,proc,sys,dev,usr/{bin,sbin}} 72 | cd .. 73 | ## create a image 74 | dd if=/dev/zero of=rootfs.img bs=1M count=512 75 | ## format filesystem 76 | mkfs.ext4 rootfs.img 77 | ## mount filesystem 78 | mkdir tmp 79 | sudo mount rootfs.img tmp 80 | ## copy and create the content of the filesystem to mount point 81 | sudo cp -r _install/* tmp/ 82 | cd tmp/dev 83 | sudo mknod console c 5 1 84 | sudo mknod null c 1 3 85 | sudo mknod tty1 c 4 1 86 | sudo mknod tty2 c 4 1 87 | sudo mknod tty3 c 4 1 88 | sudo mknod tty4 c 4 1 89 | ## create /etc/fstab 90 | cd ../etc 91 | sudo vim fstab 92 | ## copy following content to fstab 93 | proc /proc proc defaults 0 0 94 | sysfs /sys sysfs defaults 0 0 95 | ## create /etc/init.d/rcS 96 | sudo mkdir init.d && cd init.d 97 | sudo vim rcS 98 | ## copy following content to rcS 99 | #!/bin/sh 100 | echo -e "Welcome to arceos Linux" 101 | mount -a 102 | echo -e "Remounting the root filesystem" 103 | ## create /etc/inittab 104 | cd .. 105 | sudo vim inittab 106 | ## copy following content to inittab 107 | # /etc/inittab 108 | ::sysinit:/etc/init.d/rcS 109 | console::respawn:-/bin/sh 110 | ::ctrlaltdel:/sbin/reboot 111 | ::shutdown:/bin/umount -a -r 112 | ## umount filesystem 113 | cd ../../ 114 | sudo umount tmp 115 | ``` 116 | ### 1.4 编译hypervisor 117 | ```shell 118 | # clone arceos 119 | git clone https://github.com/arceos-hypervisor/arceos.git -b hypervisor-aarch64-dev --recurse-submodules 120 | cd arceos 121 | 122 | # set up rust tools 123 | cargo install cargo-binutils 124 | 125 | # build and run arceos aarch64 hypervisor 126 | make A=apps/hv ARCH=aarch64 HV=y LOG=info SMP=2 build 127 | ``` 128 | ### 1.5 运行hypervisor 129 | 在qemu中运行arceOS,并在arceOS上起虚拟机。在0号核(主核)上启动Linux虚拟机,在1号核(副核)上启动Nimbos虚拟机。 130 | ```shell 131 | # move nimbos image to arceos hv 132 | cp $(WORKSPACE)/nimbos/kernel/target/aarch64/release/nimbos.bin apps/hv/guest/nimbos/nimbos-aarch64.bin 133 | 134 | # move linux image and rootfs to arceos hv 135 | cp $(WORKSPACE)/linux-6.2.15/build/arch/arm64/boot/Image $(WORKSPACE)/arceos/apps/hv/guest/linux/linux-aarch64.bin 136 | cp $(WORKSPACE)/busybox-1.36.1/build/rootfs.img $(WORKSPACE)/arceos/apps/hv/guest/linux/rootfs-aarch64.img 137 | 138 | # run linux(virtio) and nimbos 139 | qemu-system-aarch64 -m 2G -smp 2 -cpu cortex-a72 -machine virt -kernel apps/hv/hv_qemu-virt-aarch64.bin -device loader,file=apps/hv/guest/linux/linux-aarch64.dtb,addr=0x70000000,force-raw=on -device loader,file=apps/hv/guest/linux/linux-aarch64.bin,addr=0x70200000,force-raw=on -machine virtualization=on,gic-version=2 -drive if=none,file=apps/hv/guest/linux/rootfs-aarch64.img,format=raw,id=hd0 -device virtio-blk-device,drive=hd0 -device loader,file=apps/hv/guest/nimbos/nimbos-aarch64_1.dtb,addr=0x50000000,force-raw=on -device loader,file=apps/hv/guest/nimbos/nimbos-aarch64_1.bin,addr=0x50200000,force-raw=on -machine virtualization=on,gic-version=2 -nographic 140 | ``` 141 | 142 | ## 注意 143 | - 目前将pl011串口直通给0号虚拟机,1号虚拟机利用虚拟化pl011进行输出。pl011虚拟化还未完整实现,无法进行1号虚拟机的输入读取。 144 | - 目前hypervisor可支持运行2个nimbos,2个linux,1个linux+1个nimbos。对于2个虚拟机的文件系统支持,中断虚拟化还存在一些问题,所以无法在2个虚拟机的情况下使用virtio文件系统,只能以initramfs的方式启动2个虚拟机。(initramfs的编译方式可参见https://www.digwtx.com/initramfs.html) 145 | - 目前还未完整编写好启动脚本,所以直接用qemu命令启动,包括镜像、dtb、文件系统镜像放置地址都需手动设置。具体需要修改apps/hv/src/main.rs中第78、79行,第218、219行的地址,同时还需要对dts中的memory region起始地址(第20行),initramfs的地址(第3984-385行)进行修改,修改完成后重新编译dts到dtb。 146 | ```shell 147 | 2 nimbos 148 | qemu-system-aarch64 -m 2G -smp 2 -cpu cortex-a72 -machine virt -kernel apps/hv/hv_qemu-virt-aarch64.bin -device loader,file=apps/hv/guest/nimbos/nimbos-aarch64.dtb,addr=0x70000000,force-raw=on -device loader,file=apps/hv/guest/nimbos/nimbos-aarch64.bin,addr=0x70200000,force-raw=on -device loader,file=apps/hv/guest/nimbos/nimbos-aarch64_1.dtb,addr=0x50000000,force-raw=on -device loader,file=apps/hv/guest/nimbos/nimbos-aarch64_1.bin,addr=0x50200000,force-raw=on -machine virtualization=on,gic-version=2 -nographic 149 | 150 | 2 linux(initramfs) 151 | qemu-system-aarch64 -m 2G -smp 2 -cpu cortex-a72 -machine virt -kernel apps/hv/hv_qemu-virt-aarch64.bin -device loader,file=apps/hv/guest/linux/linux-aarch64.dtb,addr=0x70000000,force-raw=on -device loader,file=$(linux image path),addr=0x70200000,force-raw=on -machine virtualization=on,gic-version=2 -device loader,file=$(initramfs path),addr=0x78000000,force-raw=on -device loader,file=/home/tang/arceos/apps/hv/guest/linux/linux-aarch64-1.dtb,addr=0x50000000,force-raw=on -device loader,file=$(linux image path),addr=0x50200000,force-raw=on -machine virtualization=on,gic-version=2 -device loader,file=$(initramfs path),addr=0x58000000,force-raw=on -nographic 152 | ``` 153 | 154 | 155 | -------------------------------------------------------------------------------- /hypervisor/branch.md: -------------------------------------------------------------------------------- 1 | # ArceOS-Hypervisor分支说明 2 | 3 | ## hypervisor 4 | 2023年秋冬训练营使用的分支。 5 | 6 | x86_64架构:能够支持单核单vm的nimbos系统启动。具体运行方式参见[x86_64 nimbos](https://github.com/arceos-hypervisor/hypercraft?tab=readme-ov-file#x86_64-nimbos) 7 | 8 | aarch64架构:能够支持单核单vm的nimbos与linux系统的启动。具体运行方式参见[runcode](./aarch64/ch1-el-architecture-runcode.md) 9 | 10 | riscv架构:能够支持单核单vm的linux系统启动。具体运行方式参见[Riscv Linux](https://github.com/arceos-hypervisor/hypercraft?tab=readme-ov-file#riscv-linux) 11 | 12 | ## process 13 | 14 | x86_64架构:系统调用转发分支。能够支持一个linux和一个process运行,支持转发open/read/write/close四个syscall。具体运行方式:**TODO** 15 | 16 | ## boot_linux 17 | 18 | x86_64架构:能够支持多核多VM的Linux系统启动。基于type1.5的方式启动ubuntu,在完成ubuntu系统降级后,能够启动第二个Linux系统。具体运行方式参见[How-to-boot-VM](https://github.com/arceos-hypervisor/arceos/blob/boot_linux/How-to-boot-VM.md) 19 | 20 | ## hypervisor-aarch64-dev 21 | 22 | aarch64架构:能够支持两个单核Linux系统的VM启动(initramfs)。具体运行方式参见[multi-vm](./aarch64/multi-vm.md) 23 | 24 | ## multicore-aarch64 25 | 26 | aarch64架构:能够支持单个两个核的Linux系统的VM启动。具体运行方式参见[multi-core](./aarch64/multi-core.md) 27 | -------------------------------------------------------------------------------- /hypervisor/x86_64/00-env.md: -------------------------------------------------------------------------------- 1 | # 0. 开发环境和代码结构 2 | 3 | ## 0.1 开发环境 4 | 5 | * 硬件: 6 | * 具有 Intel CPU 的计算机 7 | * CPU 支持 VT-x(现代 Intel CPU 一般都支持),并在 BIOS 中已启用 8 | * 软件: 9 | * 基于 Linux 内核的操作系统,如 Ubuntu 等 10 | * 推荐直接安装在物理机上,如果使用虚拟机则需要正确配置嵌套虚拟化 11 | * [启用 KVM](https://www.linux-kvm.org/page/Choose_the_right_kvm_%26_kernel_version) 12 | * [QEMU](https://www.qemu.org/download/) >= 7.0.0 13 | * [Rust 工具链](https://www.rust-lang.org)(nightly channel) 14 | 15 | ## 0.2 代码组织结构 16 | 17 | 这里仅仅介绍 `arceos` 仓库中直接与 Hypervisor 直接相关的部分。 18 | 19 | * `crates/hypercraft/`: Hypercraft 的代码。Hypercraft 是基于 arceos 实现的一个 Type-1 Hypervisor 库。 20 | * `app/hv/`: 使用 Hypercraft 实现的 Hypervisor。 21 | * `modules/axruntime/src/hv/`: 运行时 arceos 给 Hypervisor 提供服务的代码,包括内存的分配和管理,虚拟设备的实现。 22 | * `crates/page_table_entry/src/arch/x86_64/`: EPT 的定义。 -------------------------------------------------------------------------------- /hypervisor/x86_64/01-vmx.md: -------------------------------------------------------------------------------- 1 | # 1. Intel VMX简介和初始化 2 | 3 | 在本阶段,我们将对虚拟化中的基本概念,以及 Intel VMX 指令扩展进行简单介绍。在代码部分,将介绍 VMX 模式的使能与关闭。 4 | 5 | > 架构手册:[Intel 64 and IA-32 Architectures Software Developer’s Manual (SDM) Vol. 3C](https://cdrdv2.intel.com/v1/dl/getContent/671447), Chapter 24 6 | 7 | ## 1.1 虚拟化基本概念 8 | 9 | * **Host 模式**:能直接访问硬件,有最高的特权级的模式。Hypervisor 运行在此模式。 10 | * **Guest 模式**:特权级低于 Host 模式,对硬件和内存的访问受 Host 模式下的 Hypervisor 控制。但在 Guest 所能访问的范围内能执行 OS 级别的特权指令,运行 Guest OS 等需要 OS 级特权操作的软件。 11 | * **Hypervisor**:也叫 Virtual-Machine Monitors(VMM),运行在 Host 模式下,拥有硬件资源的完全控制权限,并管理其上运行的 Guest 软件。 12 | * **Guest 软件**:运行在 Guest 模式的软件(一般是一个操作系统,即 Guest OS),比 Hypervisor 特权级低,执行特定指令时会被 Hypervisor 拦截(intercept),在受控情况下访问硬件资源。 13 | * **VM entry**:从 Host 模式切换到 Guest 模式,开始执行 Guest 软件的代码。 14 | * **VM exit**:从 Guest 模式切换回 Host 模式,开始执行 Hypervisor 的代码。通常发生在执行特定指令、发生中断、非法操作等情况下。 15 | * **VCPU**:由 Hypervisor 虚拟出来的,运行 Guest 软件的虚拟 CPU 及其私有状态。对于 Guest 软件来说类似于物理 CPU,对于 Hypervisor 来说类似于传统 OS 中的线程。一个 Guest OS 可具有多个 vCpu,vCpu 数量也可以多于物理 CPU 数量。 16 | * **Guest VM**:即我们通常说的虚拟机,除了包含一个或多个 vCpu 外,还包含其他全局的系统状态和资源,如 Guest 物理内存、虚拟设备等。对于 Guest 来说类似于一台真实的计算机,对 Hypervisor 来说类似于传统 OS 中的进程,Hypervisor 可创建多个 Guest VM,各运行一个 Guest OS。 17 | 18 | ## 1.2 Intel Virtual-Machine eXtensions (VMX) 19 | 20 | Intel VMX (也叫 Intel VT-x),是 Intel 处理器提供的硬件虚拟化的技术。虽然同为 x86 架构,但 AMD 与 Intel 的硬件虚拟化技术不同,AMD 的被称为 SVM 或 AMD-V;我们目前只支持了 VMX。 21 | 22 | 在处理器使能VMX后,将具有两种运行模式 (统称VMX operation): 23 | 24 | 1. **VMX root operation**:即 Host 模式,运行 Hypervisor。 25 | 2. **VMX non-root operation**:即 Guest 模式,运行 Guest 软件。 26 | 27 | 需要指出的是,VMX operation 和 x86 特权级并没有直接的关系,两者描述的是不同方向上的权限问题。VMX operation 和 x86 特权级可以组合,如 VMX non-root ring-0 表示 Guest 内核态,运行 Guest OS;Guest 用户程序运行在 VMX non-root ring-3;Hypervisor 通常运行在 VMX root ring-0。 28 | 29 | 为了配置 VMX non-root operation 的进入、运行、退出,以及保存 Guest 和 Host 的状态,VMX 中还有一个重要数据结构 Virtual-Machine Control Structure(**VMCS**),我们将在下一节中详细介绍。 30 | 31 | 此外VMX还提供了一些额外的指令,主要有: 32 | 33 | | 指令 | 描述 | 34 | |-----|------| 35 | | `VMXON` | 使能 VMX,进入 VMX (root) operation | 36 | | `VMXOFF` | 退出 VMX operation | 37 | | `VMCLEAR` | 清除指定 VMCS,使得其可以被激活 | 38 | | `VMPTRLD` | 在当前 CPU 上激活指定 VMCS,使其成为当前 VMCS | 39 | | `VMREAD` | 读当前 VMCS 字段 | 40 | | `VMWRITE` | 写当前 VMCS 字段 | 41 | | `VMLAUNCH` | 进入 VMX non-root operation (当前 VMCS 首次进入) | 42 | | `VMRESUME` | 进入 VMX non-root operation (当前 VMCS 非首次进入) | 43 | | `VMCALL` | 在 VMX non-root operation 执行,触发 VM exit,从而调用 hypervisor 的功能 (类似 syscall) | 44 | 45 | ## 1.3 Hypervisor 工作流程 46 | 47 | 基于 Intel VMX 实现的 Hypervisor,其基本工作流程如下(相关代码在`crates/hypercraft/src/arch/x86_64/vmx/`): 48 | 49 | 1. Hypervisor 执行 `VMXON` 指令,使能 VMX,进入 VMX root operation。 50 | 2. Hypervisor 配置 VMCS。 51 | 3. Hypervisor 执行 `VMLAUNCH`/`VMRESUME` 指令,发生 VM entry,切换到 VMX non-root operation,执行 guest 代码。 52 | 4. Guest 软件在发生特定事件后,会导致 VM exit,自动切换回 VMX root operation,hypervisor 需要根据 VM exit 的原因采取进一步操作。 53 | 5. Guest 执行完毕,hypervisor 执行 `VMOFF` 指令,退出 VMX 模式。 54 | 55 | ![hypervisor 工作流程](figures/1-flow.svg) 56 | 57 | ## 1.4 使能 VMX 58 | 59 | 本节我们介绍如何使能 VMX 模式。 60 | 61 | ### 1.4.1 检查 VMX 支持情况 62 | 63 | 首先,我们需要检查当前硬件平台是否支持 VMX 指令扩展,这一步又可以细分为两种情况: 64 | 65 | * CPU 不支持 VMX 66 | * CPU 支持 VMX,但被 BIOS 禁用 67 | 68 | 对于 CPU 支持情况,是通过 [`CPUID` 指令](https://en.wikipedia.org/wiki/CPUID#EAX=1:_Processor_Info_and_Feature_Bits) 查询 Processor Info and Feature Bits (`EAX=1`) 中的相关位获知。 69 | 70 | 对于 BIOS 支持情况,需要查询 `IA32_FEATURE_CONTROL` MSR 的以下两位: 71 | 72 | * Bit 0 lock bit:如果设置,无法修改该 MSR;如果不设置,无法执行 `VMXON`。 73 | * Bit 2 enables VMXON outside SMX operation:如果不设置,无法 (在 SMX 模式外) 执行 `VMXON`。 74 | 75 | 我们希望这两个位都已被设为了 1,否则就要重新设置。不过在 bit 0 为 1,而 bit 2 为 0 时,我们既无法使用 `VMXON` 指令,又无法修改该 MSR。此时说明 VMX 被 BIOS 禁用,需要在 BIOS 中手动开启。 76 | 77 | ### 1.4.2 (可选) 检查相关系统寄存器 78 | 79 | 在使能 VMX 之前,一般还需要检查相关系统寄存器的某些位设置是否符合要求,并检查 VMX 的一些子功能是否被支持,这不影响 VMX 的使能成功,但便于在使能失败或后续操作失败时排查原因。感兴趣的可以参见代码与架构手册。 80 | 81 | ### 1.4.3 执行 VMXON 82 | 83 | 在执行 `VMXON` 指令前,需要先将 `CR4` 的 `VMXE` 位 (13 位) 设为 1。然后创建 **VMXON region**,这是一块 4KB 大小 4KB 对齐的特殊内存,其物理地址将作为操作数提供给 `VMXON` 指令。VMXON region 还需要满足特定的格式:前 4 字节的低 31 位需要设置为 **VMCS revision identifier**,该值可以在 `IA32_VMX_BASIC` MSR 的低 31 位中查到。 84 | 85 | 由于 `VMXON` 的作用范围只有当前 CPU,所以在多核系统中如果有多个 CPU 需要使能 VMX,则需要在相应 CPU 上各自执行一遍 `VMXON` 指令,并提供不同的 VMXON region。 86 | 87 | 如果已使能 VMX,并在 guest 运行结束后要关闭 VMX,应当先执行 `VMXOFF` 指令,再将 `CR4` 的 `VMXE` 位清除。 88 | 89 | ### 1.4.4 小结 90 | 91 | 最后,总结一下使能 VMX 的一般流程: 92 | 93 | 1. 执行 `CPUID` 指令,检查 CPU 支持情况。 94 | 2. 查询或设置 `IA32_FEATURE_CONTROL` MSR,检查 VMX 是否被 BIOS 禁用。 95 | 3. 查询 `IA32_VMX_BASIC` MSR,获取 VMCS revision identifier。 96 | 4. 创建 VMXON region,写入 VMCS revision identifier。 97 | 5. 设置 `CR4` 的 `VMXE` 位。 98 | 6. 传入 VMXON region 物理地址,执行 `VMXON` 指令。 99 | 100 | ## 1.5 实现 101 | 102 | 首先是检查 VMX 的支持情况,Rust 可使用 [raw_cpuid](https://docs.rs/raw-cpuid/latest/raw_cpuid/) 库来进行 `CPUID` 各功能的查询: 103 | 104 | ```Rust 105 | pub fn has_hardware_support() -> bool { 106 | if let Some(feature) = CpuId::new().get_feature_info() { 107 | feature.has_vmx() 108 | } else { 109 | false 110 | } 111 | } 112 | ``` 113 | 114 | `VmxPerCpuState`(及其外层封装`PerCpu`)是一个 per-CPU 结构,用于管理每个物理 CPU 私有的虚拟化相关状态 (如 VMXON region),由 hypercraft 的调用者创建。由于目前只有单核实现,故`app/hv`直接在栈上创建了`PerCpu`。 115 | 116 | ```Rust 117 | let mut p = PerCpu::::new(hart_id); 118 | p.hardware_enable().unwrap(); 119 | ``` 120 | 121 | 启用 VMX 的具体逻辑位于 `VmxPerCpuState::hardware_enable` 函数,其中依次执行了以下步骤: 122 | 123 | * 确保当前硬件支持 VMX,且还未被使能。 124 | * 检查与配置 `IA32_FEATURE_CONTROL` MSR。 125 | * 检查 `CR0` 和 `CR4` 的位设置是否符合要求 (SDM Vol. 3C, Appendix A.7, A.8)。 126 | * 从 `IA32_VMX_BASIC` MSR 读取 VMCS revision identifier,并检查其他位是否符合要求。 127 | * 初始化 VMXON region。由于之后阶段配置 VMCS 时也需要分配一个类似的内存区域 (VMCS region),且格式与 VMXON region 一样,因此代码中用一个 `VmxRegion` 结构体统一这两种用途。 128 | * 设置 `CR4` 的 `VMXE` 位。 129 | * 传入 VMXON region 物理地址,执行 `VMXON` 指令。 130 | 131 | 在`msr.rs`中,我们通过 `Msr` 结构体实现了对各种 MSR 寄存器 (Model-Specific Registers) 的读写。 132 | 133 | ## 1.6 练习 134 | 135 | 1. 阅读[Intel SDM Vol. 3C](https://cdrdv2.intel.com/v1/dl/getContent/671447), Chapter 24: Introduction to Virtual Machine Extensions(共4页) 136 | 2. 阅读代码,描述在使能 VMX 的过程中 `vmx_region` 是如何分配和初始化的。(使能 VMX 的过程在 `crates/hypercraft/src/arch/x86_64/vmx/percpu.rs` 中的 `VmxPerCpuState::::hardware_enable` 函数) -------------------------------------------------------------------------------- /hypervisor/x86_64/02-vmcs.md: -------------------------------------------------------------------------------- 1 | # 2. VMCS 配置 2 | 3 | 在本阶段,我们将介绍并配置 VMX 中的一个重要数据结构 VMCS。 4 | 5 | > 架构手册:[Intel 64 and IA-32 Architectures Software Developer’s Manual (SDM) Vol. 3C](https://cdrdv2.intel.com/v1/dl/getContent/671447), Chapter 25 6 | 7 | ## 2.1 Virtual Machine Control Structures (VMCS) 介绍 8 | 9 | 在硬件中,一般只有一套完整的机器状态,而虚拟化要求在一个物理 CPU 上同时运行 Host 和 Guest,因此需要在 VM entry/VM exit 时切换 Host/Guest 的机器状态。这可以类比于在用户态和内核态切换时保存/恢复上下文的操作。而 Host/Guest 切换时需要保存的状态就被存储在一块专门的内存 VMCS 中。 10 | 11 | VMCS 是一个存储在内存中的数据结构,和 vCPU 一一对应,每有一个 vCPU 都需要配置其相应的 VMCS。其中除了存储 Guest/Host 状态外,还有一些字段可以控制虚拟化的行为。VMCS 的内容具体包括以下几类: 12 | 13 | 1. **Guest-state area**:在 VM exit 时,Guest 的状态会自动被保存到该区域,并在 VM entry 时自动从该区域恢复。 14 | 2. **Host-state area**:在 VM exit 时,Host 的状态会从该区域恢复 (VM entry 时不会自动保存)。 15 | 3. **VM execution/entry/exit control fields**:对运行、进入、退出 Guest 模式时处理器的行为进行配置,如配置哪些情况下会发生 VM exit。 16 | 4. **VM exit information fields**:只读字段,报告 VM exit 发生时的一些信息,如发生 VM exit 的原因。我们将在下一节中用到此类字段。 17 | 18 | VMCS 所在的内存被称为 **VMCS region**,与 VMXON region 一样,也是 4KB 大小 4KB 对齐的,且需要向前 4 字节写入 VMCS revision identifier。VMCS region 的物理地址被称为 **VMCS pointer**。 19 | 20 | 物理 CPU 在首次进入 non-root 模式之前,需要先激活 vCPU 对应的 VMCS,方法是依次执行以下两条指令 (VMCS pointer 作为操作数): 21 | 22 | 1. `VMCLEAR`:将该 VMCS 设为“干净的”,使得其可以被激活。 23 | 2. `VMPTRLD`:将该 VMCS 激活,即将该 VMCS 与当前物理 CPU 绑定,该 VMCS 就被称为“当前 VMCS”,之后的 `VMREAD`/`VMWRITE`/`VMLAUNCH`/`VMRESUME` 指令只对当前 VMCS 生效。对应的还有 `VMPTRST` 指令,用于取得当前的 VMCS pointer (在本项目中不需要用到)。 24 | 25 | ![VMCLEAR 与 VMPTRLD](figures/2-vmclear.svg) 26 | 27 | 访问 VMCS 中的字段不能直接使用内存读写指令,而是要用专门的 `VMREAD`/`VMWRITE` 指令。 28 | 29 | ## 2.2 VMCS Host 状态 30 | 31 | VMCS Host 状态会在发生 VM exit 而从 non-root 切换回 root 时,从 VMCS 自动加载进处理器中,但在 VM entry 时不会自动保存。这些状态主要包括: 32 | 33 | * 控制寄存器:`CR0`、`CR3`、`CR4`。 34 | * 指令指针与栈指针:`RIP`、`RSP`。 35 | * 段选择子 (selector):`CS`、`SS`、`DS`、`ES`、`FS`、`GS`、`TR`。 36 | * 段基址 (base address):`FS base`、`GS base`、`TR base`、`GDTR base`、`IDTR base`。 37 | * 一些 MSR,如 `IA32_PAT`、`IA32_EFER`。 38 | 39 | 在配置时,一般直接将它们设为当前 (Host) 的状态即可。但是 `RIP` 应该设为 VM exit 处理函数的地址,`RSP` 应该设为处理 VM exit 时的栈指针。 40 | 41 | ## 2.3 VMCS Guest 状态 42 | 43 | VMCS Guest 状态会在发生 VM entry 时从 VMCS 自动加载进处理器中,并在 VM exit 时自动保存到 VMCS 中。这些状态主要包括: 44 | 45 | * 控制寄存器:`CR0`、`CR3`、`CR4`。 46 | * `RIP`、`RSP`、`RFLAGS`。 47 | * 完整的段寄存器:即 `CS`、`SS`、`DS`、`ES`、`FS`、`GS`、`TR` 段的选择子、基址、界限 (limit) 与访问权限 (access rights)。 48 | * `GDTR` 与 `IDTR` 的基址与界限。 49 | * 一些 MSR,如 `IA32_PAT`、`IA32_EFER`。 50 | 51 | 在设置时,还需要考虑 Guest 的运行模式。在 x86 下,运行模式包括 16 位实模式、32 位保护模式、64 位 IA-32e 模式,不同的运行模式需要配置好对应的段寄存器访问权限。其中 `CS` 段寄存器的访问权限还指示了 Guest 的当前特权级 (CPL)。 52 | 53 | 需要注意,VMCS 也不是包含了所有的机器状态,如 `RAX`、`RBX` 等通用寄存器就不在 VMCS 字段中,因此它们需要在 VM entry/exit 时由软件实现保存与恢复。 54 | 55 | ## 2.4 VMCS 控制字段 56 | 57 | ### 2.4.1 VM-Execution Control Fields 58 | 59 | 这些字段用于控制在 non-root 模式运行时处理器的行为。常用的有以下几个: 60 | 61 | * Pin-based VM-execution controls:用于配置 hypervisor 对 Guest 异步事件的拦截 (例如中断)。 62 | * Processor-based VM-execution controls:又可分为 primary processor-based 和 secondary processor-based,用于配置 hypervisor 对 Guest 同步事件的拦截 (例如执行特定的指令)。 63 | * Exception bitmap:用于配置 hypervsior 对 Guest 异常的拦截。 64 | * I/O-bitmap address:用于配置 hypervisor 对 Guest 读写特定 I/O 端口的拦截。 65 | * MSR-bitmap address:用于配置 hypervisor 对 Guest 读写特定 MSR 的拦截。 66 | * Extended-page-table pointer:指定扩展页表 (EPT) 的基址。(Step 4) 67 | 68 | 以上字段除了带 address、pointer 外的都是一个 bitmap,将相应的位设为 1 或 0 就表示启用或关闭了相应的功能。 69 | 70 | 需要注意,对 pin-based、primary processor-based、secondary processor-based VM-execution controls 以及下文的 VM-exit controls、VM-entry controls 的配置较为繁琐。因为同一个字段,不同的 CPU 对各个位的支持情况 (默认值和允许值) 不同,需要根据支持情况设置未用到的位的默认值。感兴趣的参见代码或 SDM Vol. 3D 附录 A.2 ~ A.5。 71 | 72 | ### 2.4.2 VM-Exit Control Fields 73 | 74 | 这些字段用于控制在 VM exit 发生时处理器的行为。除了 VM-exit controls 外,还有: 75 | 76 | * VM-exit MSR-store count、VM-exit MSR-store address:VM exit 时要保存的 (Guest) MSR 数量与内存区域。 77 | * VM-exit MSR-load count、VM-exit MSR-load address:VM exit 时要载入的 (Host) MSR 数量与内存区域。 78 | 79 | 因为除了 `IA32_EFER`、`IA32_PAT` 等少量 MSR 外,VMCS 中没有空间存储其他 MSR 了,因此在这里指定了一个要额外保存与恢复的 MSR 的列表。这样的设计也使得在 VM exit 时避免切换不常用的 MSR,从而减少 Guest 与 Host 间的切换开销。VM-entry control fields 中也有类似的字段。 80 | 81 | ### 2.4.3 VM-Entry Control Fields 82 | 83 | 这些字段用于控制在 VM entry 发生时处理器的行为。除了 VM-entry controls 外,还有: 84 | 85 | * VM-entry MSR-load count、VM-entry MSR-load address:VM entry 时要载入的 (Guest) MSR 数量与内存区域。 86 | * VM-entry interruption-information field:用于向 Guest 注入虚拟中断或异常。(以后会用到) 87 | 88 | ## 2.5 实现 89 | 90 | VMCS 的实现在`crates/hypercraft/src/arch/x86_64/vmx/vmcs.rs`中,其中大量使用了宏等 Rust 编程技巧,阅读起来有一定难度。同时可以发现,读写 VMCS 的字段时不需要手动指定 VMCS region 的地址,这是因为 `VMCLEAR`、`VMPTRLD` 两条指令选定了“当前 VMCS”之后, `VMREAD`、`VMWRITE` 即可直接在“当前 VMCS”中读写了。 91 | 92 | 在 `VmxVcpu` 结构体(`crates/hypercraft/src/arch/x86_64/vmx/vcpu.rs`)内的 `setup_vmcs` 函数中,首先执行 `VMCLEAR` 和 `VMPTRLD` 指令激活 VMCS,然后又分 Host、Guest、Control 三部分进行分别配置。 93 | 94 | 在 `setup_vmcs_host` 中,大多数 Host 状态通过直接读取当前对应的系统状态进行配置,除了 `RSP` 和 `RIP`:`RIP` 设置为 `vmx_exit` 函数的地址,`RSP` 则是在 VM-entry 时进行设置。 95 | 96 | 在 `setup_vmcs_guest` 中,我们先配置 Guest 的 `CR0` 禁用保护模式与分页,并只设置 `CR4` 的 `VMXE` 位。同时还配置了 `CR0`/`CR4` 的 Guest/Host masks 和 read shadows,这些字段会影响对 Guest 读写 `CR0`/`CR4` 的拦截 (详见 SDM Vol. 3C, Section 24.6.6)。之后,我们使用一个宏 `set_guest_segment!` 来配置 Guest 的段寄存器,其中基址与选择子全为 0,界限全为 `0xffff`,访问权限为 16 位。其他系统状态也都设为了默认值。 97 | 98 | 在 `setp_vmcs_control` 中,我们通过 `vmcs::set_control` 函数,可设置或清除指定控制字段的某些位,包括: 99 | 100 | * 在 pin-based controls 设置了 NMI 中断拦截,外部中断拦截。 101 | * 在 primary processor-based controls 取消了 `CR3` 读写拦截。 102 | * 在 secondary processor-base controls 中设置了 Guest 能使用 `RDTSCP` 和 `INVPCID` 指令。 103 | * 在 exit controls 中设置了退出时为 64 位 Host、自动切换 Host/Guest 的 `IA32_PAT` 与 `IA32_EFER`。 104 | * 在 entry controls 中设置了进入时自动载入 Guest `IA32_PAT` 与 `IA32_EFER`。 105 | 106 | 此外,我们将不会在 entry/exit 时保存与恢复其他 MSR,因为 hypervisor 没有用到其他 MSR,且目前只有一个 vCPU 而不需要切换。 107 | 108 | 对于 exception 和 I/O bitmap 设为空,表示直通所有 Guest 异常与 I/O 指令;而 MSR bitmaps 为空表示拦截所有 MSR 读写。 109 | 110 | # 2.6 练习 111 | 112 | 1. 阅读 Intel SDM Vol. 3C, Chapter 25: Virtual-Machine Control Structures 相关小节,回答以下问题: 113 | 1. 如果让要 hypervisor 实现以下功能,应该如何配置 VMCS? 114 | 1. 拦截 Guest `HLT` 指令 115 | 2. 拦截 Guest `PAUSE` 指令 116 | 3. 拦截外部设备产生的中断,而不是直通给 Guest 117 | 4. 打开或关闭 Guest 的中断 118 | 5. 拦截 Guest 缺页异常 (#PF) 119 | 6. 拦截所有 Guest I/O 指令 (x86 `IN`/`OUT`/`INS`/`OUTS` 等) 120 | 7. *只拦截 Guest 对串口的 I/O 读写 (I/O 端口为 `0x3f8`) 121 | 8. 拦截所有 Guest MSR 读写 122 | 9. *只拦截 Guest 对 `IA32_EFER` MSR 的写入 123 | 10. *只拦截 Guest 对 `CR0` 控制寄存器 `PG` 位 (31 位) 的写入 124 | 2. *如果要在单核 hypervisor 中交替运行两个 vCPU,应该如何操作 VMCS? 125 | -------------------------------------------------------------------------------- /hypervisor/x86_64/03-vmlaunch.md: -------------------------------------------------------------------------------- 1 | # 3. VM-entry 和 VM-exit 2 | 3 | 在本阶段,我们将介绍 VM-entry 和 VM-exit 的过程。 4 | 5 | > 架构手册:[Intel 64 and IA-32 Architectures Software Developer’s Manual (SDM) Vol. 3C](https://cdrdv2.intel.com/v1/dl/getContent/671447), Chapter 25 ~ Chapter 28 6 | 7 | ## 3.1 VCPU 上下文切换 8 | 9 | 在 VM-entry 和 VM-exit 时,我们需要切换 Host 和 Guest 的状态,这一过程即 vCPU 上下文切换 (context switch),与传统 OS 中线程间、用户内核间的上下文切换类似。 10 | 11 | 对于保存在 VMCS 中的状态,在 VM-entry 或 VM-exit 时,硬件会自动完成切换。但对于不在 VMCS 中的状态,就需要软件手动进行切换,如下列通用寄存器 (`RSP` 已在 VMCS 状态中): 12 | 13 | | GPRs || 14 | |-|-| 15 | | `RAX` | `R8 ` | 16 | | `RCX` | `R9 ` | 17 | | `RDX` | `R10` | 18 | | `RBX` | `R11` | 19 | | ~~RSP~~ | `R12` | 20 | | `RBP` | `R13` | 21 | | `RSI` | `R14` | 22 | | `RDI` | `R15` | 23 | 24 | 因此,我们需要在 VM-entry 时,先手动从内存载入 Guest 通用寄存器,再执行 `VMLAUNCH`/`VMRESUME`,让硬件自动切换 VMCS 中的状态,然后进入 Guest。在 VM-exit 时,硬件先自动完成了 VMCS 中状态的切换,但此时的通用寄存器还是 Guest 的且没有被保存,需要将它们保存到内存中,以便处理 VM-exit 时能够访问。 25 | 26 | 由于在我们的实现中,通过 `VMLAUNCH` 进入 Guest 后,不再执行之后的指令。当发生 VM-exit 回到 Host 模式时,也是跳转到 VM-exit 处理函数,并不会回到 `VMLAUNCH` 之后。VM-exit 时也无需用到之前保存的 Host 通用寄存器。因此,我们可以无需保存与恢复每次 VM-entry 前的 Host 通用寄存器。 27 | 28 | ## 3.2 处理 VM-exit 29 | 30 | ### 3.2.1 获取 VM-exit 信息 31 | 32 | 首先通过 VMCS VM-exit information fields 中的 **Exit reason** 字段,获取 VM-exit 的基本信息,其中的主要位有: 33 | 34 | * Basic exit reason:基本原因,见 SDM Vol. 3D, Appendix C。 35 | * VM-entry failure:表明 VM-entry 失败,如发生了原因为 "VM-entry failure due to invalid Guest state" 或 "VM-entry failure due to MSR loading" 的 VM-exit。 36 | 37 | 其他常用的 VMCS VM-exit information fields 有: 38 | 39 | | VMCS 字段 | 描述 | 40 | |-|-| 41 | | Exit qualification | 因 I/O 指令、EPT violation、控制寄存器访问等导致的 VM-exit 的详细信息 | 42 | | VM-exit instruction length | 因执行特定指令导致 VM-exit 时的指令长度 | 43 | | VM-instruction error field | VMX 指令执行失败时错误码 | 44 | | Guest-physical address | EPT violation 时的出错 Guest 物理地址 (Step 4) | 45 | | VM-exit interruption information | 因外部中断导致 VM-exit 时的详细信息 (Step 6) | 46 | 47 | ## 3.3 实现 48 | 49 | 本节的内容位于`crates/hypercraft/src/arch/x86_64/vmx/vcpu.rs`中。首先是 `run` 函数: 50 | 51 | ```rust 52 | /// Run the guest, never return. 53 | pub fn run(&mut self) -> ! { 54 | VmcsHostNW::RSP 55 | .write(&self.host_stack_top as *const _ as usize) 56 | .unwrap(); 57 | unsafe { self.vmx_launch() } 58 | } 59 | ``` 60 | 61 | 该函数首先将 VMCS Host `RSP` 设为 `self.host_stack_top` 的地址,方便 VM-exit 时保存 Guest 寄存器。之后进入 `vmx_launch` 函数: 62 | 63 | ```Rust 64 | #[naked] 65 | unsafe extern "C" fn vmx_launch(&mut self) -> ! { 66 | asm!( 67 | "mov [rdi + {host_stack_top}], rsp", // save current RSP to Vcpu::host_stack_top 68 | "mov rsp, rdi", // set RSP to guest regs area 69 | restore_regs_from_stack!(), 70 | "vmlaunch", 71 | "jmp {failed}", 72 | host_stack_top = const size_of::(), 73 | failed = sym Self::vmx_entry_failed, 74 | options(noreturn), 75 | ) 76 | } 77 | ``` 78 | 79 | 这段代码是用纯汇编写成的,流程如下: 80 | 81 | 1. 将当前的 Host `RSP` 保存进 `self.host_stack_top` (`RDI` 是该函数的第一个参数,即 `self`)。 82 | 2. 切换 Host `RSP` 到 `RDI`,即 `self.guest_regs` 结构的开头。 83 | 3. 通过 `restore_regs_from_stack!()` 宏将 Guest 的通用寄存器恢复出来。 84 | 4. 执行 `VMLAUNCH`,进入 Guest。 85 | 5. 如果 `VMLAUNCH` 执行失败,将不会进入 Guest,而是执行之后的指令,这里就直通跳转到了错误处理函数 `vmx_entry_failed` (该函数会直接 panic)。 86 | 87 | ![VCPU 上下文切换](figures/3-context-switch.svg) 88 | 89 | VMCS 中将 Host `RIP` 设置为 `vmx_exit` 函数的地址,因此 VM-exit 时会直接跳转到该函数: 90 | 91 | ```Rust 92 | #[naked] 93 | unsafe extern "C" fn vmx_exit(&mut self) -> ! { 94 | asm!( 95 | save_regs_to_stack!(), 96 | "mov r15, rsp", // save temporary RSP to r15 97 | "mov rdi, rsp", // set the first arg to &Vcpu 98 | "mov rsp, [rsp + {host_stack_top}]", // set RSP to Vcpu::host_stack_top 99 | "call {vmexit_handler}", // call vmexit_handler 100 | "mov rsp, r15", // load temporary RSP from r15 101 | restore_regs_from_stack!(), 102 | "vmresume", 103 | "jmp {failed}", 104 | host_stack_top = const size_of::(), 105 | vmexit_handler = sym Self::vmexit_handler, 106 | failed = sym Self::vmx_entry_failed, 107 | options(noreturn), 108 | ); 109 | } 110 | ``` 111 | 112 | 其流程如下: 113 | 114 | 1. 通过 `save_regs_to_stack!()` 宏将 Guest 的通用寄存器保存到 `guest_regs` 中。 115 | 2. 保存此时的 Host `RSP` 到临时寄存器 `R15`,便于之后再次 VM-entry 时恢复 Guest 通用寄存器。 116 | 3. 将 `RDI` 设为 `RSP`,即接下来调用的 `vmexit_handler` 函数的第一个参数为当前 `VmxVcpu` 结构。 117 | 4. 从 `VmxVcpu::host_stack_top` 恢复 Host `RSP`。 118 | 5. 调用 VM-exit 处理函数 `vmexit_handler`。 119 | 6. VM-exit 处理完毕,从 `R15` 恢复栈,并恢复 Guest 通用寄存器,准备再次进入 Guest。 120 | 7. 执行 `VMRESUME`,再次进入 Guest。 121 | 8. 如果 `VMRESUME` 执行失败,直通跳转到 `VmxVcpu::vmx_entry_failed()`。 122 | 123 | ## 3.4 练习 124 | 125 | 1. 阅读代码,详细阐述: 126 | 1. 在 VM-entry 和 VM-exit 的过程中,Host 的 `RSP` 寄存器的值是如何变化的?包括:哪些指令 127 | 2. 在 VM-entry 和 VM-exit 的过程中,Guest 的通用寄存器的值是如何保存又如何恢复的?(提示:与`RSP`的修改有关) 128 | 3. VM-exit 过程中是如何确保调用 `vmexit_handler` 函数时栈是可用的? 129 | -------------------------------------------------------------------------------- /hypervisor/x86_64/04-ept.md: -------------------------------------------------------------------------------- 1 | # 4. EPT 与内存隔离 2 | 3 | 在本阶段,我们将了解 EPT 与内存虚拟化,实现 Guest 与 Hypervisor 的内存隔离。 4 | 5 | > 架构手册:[Intel 64 and IA-32 Architectures Software Developer’s Manual (SDM) Vol. 3C](https://cdrdv2.intel.com/v1/dl/getContent/671447), Chapter 29 6 | 7 | ## 4.1 内存虚拟化概述 8 | 9 | 在启用分页机制的处理器中,软件使用的是虚拟地址,经过页表的转换,转换为物理地址后,才能访问物理内存。在虚拟化中,我们需要虚拟化 Guest VM 的物理内存。此时 Guest 上的软件使用的就是 **Guest 虚拟地址 (gVA)**,通过 Guest 页表转换为 **Guest 物理地址 (gPA)**。而为了安全和功能的考虑, Hypervisor 不能让 Guest 直接用 Guest 物理地址去访问物理内存,需要再经过一次转换,转换为 **Host 物理地址 (hPA)** 才能访问。通过这个过程,Hypervisor 就实现了内存的虚拟化,`gVA`、`gPA`、`hPA`就是内存虚拟化所涉及的三种地址。 10 | 11 | 实现内存虚拟化有两种方式:无需硬件支持的影子分页,和需要硬件支持的嵌套分页。 12 | 13 | ### 4.1.1 影子分页 (shadow paging) 14 | 15 | 对于影子分页,无需专门的硬件,即无嵌套分页支持的硬件。早期的 VMX 虽然是硬件虚拟化,但还未实现 EPT,就需要通过影子分页实现内存虚拟化。影子分页只使用了普通页表就能实现 Guest 物理内存的虚拟化。 16 | 17 | 对于 Guest OS 来说,一般情况下意识不到 Hypervisor 的存在,因此会像在真实机器上那样,配置好 Guest 页表,负责把 gVA 转换为 gPA。而在 Hypervisor 中也会维护一个 Guest 物理内存映射,负责把 gPA 转换为 hPA,但不一定要是页表这样的分级结构,因为硬件不会直接使用。 18 | 19 | **影子页表 (shadow page table)** 就是通过合并 Guest 页表与 Hypervisor 中的 Guest 物理内存映射,直接得到 gVA 到 hPA 的映射,然后根据这一映射构造出的普通页表。运行 Guest 时,只需将 Host 页表基址 (`CR3`) 设为影子页表,当 Guest 使用 gVA 进行访问时,就经过影子页表的转换,直接得到 hPA,从而正确进行内存访问。 20 | 21 | 当 Guest 修改 Guest 页表时,Hypervisor 需要进行拦截,并修改对应的影子页表,这可以通过在影子页表中去掉 Guest 自身页表的映射来实现。当 Guest 切换 Guest 的 `CR3` 以切换地址空间时,Hypervisor 也需要进行拦截,并切换对应的影子页表。 22 | 23 | ![Shadow paging 1](figures/4-shadow1.svg) 24 | ![Shadow paging 2](figures/4-shadow2.svg) 25 | 26 | ### 4.1.2 嵌套分页 (nested paging) 27 | 28 | 嵌套分页又称二维分页 (2-dimensional paging)、二级地址转换 (Second Level Address Translation),是由硬件提供了一个类似普通页表的结构,把 gPA 转换为 hPA,即**嵌套页表 (nested page table)**。 29 | 30 | Intel VMX 中的嵌套页表称作 **EPT (Extended Page Table)**,而 AMD SVM 中的称作 NPT (Nested Page Table),ARM 中的称作 Stage-2 Page Table。 31 | 32 | 嵌套页表的基址 (NPT root),以及 Guest 页表基址在硬件中都有对应的寄存器。嵌套页表和 Host 页表是独立的,互不影响。当 Guest 使用 gVA 进行访问时,硬件会先通过 Guest 页表转换为 gPA,再通过嵌套页表转换为 hPA,才能进行内存访问,因此会比用影子分页多一次页表结构的遍历。当 Guest 应用 TLB 缺失比较严重时,最坏情况下会触发多达 20 多次内存访问,带来严重的性能下降。在实际应用中我们可以通过在嵌套页表中使用大页来减少这种开销。 33 | 34 | ![Nested paging](figures/4-nested.svg) 35 | 36 | ### 4.1.3 对比 37 | 38 | || 影子分页 | 嵌套分页 | 39 | |-|-|-| 40 | | 特殊硬件 | | | 41 | | 实现方式 | 复杂 | 简单 | 42 | | TLB 缺失开销 | | | 43 | | 映射修改开销 | | | 44 | | 页表切换开销 | | | 45 | | 内存空间占用 | | | 46 | 47 | ## 4.2 Intel EPT 介绍 48 | 49 | 本项目会使用 Intel EPT 提供的嵌套分页机制,实现内存虚拟化。 50 | 51 | ### 4.2.1 EPT 结构 52 | 53 | EPT 与普通页表结构相似,一般都是 4 级,而且都可以设置大页来直接映射一块 1GB 或 2MB 大小的页面。Guest 页表基址保存在 VMCS **Guest CR3** 字段中,EPT 基址保存在 **extended-page-table pointer (EPTP)** 字段中。 54 | 55 | 下图展示了一个 Guest 虚拟地址,通过 Guest 页表转换为 Guest 物理地址,再通过 EPT 转换为 Host 物理地址的过程。 56 | 57 | ![EPT](figures/4-ept.svg) 58 | 59 | ### 4.2.2 EPT 页表项 60 | 61 | 与普通页表一样,EPT 页表的每个表项都有一些标志位,表示访问权限等信息。 62 | 63 | 下图分别给出了 EPT pointer 和几种页表项的格式,包括指向下一级页表的 table 表项、指向 1GB/2MB 大页的表项、以及指向 4KB 页面的表项: 64 | 65 | ![EPT entries](figures/4-ept-entries.svg) 66 | 67 | 其中低 3 位 RWX 分别为读写执行权限位;第 7 位表示该页表项指向大页还是中间级页表;3 至 5 位为内存类型,对于普通内存就是 6,启用 write-back cache,对于 MMIO 这样的设备内存就是 0 不启用 cache;从第 12 位开始都是一个 (Host) 物理地址,表示下一级页表物理地址,或是目标的页表的物理地址。 68 | 69 | ### 4.2.3 EPT Violation 70 | 71 | 当硬件使用 EPT 转换一个 gPA 时,如果中途发生了页面不存在,或是权限不匹配等错误时,会触发一个 VM exit,名为 **EPT violation**,类似普通页表中的缺页异常 (page fault)。 72 | 73 | 一般情况下,发生 EPT violation,就是 Guest 非法访问了一个 Guest 物理地址,应该杀掉整个 Guest 或报错。但我们也可以利用 EPT violation 实现一些功能。如实现页面交换、按需分配 Guest 物理内存、虚拟化对设备的 MMIO 访问等。 74 | 75 | VMCS 中有提供了一些与 EPT violation 有关的信息,比如 **Exit qualification** 会保存访问者的权限信息,用 bit 0/1/2 分别表示是一个 读/写/执行 访问导致了这个 EPT violation (类似缺页异常时的 error code)。此外 **Guest-physical address** 表示出错的 Guest 物理地址,**Guest-linear address** 表示出错的 Guest 虚拟地址等。 76 | 77 | ### 4.2.4 小结 78 | 79 | 最后,我们给出一个普通页表和 EPT 的对比: 80 | 81 | || Page Table | Extended Page Table | 82 | |-|-|-| 83 | | 地址转换 | VA → PA | gPA → hPA | 84 | | 基址 | CR3 | VMCS EPT pointer | 85 | | 转换失败 | 缺页异常 (#PF) | VM Exit: EPT violation | 86 | | Invalidate TLB | `MOV` to CR3 | `INVEPT` | 87 | 88 | ## 4.3 实现 89 | 90 | ### 4.3.1 处理 EPT Violation 91 | 92 | 由于我们暂时不支持动态 Guest 内存分配或 MMIO 设备虚拟化,故目前不对 **EPT violation** 做任何特殊处理,直接 `panic`。 93 | 94 | ### 4.3.2 VMCS 配置 95 | 96 | `crates/hypercraft/src/arch/x86_64/vmx/vmcs.rs` 中提供了以下和 EPT 直接相关的功能: 97 | 98 | 1. 设置EPT的函数 `set_ept_pointer`; 99 | 2. 通过 `INVEPT` 指令无效 TLB 的函数 `invept`; 100 | 101 | ### 4.3.4 页表相关结构 102 | 103 | `crates/page_table_entry/src/arch/x86_64/epte.rs` 中定义了 EPT 的页表项 `EPTEntry`。 104 | 105 | `crates/page_table/src/bits64.rs` 中实现了一个通用的四级页表结构 `PageTable64`。 106 | 107 | 组合以上两者,即可得到一个 EPT `pub type ExtendedPageTable PageTable64;`。 108 | 109 | # 4.4 练习 110 | 111 | 1. 假设一个 Hypervisor 中运行有 4 个 Guest VM,每个 Guest VM 中运行有 10 个应用 (需要 10 个 Guest 页表),请问在用影子分页方式实现内存虚拟化时,Hypervisor 共需维护多少份影子页表? 112 | 2. 假设在 Guest OS 中启用了 4 级页表,Guest 的一次访存 (使用 Guest 虚拟地址) 会导致多少次内存访问?(使用 4 级嵌套页表实现内存虚拟化;假设 TLB 全部失效;假设不出现缺页或 EPT Violation) 113 | 3. 简述:如果要改成按需分配内存,应该在代码上作出哪些修改? 114 | -------------------------------------------------------------------------------- /hypervisor/x86_64/05-nimbos.md: -------------------------------------------------------------------------------- 1 | # 5. 运行一个相对完整的Guest OS 2 | 3 | 在本阶段,我们将运行一个相对完整的 Guest OS。 4 | 5 | ## 5.1 Guest OS 与 Guest BIOS 介绍 6 | 7 | ### 5.1.1 Guest NimbOS 8 | 9 | 我们准备的 Guest OS 为 [NimbOS](https://github.com/equation314/nimbos)。该 OS 也是用 Rust 语言编写的实验操作系统,代码量在 5K 行左右,而与 x86 硬件相关部分的代码小于 1K 行。此外,它还实现了抢占式调度、多线程用户程序、基本系统调用的支持等 OS 功能。在设备方面,实现了 x86 的串口、HPET 高精度时钟、Local APIC 和 I/O APIC 中断控制器等。因此,NimbOS 既简单,又具有一个操作系统的最基本功能,非常适合作为我们的 Guest OS,运行到我们的 hyperviosr 上。 10 | 11 | 为了能在我们的 Hypervisor 上运行,我们只需关注 NimbOS 与硬件相关部分的代码即可,比如启动过程、x86 特殊指令、设备访问等。我们首先来了解 NimbOS 的启动过程,这部分的代码位于 [nimbos/kernel/src/platform/pc/multiboot.S](https://github.com/equation314/nimbos/blob/545ab808b462cf606bd5073c6dad9b86297b761c/kernel/src/platform/pc/multiboot.S)。 12 | 13 | NimbOS 是通过 [multiboot 协议](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Machine-state) 启动的。因此启动过程分为两部分,首先是 BIOS 或 bootloader 启动,将系统设置为协议规定的状态,再开始运行 NimbOS 的启动代码,完成剩余部分的启动。例如,在 x86 处理器上,OS 一般是从 16 位实模式开启启动的,但对于 multiboot 协议,就规定了刚进入 OS 时已经是 32 位保护模式。因此就需要 BIOS 或 bootloader 完成 16 位到 32 位的切换,然后 OS 直接从 32 位开始启动。下面列出了按照 multiboot 协议,在进入 OS 前,要求 BIOS 或 bootloader 设置好的系统状态: 14 | 15 | * `CR0.PE = 1` (保护模式) 16 | * GDT 有效 17 | * `CS`、`DS`、`ES`、`FS`、`GS`、`SS` 这些段寄存器有效 18 | * `EAX` = `0x2BADB002` (magic number) 19 | * `EBX` = Multiboot information address (目前不支持) 20 | * `ESP` 有效 21 | * 关中断 22 | 23 | 因此,在我们的 Hypervisor 中,为了遵循 multiboot 启动协议,有两种启动方式: 24 | 25 | 1. Hyperviosr 为 Guest OS 建立好所需的状态,直接进入 32 位 Guest。但是这样就需要配置 Guest GDT 和段寄存器; 26 | 2. 通过一段类似 BIOS 的代码,Hypervisor 先进入 16 位 Guest BIOS,之后 Guest GDT 的配置,16 位到 32 位的切换,Guest OS 启动状态的设置,都由 BIOS 来完成。 27 | 28 | 本项目实现的是方法二。该方法将复杂的状态设置都交给 Guest BIOS 来处理,而对 Hypervisor 的配置相对较少。 29 | 30 | ### 5.1.2 Guest BIOS 31 | 32 | 本节将介绍 Guest BIOS。虽然名字叫 BIOS,但为了简单,我们并没有实现一个 BIOS 应该具有的功能,如初始化设备、配置中断向量表 (为 OS 提供一些服务) 等,所以只能称得上是一个 bootloader,只用于 Guest OS 的启动。 33 | 34 | BIOS 完成了以下工作: 35 | 36 | 1. 配置初始 GDT,这是进入 32 所必须的 37 | 2. 从 16 位实模式切换到 32 位保护模式 38 | 3. 根据 multiboot 协议,初始化其他系统状态 39 | 4. 跳转到 32 位 Guest OS 的入口地址 (gPA),这里我们硬编码为 `0x20_0000` 40 | 41 | Guest BIOS 的核心代码如下: 42 | 43 | ```asm 44 | .code16 45 | entry16: 46 | cli 47 | cld 48 | xor ax, ax 49 | mov ds, ax 50 | mov es, ax 51 | mov ss, ax 52 | 53 | lgdt [prot_gdt_desc] ; initial GDT 54 | mov eax, cr0 55 | or eax, 0x1 ; CR0.PE = 1 56 | mov cr0, eax 57 | 58 | ljmp 0x8, entry32 ; long jump, CS = 0x8, EIP = entry32 59 | 60 | .code32 61 | entry32: 62 | mov ax, 0x10 ; set all data segment selectors to 0x10 63 | mov ds, ax 64 | mov es, ax 65 | mov ss, ax 66 | mov fs, ax 67 | mov gs, ax 68 | 69 | mov esp, 0x7000 ; temporary stack 70 | mov ecx, 0x200000 ; kernel entry 71 | mov eax, 0x2BADB002 ; multiboot magic 72 | mov ebx, 0 ; multiboot information (unsupported) 73 | jmp ecx 74 | 75 | .balign 16 76 | prot_gdt: 77 | .quad 0x0000000000000000 ; 0x00: null 78 | .quad 0x00cf9b000000ffff ; 0x08: code segment (base=0, limit=0xfffff, type=32bit code exec/read, DPL=0, 4k) 79 | .quad 0x00cf93000000ffff ; 0x18: data segment (base=0, limit=0xfffff, type=32bit data read/write, DPL=0, 4k) 80 | 81 | prot_gdt_desc: 82 | .short prot_gdt_desc - prot_gdt - 1 ; limit 83 | .long prot_gdt ; base 84 | ``` 85 | 86 | ### 5.1.3 Guest 物理内存布局 87 | 88 | 下面给出了在本阶段中,Guest 的物理内存布局。 89 | 90 | 在 Hypervisor 地址空间的两处高地址 `0x400_0000` 和 `0x400_1000`,分别是一块保留内存,用于存放 Guest BIOS 与 Guest OS 镜像。这两个地址在 QEMU 启动参数中指定。 91 | 92 | Guest 的物理内存布局(即 Hypervisor 所分配的 `GUEST_PHYS_MEMORY` 那块内存)如下。入口地址 `0x8000` 处放置Guest BIOS,地址 `0x20_0000`处放置Guest OS。在启动前,Guest BIOS 与 Guest OS 的镜像会先从 `0x400_0000` 拷贝到 `GUEST_PHYS_MEMORY` 中的相应位置。 93 | 94 | 除了 `0 ~ 0x100_0000` 这 16MB 的物理内存,Guest NimbOS 还会通过 MMIO 访问设备。目前 Guest 对设备的访问都是直通,因此 MMIO 内存段在嵌套页表中都使用对等映射,即 gPA 等于 hPA。此外,由于 MMIO 内存段都是设备内存,需要在 EPT 中配置为不使用 cache。Guest 所用的几个 MMIO 内存段如下表所示: 95 | 96 | | 名称 | 起始 gPA | 大小 | MemFlags | EPT 映射 | 97 | |-|-|-|-|-| 98 | | RAM | `0x0` | `0x100_0000` | RWX | `GUEST_PHYS_MEMORY` | 99 | | IO APIC | `0xFEC0_0000` | `0x1000` | RW, Dev | 对等 | 100 | | HPET | `0xFED0_0000` | `0x1000` | RW, Dev | 对等 | 101 | | Local APIC | `0xFEE0_0000` | `0x1000` | RW, Dev | 对等 | 102 | 103 | ## 5.2 处理更多 VM Exit 104 | 105 | ### 5.2.1 MSR 读写 106 | 107 | 当完成以上设置后,如果直接运行 Guest NimbOS,会出现以下错误: 108 | 109 | ![VM exit: WRMSR](figures/5-wrmsr-failed.png) 110 | 111 | 根据提示,出错原因是发生了一个名为 “MSR write” 的 VM exit,但我们未处理。出错的 Guest RIP 为 `0x200062`,反汇编 Guest NimbOS 代码可得: 112 | 113 | ```asm 114 | 200053: b9 80 00 00 c0 mov ecx, 0xc0000080 115 | 200058: ba 00 00 00 00 mov edx, 0x0 116 | 20005d: b8 00 09 00 00 mov eax, 0x900 117 | 200062: 0f 30 wrmsr 118 | ``` 119 | 120 | 即 Guest 执行了一条 `WRMSR` 指令,向 IA32_EFER MSR (`0xC000_0080`) 写入 `0x900`,导致发生 VM exit。为了解决这一问题,我们的处理方法很简单:直通所有 MSR 的访问。下面就来分析直通所有 MSR 访问是否会有问题。 121 | 122 | 下表列出了 Guest NimbOS 会用到的所有 MSR: 123 | 124 | | MSR | 用途 | 125 | |-|-| 126 | | `IA32_EFER` | 切换到 64 位,启用 syscall | 127 | | `IA32_STAR` | syscall 支持 | 128 | | `IA32_LSTAR` | syscall 支持 | 129 | | `IA32_FMASK` | syscall 支持 | 130 | | `IA32_GS_BASE` | per-CPU 数据支持 | 131 | | `IA32_KERNEL_GSBASE` | per-CPU 数据支持 | 132 | | x2APIC 相关 MSR | Local APIC 访问 | 133 | 134 | 对于 `IA32_EFER` 与 `IA32_PAT`,我们在 VMCS 中配置了对它们的切换,因此直通这两个 MSR 不会有问题。 135 | 136 | 对于 syscall 和 per-CPU 数据相关的 MSR,由于 Hypervisor 没有用到这些功能,且我们只有一个 Guest,所以将这些 MSR 完全分配给这唯一的 Guest 使用,也不会有问题,因此也可以直通。 137 | 138 | 剩下的 MSR 与 Local APIC 访问相关。会在下一章进行处理。 139 | 140 | 综上所述,目前我们可以安全地直通 Guest NimbOS 的所有 MSR 访问。 141 | 142 | 对于 MSR 的直通,需要在 VMCS 中配置 “**MSR-bitmap address**”。这是一个 4K 大小的页面,其中又分为 4 个子 bitmap,每个占用 1K,分别用于: 143 | 144 | * Read bitmap for low MSRs (`0x0 ~ 0x1FFF`) 145 | * Read bitmap for high MSRs (`0xC000_0000 ~ 0xC000_1FFF`) 146 | * Write bitmap for low MSRs (`0x0 ~ 0x1FFF`) 147 | * Write bitmap for high MSRs (`0xC000_0000 ~ 0xC000_1FFF`) 148 | 149 | 将 bitmap 中的相应位设为 1,就表示拦截这个 MSR 的访问,否则就直通。因此,如果要直通所有 MSR 的读写访问,就将该 4KB 页面全部清零。 150 | 151 | ### 5.2.2 CPUID 指令 152 | 153 | 处理 MSR 访问问题后,再次运行,会发生以下错误: 154 | 155 | ![VM exit: CPUID](figures/5-cpuid-failed.png) 156 | 157 | 此次错误的原因是 Guest 在 `0xffffff8000214068` 处执行了 `CPUID` 指令: 158 | 159 | ```asm 160 | ffffff800021405e: 48 89 fb mov rbx, rdi 161 | ffffff8000214061: 31 c0 xor eax, eax 162 | ffffff8000214063: 31 c9 xor ecx, ecx 163 | ffffff8000214065: 49 89 dd mov r13, rbx 164 | ffffff8000214068: 0f a2 cpuid 165 | ``` 166 | 167 | `CPUID` 指令用于获取当前处理器所支持的硬件特性等信息。在虚拟化下,Guest OS 通过该指令查询处理器硬件特性,Hypervisor 应该返回 vCPU 的虚拟硬件特性,而不是 Host CPU 的,所以不能直接运行,会强制进行拦截。 168 | 169 | 在执行 `CPUID` 指令前,需要先设置 `EAX`,我们把它叫做 leaf,即查询哪一类信息。有时候还可设置 `ECX`,被称为 sub-leaf。执行完该指令后,硬件会自动设置 `EAX`,`EBX`,`ECX`,`EDX` 这 4 个寄存器,表示返回结果。所以我们的 Hypervisor 就需要模拟硬件的这一操作,设置 Guest 的这些寄存器。 170 | 171 | 我们使用以下方法实现对 `CPUID` 指令的模拟:先在 Host CPU 上执行 `CPUID` 指令,得到 Host CPU 的硬件信息,然后进行少量修改并返回。由于 Guest NimbOS 没有太多地依赖 `CPUID` 指令,所以即使不做特殊处理也不会对 Guest 的正常运行造成影响。不过这里我们为了演示 `CPUID` 指令的模拟,对以下这些情况做特殊处理: 172 | 173 | * `EAX = 1` (Processor Info and Feature Bits):清除 VMX 位 (`ECX` bit 5),开启 Hypervisor 位 (`ECX` bit 31) 174 | * `EAX = 0x4000_0000` (Hypervisor Information):返回 Hypervisor 厂商信息字符串 (“`RVMRVMRVMRVM`”) 175 | * `EAX = 0x4000_0001` (Hypervisor Feature Bits):全返回 0 176 | 177 | ## 5.4 练习 178 | 1. 结合之前学习到的知识,解释 Guest BIOS 核心代码中 `prot_gdt` 和 `prot_gdt_desc` 都是什么内容。 179 | 2. 修改代码,使分配给 Guest OS 的内存容量从 16 MB 增加到 32 MB。 180 | 3. 简述:如果要使 NimbOS OS 被加载的地址从`0x20_0000`更改到其他地址,需要做哪些修改? -------------------------------------------------------------------------------- /hypervisor/x86_64/06-io-int.md: -------------------------------------------------------------------------------- 1 | # 6. I/O 和中断虚拟化 2 | 3 | 在本阶段,我们将介绍对串口和 Local APIC 时钟设备的模拟,作为 I/O 与中断虚拟化的一个简单示例。 4 | 5 | > 架构手册:[Intel 64 and IA-32 Architectures Software Developer’s Manual, Vol. 3A](https://cdrdv2.intel.com/v1/dl/getContent/671447), Chapter 11 6 | 7 | ## 6.1 概述:设备虚拟化 8 | 9 | 对于虚拟化来说,除了最基本的 CPU 与内存外,还包括重要的一部分,那就是外部设备 (device)。外部设备虽然种类繁多,如时钟、串口、网卡、键盘、磁盘控制器等等,但 CPU 与它们的基本交互方式都是一样的,无非就是两种:CPU 通过 I/O 操作调用设备的功能,或是反过来,设备通过中断来调用 CPU 的功能。因此,设备的虚拟化即 **I/O** 与**中断**的虚拟化。 10 | 11 | 根据虚拟化下 Guest 对设备访问方式的不同,可以分为以下三种实现: 12 | 13 | 1. **直通 (pass-through)**:即把一个物理设备完全分配给一个 Guest,其他 Guest 或是 Hypervisor 都无法再使用该设备。这种方法具有最佳的性能,实现起来也非常简单,但是不支持多路复用,即一个设备只能给一个 Guest 使用。到上一阶段为止,我们的 Guest NimbOS 对设备的访问都是采用直通的方式。 14 | 2. **模拟 (emulation)**:Hypervisor 拦截 Guest 的 I/O 操作或是发送给 Guest 的中断,并根据这些操作模拟设备的功能。这种方法通过 Hypervisor 的介入来支持多个 Guest 使用同一设备,还能虚拟出物理上不存在的设备。不过缺点是软件模拟的方式实现复杂、开销大。 15 | 3. **半虚拟化 (para-virtualization)**:是对设备模拟的一种优化,通过让 Guest 与 Host 按照人为规定的接口进行通信,而不需符合真实物理设备的规范。这种方法可以省去一些真实设备规范中的繁琐操作,从而大大提高设备访问速度,并简化设备驱动的实现。比如 [virtio](http://docs.oasis-open.org/virtio/virtio/v1.0/virtio-v1.0.html) 就提供了一套虚拟设备的规范,以此实现的设备驱动在性能与复杂性上都要远远优于按真实设备规范进行的实现。不过这种方式需要在 Guest 中实现专门的设备驱动,以此实现的 Guest OS 无法在真实物理机器上运行。 16 | 17 | | | 直通 | 模拟 | 半虚拟化 | 18 | |-|-|-|-| 19 | | 实现方式 | 简单 | 复杂 | 复杂 | 20 | | 性能开销 | 小 | 大 | 较小 | 21 | | 多路复用 | 不支持 | 支持 | 支持 | 22 | | Guest OS | 无修改 | 无修改 | 需要修改 | 23 | 24 | ## 6.2 I/O 虚拟化 25 | 26 | 首先我们来介绍几种基本的 I/O 方式,然后分别介绍它们的虚拟化方法: 27 | 28 | 1. **端口映射 I/O** (port-mapped I/O, PMIO):一个设备被分到一个或多个专用的端口,处理器通过专门的 I/O 指令,对这些端口进行读写,来访问设备。如 x86 使用 `IN`/`OUT` 指令读写 I/O 端口。 29 | 2. **内存映射 I/O** (memory-mapped I/O, MMIO):一段特殊的内存区域会被映射到设备上,处理器使用普通的内存读写指令,对该内存区域进行读写即可访问设备。这种方式在嵌入式系统中很常见。 30 | 3. **直接内存访问** (DMA):让设备直接读写物理内存,而无需处理器的介入,从而实现高速访问。 31 | 32 | ### 6.2.1 PMIO 33 | 34 | 在 x86 中,可使用以下指令进行端口映射 I/O 访问: 35 | 36 | * `IN`/`OUT`:向一个 I/O 端口,一次输入或输出一个字节/字/双字。 37 | * `INS` (`INSB`,`INSW`, `INSD`) / `OUTS` (`OUTSB`, `OUTSW`, `OUTSD`):字符串 I/O 指令,一般与 `REP` 前缀结合,可以一次输入或输出多个字节、字或是双字,具体数量可在 `ECX` 寄存器中指定。 38 | 39 | 为了模拟使用 PMIO 设备的行为,需要拦截先 Guest 的这些 I/O 指令。我们可以对 VMCS 进行如下配置: 40 | 41 | 1. 设置 VMCS primary processor-based VM-execution controls 的 **Unconditional I/O exiting** 位 (bit 24),可拦截所有的 I/O 指令。 42 | 2. 或是在 VMCS 中配置 **I/O bitmap**,那么只有当 I/O 指令访问的端口在 bitmap 中被设置了,才会被拦截。 43 | 44 | 当我们设置好拦截后,如果 Guest 执行了相应的 I/O 指令,就会发生一个名为 “I/O instruction” 的 VM exit。此时可查询 VMCS 中的 **Exit qualification** 字段,获取有关该 I/O 指令的详细信息,便于之后对该 I/O 操作的模拟,包括:(Intel SDM Vol. 3C, Section 27.2.1, Table 27-5) 45 | 46 | * 访问的端口 47 | * 访问的大小 (字节、单字、双字) 48 | * 访问的方向 (`IN` 或 `OUT`) 49 | * 是否是字符串指令 (带后缀 `S`) 50 | 51 | ### 6.2.2 MMIO 52 | 53 | MMIO 的访问与普通内存访问无异,主要看访问的物理地址是否在设备内存范围内。如下面这条指令: 54 | 55 | ```asm 56 | 88 07 mov byte ptr [rdi], al 57 | ``` 58 | 59 | 对于 MMIO 指令的拦截,需要与内存虚拟化相配合。在内存虚拟化中,Guest 的地址会经过影子页表或是嵌套页表的转换,映射为 Host 物理地址。我们只需在影子页表或是嵌套页表中,取消 MMIO 内存段的映射,即可使 Guest 在进行访问时发生 VM exit,从而进行拦截。例如在 EPT 中取消 MMIO 内存段对应的 gPA 的映射,就能让 Guest 的访问触发 EPT violation。 60 | 61 | 在拦截 MMIO 指令后,我们还需获取此访问的相关信息。对于访问的 gPA、是读是写这些信息,可以像处理 EPT violation 那样,通过查询 **Exit qualification** 字段获取。但对于读写的具体数值、访问大小这些信息,不能直接查到,需要手动解密进行访问的那条指令才行。例如以上指令编码为 `88 07`,解码后可知是一条内存写指令,写入大小为单字节,但写入的具体数值还需查询 `RAX` 寄存器的低 8 位才能得到。由于过于繁琐,本项目未实现 MMIO 指令的拦截与模拟。 62 | 63 | ### 6.2.3 DMA 64 | 65 | 最后简单介绍一下对 DMA 访问的虚拟化。 66 | 67 | 在 DMA 中,设备可直接访问物理内存,访问前需要由处理器告知设备所访问的内存的物理地址。如果采用设备直通的方式,将一个 DMA 设备完全分配给 Guest,Guest 告知设备的将是 Guest 物理地址,但只有将其转换为 Host 物理地址后才能正确访问。 68 | 69 | 因此,在没有特殊硬件的情况下,如果要直通一个 DMA 设备,仍然需要拦截 Guest 对访问的物理地址的配置,将其转换为正确的 Host 物理地址,才能告知设备。从而产生一定的拦截开销。 70 | 71 | 为了消除这部分的开销,就产生了一种特殊的硬件,即 IOMMU。类似普通的 MMU,将虚拟地址转换为物理地址 (在嵌套页表下是 Guest 虚拟地址转换为 Host 物理地址),IOMMU 也提供类似的转换,将 DMA 访问时的 Guest 物理地址转换为 Host 物理地址。这样在启用了 IOMMU 后,Guest 可照常告知设备一个 gPA,设备在使用该地址进行 DMA 访问时,会自动经过 IOMMU 的转换,得到相应的 hPA,从而实现正确的访问。 72 | 73 | 使用 IOMMU 可使 Guest 使用 DMA 设备时免去 Hypervisor 的介入,做到了真正的直通。IOMMU 还能用于设备的隔离,让设备的 DMA 访问限制在一个给定的范围内。 74 | 75 | ![IOMMU](figures/6-iommu.svg) 76 | 77 | ### 6.2.4 串口 I/O 介绍 78 | 79 | Guest NimbOS 和 Hypervisor 都会使用串口进行输入输出,与用户交互。串口 (serial port) 是一种串行端口,一次只能传输一个数据,与并行端口或并口相对应,是一种非常简单计算机与外界的通信方式。在 x86 上,串口控制器的硬件型号一般为 UART 16550,CPU 通过 I/O 端口对其进行控制。例如 COM1 串口使用 `0x3F8` ~ `0x3FF` 共 8 个端口。如果还有 COM2,则使用 `0x2F8` 开始的 8 个端口。 80 | 81 | 对于每个 I/O 端口,都对应一个串口控制器的寄存器,其中最重要的是 Data Register (DR) 和 Line Status Register (LSR)。如下表所示: 82 | 83 | | 端口偏移 | 寄存器 | 84 | |-|-| 85 | | **0** | **Data Register** | 86 | | 1 | Interrupt Enable Register | 87 | | 2 | Interrupt Identification and FIFO control registers | 88 | | 3 | Line Control Register | 89 | | 4 | Modem Control Register | 90 | | **5** | **Line Status Register** | 91 | | 6 | Modem Status Register | 92 | | 7 | Scratch Register | 93 | 94 | 当要将数据写入串口时,需要先读 LSR,直到它的第 5 位为 1,否则就一直读,然后往 DR 写入要发送的数据 (一个字节)。 95 | 96 | 当要从串口中读取数据时,也是先读一下 LSR,这时看第 0 位是否为 1。如果不是,表示目前串口中还没有数据可读;如果是 1,则再读一下 DR,即可读出 1 个字节。如下图所示。 97 | 98 | ![](figures/6-serial-io.svg) 99 | 100 | ### 6.2.5 串口 I/O 模拟 101 | 102 | 为了模拟串口设备,首先需要拦截 Guest 对串口的所有 I/O 访问,因此我们在 VMCS 中配置了对所有 I/O 指令的拦截。Guest 执行任何 I/O 指令,都会触发 VM exit,如果访问的端口在 `0x3F8` ~ `0x3FF` 范围内,就说明访问的是串口。此时 Hypervisor 需要模拟 UART 16550 的那些寄存器的行为 (主要就是 DR 与 LSR)。最终,我们会调用 arceos 提供的输入输出函数,实现对串口输入输出的模拟。 103 | 104 | 对串口写的模拟比较简单,让 LSR 直接返回 `0x20` 即可,即第 5 位一定为 1,这样 Guest 读完 LSR 后,就可以直接写 DR 了。而 Guest 对 DR 的写,就直接通过 `putchar()` 输出给定的字节。 105 | 106 | 对于串口读的处理稍有复杂。我们注意到 Guest 读串口这一过程可以看做一个生产者消费者模型,可读的字节会不断地被生产出来,而 Guest 就通过读 DR 来消耗一个字节。如果生产字节的速度大于 Guest 消耗字节的速度,就需要将剩余字节缓存起来。我们在实现中使用一个先进先出队列 (FIFO) 来缓存已生产但还未被 Guest 读到的字节。 107 | 108 | 对于字节的生产,需要 Hypervisor 不断调用 `getchar()`。关于调用的时机,一种方法是只在真的有输入时才调用,但这样就需要实现串口中断,使每次用户输入时都会产生一个中断。我们的实现采用的是另一种方法:注意到对于一个实现正确的 Guest,在读 DR 之前,一定会先读一下 LSR。只要我们在此时尝试“生产字节”,就能保证 Guest 在读 DR 时一定能读到现有的字节,所以我们只需在 Guest 读 LSR 时调用 `getchar()` 即可。 109 | 110 | * Guest 读 LSR,Hyper先调用 `getchar()` 检查目前是否有新输入的字节。如果有,则将其加入缓存队列的末尾。最后的返回结果需要看目前缓存队列是否为空,如果为空,返回 `0x20` (第 5 位为 1);否则,返回 `0x21` (第 5 位和第 0 位都为 1)。 111 | * Guest 读 DR,Hypervisor 从缓存队列开头取走一个字节,返回给 Guest。 112 | * Guest 写 DR,Hypervisor 直接调用 `putchar()`,输出一个字节。 113 | 114 | ## 6.3 中断虚拟化 115 | 116 | 与 I/O 操作相反,中断是由设备产生,发送给处理器的异步事件。CPU 在收到中断后,会打断当前的执行流,进入内核态的中断处理程序,对该中断进行相应的处理。 117 | 118 | ![中断虚拟化](figures/6-ints.svg) 119 | 120 | 在上图 (a) 中,是没有虚拟化时的情形。设备产生中断后 (一般被称为 IRQ),CPU 就会进入 OS 提供的中断处理程序。如在 x86 中,OS 会配置 IDT 来设置中断处理程序。 121 | 122 | 在上图 (b) 中,是虚拟化下并采用设备直通时的情形。此时设备产生的中断会绕过 Hypervisor,CPU 会直接进入 Guest OS 中的中断处理程序,而无需 Hypervisor 的介入。 123 | 124 | 在上图 (c) 中,是虚拟化下并采用设备模拟时的情形。此时设备产生的中断会触发 VM exit,进入 Hypervisor 中的 VM exit 处理程序,Hypervisor 可以选择向 Guest 注入一个虚拟中断 (vIRQ),使得下次进入 Guest 后直接进入 Guest 的中断处理程序。 125 | 126 | VMCS 中可以配置上图的两种虚拟化方式。开启 pin-based VM execution controls 的 “**External interrupt**” 位 (bit 0),即可让 Hypervisor 拦截所有外部中断,即上图 (c) 所示;如果不设置该位,则是上图 (b) 的中断直通情形。 127 | 128 | 此外,VMCS 中还有一些与中断虚拟化相关的字段: 129 | 130 | - VM-exit controls 字段的 “Acknowledge interrupt on exit” 位 (bit 15):当因外部中断发生 VM exit 时,是否保留该中断的信息; 131 | - VM-exit interruption information:因外部中断发生 VM exit 时的中断信息 (需要设置 “Acknowledge interrupt on exit” 位); 132 | - VM-entry interruption-information field:向 Guest 注入一个虚拟事件 (中断或异常)。 133 | 134 | ### 6.3.1 Local APIC 介绍 135 | 136 | 在 Guest NimbOS 中,与中断相关的设备包括 Local APIC 和 I/O APIC。我们将在本阶段实现对 Local APIC 的模拟,以及由它产生的时钟中断。 137 | 138 | APIC 的全称是**高级可编程中断控制器 (Advanced Programmble Interrupt Controller)**,用于 Intel 多核处理器上中断的控制。区别于 Intel 8259 PIC (Programmble Interrupt Controller),PIC 只能用于单核处理器。共有两种 APIC:集成在处理器内部,只能被当前核访问的 Local APIC;以及在处理器外部,全系统都可访问的 I/O APIC。在我们的实现中,仍然将 I/O APIC 直通给 Guest,只模拟 Local APIC。 139 | 140 | Local APIC 有两种工作模式:**xAPIC** 与 **x2APIC**。两者的主要区别在于寄存器的访问方式,xAPIC 是通过 MMIO 访问的,基址为 `0xFEE0_0000`;而 x2APIC 使用 `RDMSR` 与 `WRMSR` 指令,读写 MSR 来访问。此外 xAPIC 只支持 8 位 APIC ID,而 x2APIC 支持 32 位 ID。通过设置 `IA32_APIC_BASE` MSR 的第 11 与 10 位,可在硬件支持的情况下,分别启用这两种模式。 141 | 142 | 下表为 Local APIC 中一些重要的寄存器: 143 | 144 | | MSR Address. (x2APIC) | MMIO Offset (xAPIC) | 名称 | 描述 | 145 | |-|-|-|-| 146 | | `0x802` | `0x020` | Local APIC ID register | 编号 | 147 | | `0x803` | `0x030` | Local APIC Version register | 版本 | 148 | | `0x80B` | `0x0B0` | **EOI register** | **End-Of-Interrupt** | 149 | | `0x830` | `0x300`/`0x310` | Interrupt Command Register (ICR) | 发送 IPI | 150 | | `0x832` | `0x320` | **LVT Timer register** | **LVT 表项 (时钟)** | 151 | | `0x838` | `0x380` | **Initial Count register** | **时钟初始计数器** | 152 | | `0x839` | `0x390` | **Current Count register** | **时钟当前计数器** | 153 | | `0x83E` | `0x3E0` | **Divide Configuration Register (DCR)** | **时钟分频寄存器** | 154 | 155 | 例如,Local APIC ID 寄存器为每个 Local APIC 提供了一个唯一的编号,由于 Local APIC 与 CPU 绑定,该编号也可用作 CPU 的编号;当 CPU 收到一个中断,处理完毕后,需要向 EOI 寄存器写入 0,告知中断处理结束;此外还有一些与时钟相关的寄存器。 156 | 157 | ### 6.3.2 Local APIC 时钟 158 | 159 | Local APIC 自身也能产生一些中断,可在 LVT (local vector table) 表中配置。比如时钟中断,每个 Local APIC 上都有一个时钟,能以 one-shot (只产生一次) 或 periodic (周期性产生) 的方式产生时钟中断。时钟的基础频率随机器不同而不同,一般在使用之前需要标定测量。我们的 Guest NimbOS 就是使用 Local APIC 时钟作为时钟中断发生器的。通过 LVT Timer 寄存器,可对时钟进行配置,其格式如下: 160 | 161 | 162 | 163 | 当软件配置好时钟分频寄存器 (DCR) 和初值计数器 (Initial Count) 后,硬件会将当前计数器 (Current Count) 设为和初值一样,然后时钟就启动了,当前计数器会按照设定的频率开始倒数。一旦当前计数器倒数到 0,就会产生一个时钟中断。此时如果时钟模式是 one-shot,时钟就停止了;否则如果是 periodic,当前计数器就会再次变为初值,重新开始倒数,周期性地产生时钟中断。 164 | 165 | ### 6.3.3 Local APIC 时钟模拟 166 | 167 | 由于每个 CPU 都有一个 Local APIC,所以在模拟时,需要为每个 `VmxVcpu` 结构都创建一个虚拟的 Local APIC 实例。为了简单起见,我们模拟的是 x2APIC,这样只需拦截 MSR 访问即可,而 xAPIC 就需要拦截 MMIO 访问。 168 | 169 | 我们的模拟主要需要考虑以下三种情况: 170 | 171 | 1. Guest 启动时钟。这种情况发生在 Guest 修改了时钟参数,需要重新启动时钟时。此时我们需要根据 Guest 之前对时钟分频寄存器 (DCR) 的配置,计算出所设置的时钟频率。然后根据时钟频率,计算出时钟下一次超时的具体时间,保存到变量 `deadline_ns` 中 (单位:纳秒)。 172 | 2. 时钟超时。Hypervisor 会不断检查时钟是否超时 (如在每次 VM exit 时),如果当前的时间大于等于 `deadline_ns` 就说明时钟超时,此时需要向 Guest 注入一个虚拟时钟中断 (如果 Guest 在 LVT Timer 寄存器中屏蔽了时钟中断则不注入)。此外,我们还需要正确处理不同的时钟模式:如果时钟模式是 periodic,则更新 `deadline_ns` 作为下一次超时的时间;如果是 one-shot,则停止时钟。 173 | 3. Guest 读当前计数器 (Current Count)。由于在真实的硬件中,该寄存器的值会随时间的流逝时刻变化着,所以我们需要模拟出这种情况。当 Guest 读该寄存器时,我们先计算自上次启动时钟以来,过去了多少时间 (用 `elapsed_ns` 表示),然后根据时钟分频寄存器 (DCR) 的值,将其换算为 Local APIC 的时钟周期数 (用 `elapsed_cycles` 表示)。最后,返回初值计数器 (Initial Count) 减去 `elapsed_cycles`,即可模拟时钟当前计数器的值。 174 | 175 | 这三种情况的核心代码如下: 176 | 177 | ```Rust 178 | impl ApicTimer { 179 | /// Check if an interrupt generated. if yes, update it's states. 180 | pub fn check_interrupt(&mut self) -> bool { 181 | if self.deadline_ns == 0 { 182 | false 183 | } else if H::current_time_nanos() >= self.deadline_ns { 184 | if self.is_periodic() { 185 | self.deadline_ns += self.interval_ns(); 186 | } else { 187 | self.deadline_ns = 0; 188 | } 189 | !self.is_masked() 190 | } else { 191 | false 192 | } 193 | } 194 | 195 | /// Current Count Register. 196 | pub fn current_counter(&self) -> u32 { 197 | let elapsed_ns = H::current_time_nanos() - self.last_start_ns; 198 | let elapsed_cycles = (elapsed_ns / APIC_CYCLE_NANOS) >> self.divide_shift; 199 | if self.is_periodic() { 200 | self.initial_count - (elapsed_cycles % self.initial_count as u64) as u32 201 | } else if elapsed_cycles < self.initial_count as u64 { 202 | self.initial_count - elapsed_cycles as u32 203 | } else { 204 | 0 205 | } 206 | } 207 | 208 | const fn interval_ns(&self) -> u64 { 209 | (self.initial_count as u64 * APIC_CYCLE_NANOS) << self.divide_shift 210 | } 211 | 212 | fn start_timer(&mut self) { 213 | if self.initial_count != 0 { 214 | self.last_start_ns = H::current_time_nanos(); 215 | self.deadline_ns = self.last_start_ns + self.interval_ns(); 216 | } else { 217 | self.deadline_ns = 0; 218 | } 219 | } 220 | } 221 | ``` 222 | 223 | ### 6.3.4 处理 Guest 中断屏蔽 224 | 225 | 在按以上方法实现对时钟中断的模拟后,运行发现,Guest NimbOS 还是无法正常收到 Hypervisor 向其注入的虚拟中断。这是因为在进行中断注入时,Guest 正好处于关中断状态 (`RFLAGS.IF = 0`),此时将无法成功注入该虚拟中断,造成了中断的丢失。 226 | 227 | 为了解决这一问题,Intel VMX 提供了一种 interrupt-window exiting 的机制。我们可在 VMCS primary processor-based VM-execution controls 中配置 “**Interrupt-window exiting**” (bit 2),当设置了这一位后,即使 Guest 目前关了中断,只要它一打开,就会发生一个名为 “**interrupt window**” 的 VM exit。此时的 Guest 一定处于开中断状态,就能成功进行中断注入了。 228 | 229 | 除了 `RFLAGS.IF = 0`,Guest 在一些其他状态下,也可能导致无法成功注入虚拟中断。在 VMCS Guest-state area 中,有一个 “**interruptibility state**” 字段,记录了中断被阻塞的原因。因此,只有该字段为 0,且 `RFLAGS.IF = 1`,才能向 Guest 注入虚拟中断。 230 | 231 | 在实现中,我们用一个队列缓存所有要向 Guest 注入的事件 (包括中断与异常),即我们在 `VmxVcpu` 结构中新增的 `pending_events` 字段,其中保存了中断或异常的向量号,以及可选的错误码 (error code)。当要向 Guest 注入虚拟中断时,不直接注入,而是将其加入该队列。 232 | 233 | 然后,我们在每次 VM entry 前 (VM exit 处理完毕) 进行以下操作: 234 | 235 | 1. 检查该队列是否为空,即是否有待注入的事件; 236 | 2. 如果有待注入的事件,检查目前是否可向 Guest 注入中断 (检查 “interruptibility state” 字段以及 `RFLAGS.IF`): 237 | * 如果可注入,从缓存队列中取走一个事件,直接进行注入; 238 | * 否则,不进行注入,而是配置 VMCS 控制字段,开启 interrupt-window exiting。如此一来,只要 Guest 一打开中断,就会触发一个名为 “interrupt window” 的 VM exit,此时我们再将 interrupt-window exiting 功能关闭。当完毕后再次执行第 1 步时,就一定是可注入的。 239 | 240 | ```Rust 241 | /// Whether the guest interrupts are blocked. (SDM Vol. 3C, Section 24.4.2, Table 24-3) 242 | fn allow_interrupt(&self) -> bool { 243 | let rflags = VmcsGuestNW::RFLAGS.read().unwrap(); 244 | let block_state = VmcsGuest32::INTERRUPTIBILITY_STATE.read().unwrap(); 245 | rflags as u64 & x86_64::registers::rflags::RFlags::INTERRUPT_FLAG.bits() != 0 246 | && block_state == 0 247 | } 248 | 249 | /// Try to inject a pending event before next VM entry. 250 | fn check_pending_events(&mut self) -> HyperResult { 251 | if let Some(event) = self.pending_events.front() { 252 | if event.0 < 32 || self.allow_interrupt() { 253 | // if it's an exception, or an interrupt that is not blocked, inject it directly. 254 | vmcs::inject_event(event.0, event.1)?; 255 | self.pending_events.pop_front(); 256 | } else { 257 | // interrupts are blocked, enable interrupt-window exiting. 258 | self.set_interrupt_window(true)?; 259 | } 260 | } 261 | Ok(()) 262 | } 263 | ``` 264 | 265 | # 6.4 练习 266 | 267 | 1. 编码实现一个新的虚拟PMIO设备,端口号自定: 268 | 1. 实现其功能:Guest 写数据时打印一行日志,Guest 读数据时返回一个常数; 269 | 2. *编码在 Guest BIOS 中使用 `IN`/`OUT` 指令调用这个设备。 270 | 2. **编码实现一个新的虚拟PMIO设备,端口号自定 271 | 1. **实现其功能:Guest 读数据时,返回 Guest 最后一次写入的数据;若 Guest 没有写过数据,返回0。 272 | 2. **简述:为了让虚拟设备能够保存内部状态,代码要做哪些修改? 273 | -------------------------------------------------------------------------------- /hypervisor/x86_64/README.md: -------------------------------------------------------------------------------- 1 | # Arceos x86-64 Hypervisor介绍 2 | 3 | 本文档介绍x86-64下基于arceos的Hypervisor的设计和实现。此Hypervisor的设计和实现源自[RVM-Tutorial](https://github.com/rcore-os/RVM-Tutorial.git),文档中大部分也来自于[RVM-Tutorial的文档](https://github.com/equation314/RVM-Tutorial/wiki)。 4 | 5 | ## 目录 6 | 7 | * [0. 开发环境和代码结构](./00-env.md) 8 | * [1. Intel VMX简介和初始化](./01-vmx.md) 9 | * [2. VMCS 配置](./02-vmcs.md) 10 | * [3. VM-entry 和 VM-exit](./03-vmlaunch.md) 11 | * [4. EPT 与内存隔离](./04-ept.md) 12 | * [5. 运行一个相对完整的Guest OS](./05-nimbos.md) 13 | * [6. I/O 和中断虚拟化](./06-io-int.md) 14 | -------------------------------------------------------------------------------- /hypervisor/x86_64/aa-answer.md: -------------------------------------------------------------------------------- 1 | # 各节练习参考答案 2 | 3 | ## 1.6 4 | 5 | 1. 略 6 | 2. `hardware_enable` 函数调用 `VmxRegion::new` 以分配并初始化 `vmx_region`。`VmxRegion::new` 调用 `PhysFrame::alloc_zero` 以分配一页4KB的物理内存并清零,然后在此页的前4字节写入 `revision_id`。 7 | 8 | ## 2.6 9 | 10 | 1. 如下: 11 | 1. 如下: 12 | 1. 设置 `Primary Processor-Based VM-Execution Controls` 字段中的 `HLT exiting` 位; 13 | 2. 设置 `Primary Processor-Based VM-Execution Controls` 字段中的 `PAUSE exiting` 位; 14 | 3. 设置 `Pin-Based VM-Execution Controls` 中的 `External-interrupt exiting` 位; 15 | 4. Guest 的 `rflags` 寄存器存放在 `VMCS` 的 `Guest-state area` 中,使用 `VMREAD/VMWRITE` 即可编辑; 16 | 5. 设置 `Exception Bitmap` 的 14 位(#PF的编号),同时要正确设置 `Page-Fault Error-Code Mask` 和 `Page-Fault Error-Code Match`; 17 | 6. 设置 `Primary Processor-Based VM-Execution Controls` 中的 `Unconditional I/O exiting` 位; 18 | 7. 设置 `Primary Processor-Based VM-Execution Controls` 中的 `Use I/O bitmaps` 位,分配两个4KB页存储 `I/O bitmaps A` 和 `I/O bitmaps B`,设置其中对应 `0x3f8` 的位,并在 `VMCS` 中设置其地址; 19 | 8. 清空 `Primary Processor-Based VM-Execution Controls` 中的 `Use MSR bitmaps` 位; 20 | 9. 设置 `Primary Processor-Based VM-Execution Controls` 中的 `Use MSR bitmaps` 位,分配一个4KB页存储 `MSR bitmap`,设置其中控制 `IA32_EFER(0xC0000080)` 写入的位,并在 `VMCS` 中设置其地址; 21 | 10. 设置 `Cr0 Guest/Host Masks` 的第 31 位。 22 | 2. 首先要给每一个 vCPU 创建并配置一个 `VMCS`。在切换 vCPU 时,要把目标 vCPU 的 `VMCS` 设置为“当前 `VMCS`”,并切换其他不在 `VMCS` 中的 Guest 状态。 23 | 24 | ## 3.4 25 | 26 | 1. 如下: 27 | 1. RSP的变化过程: 28 | 1. 在 VM-entry 的过程中,RSP 的原始值被保存到 `VmxVcpu` 结构体的 `host_stack_top` 字段中,然后 RSP 被设置为 `VmxVcpu` 结构体的地址,让 `restore_regs_from_stack` 宏能从 `VmxVcpu` 结构体的 `guest_regs` 字段中弹出 Guest 通用寄存器的值。执行 `vmlaunch` 时,RSP 被切换为 `VMCS Guest-state area` 中记录的 Guest RSP。 29 | 2. 在 VM-exit 时,Guest 的 RSP 被保存到 `VMCS Guest-state area` 中,CPU 从 `VMCS Host-state area` 中恢复 Host RSP(被提前设置为 `VmxVcpu` 结构体的 `host_stack_top` 字段的地址),然后 `save_regs_to_stack` 将 Guest 通用寄存器的值压入 `VmxVcpu` 结构体的 `guest_regs` 字段中;Guest 通用寄存器保存完成后,RSP 指向 `VmxVcpu` 结构体的地址,代码一方面把这个地址保存到 R15 寄存器中以供后面使用(因为 R15 是被调用者保存的寄存器,在调用 `vmexit_handler` 后保持不变),另一方面保存到 RDI 寄存器中,作为调用 `vmexit_handler` 的参数;然后,代码从 `VmxVcpu` 结构体的 `host_stack_top` 字段中恢复VM-entry前的 RSP 并调用 `vmexit_handler`;调用完成后,按照和 VM-entry 中一致的程序恢复 Guest 通用寄存器的值并回到 Guest 中。 30 | 2. Guest 通用寄存器的值保存在 `VmxVcpu` 结构体的 `guest_regs` 字段中,由 `restore_regs_from_stack` 和 `save_regs_to_stack` 两个宏负责恢复和保存。具体来说,恢复和保存时,将 RSP 临时指向 `VmxVcpu` 结构体的 `guest_regs` 字段,然后用 push/pop 指令读写其中的值到通用寄存器。 31 | 3. 在 VM-entry 时,代码保存了 RSP 的值到 `VmxVcpu` 结构体的 `host_stack_top` 字段中;在 VM-exit 时调用 `vmexit_handler` 前,代码会将栈切回这里,从而给 `vmexit_handler` 提供一个可用的栈。 32 | 33 | ## 4.4 34 | 35 | 1. 考虑每个 Guest VM 的操作系统也需要一份单独的页表,共需要 44 份影子页表; 36 | 2. 开启 4 级页表后,Guest 的一次访存实际需要访问 5 次 Guest 内存,而每次访问 Guest 内存又需要 5 次物理内存访问,故一共 25 次; 37 | 3. 主要需要处理 EPT Violation,如果并非真的非法访问而是访问尚未分配的内存,则动态分配一定量的内存给Guest。为此还需要实现合适的分配和记录机制。 38 | 39 | ## 5.4 40 | 41 | 1. prot_gdt 是一个有两个有效项的全局描述符表,其中定义了一个代码段和一个数据段,均为4GB大小的对等映射。prot_gdt_desc 是指向这个全局描述符表的描述符,记录了其地址和大小,用作 lgdt 指令的参数; 42 | 2. 修改 `GUEST_PHYS_MEMORY_SIZE` 为 `0x200_0000`; 43 | 3. 修改 `GUEST_ENTRY` 以让 NimbOS 内核被复制到其他地址;同时修改 NimbOS BIOS 中的跳转地址。 44 | 45 | ## 6.1 46 | 47 | 1. 编写一个结构体并正确实现 `PortIoDevice` trait,然后创建一个该结构体添加到 `VIRT_DEVICES` 中; 48 | 2. 为了让虚拟设备能保存并修改内部状态,需要修改 `PortIoDevice` trait,在其 `read` 和 `write` 函数中使用 `&mut self`。 49 | -------------------------------------------------------------------------------- /hypervisor/x86_64/figures/4-ept.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
offset
offset
guest CR3
guest CR3
offset
offset
EPT pointer
EPT pointer
Guest Physical Address
Guest Physical Address
Host Physical Address
Host Physical Address
Guest Virtual Address
Guest Virtual Address
Guest Page Table
Guest Page Table
Extended Page Table
Extended Page Table
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /hypervisor/x86_64/figures/4-nested.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Guest VM
Guest VM
gVA
gVA
gPA
gPA
hPA
hPA
hVA
hVA
Guest page table
Guest page...
Host page table
Host page table
Nested page table
Nested page t...
host CR3
host CR3
guest CR3
guest CR3
NPT root
NPT root
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /hypervisor/x86_64/figures/4-shadow1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Guest VM
Guest VM
gVA
gVA
gPA
gPA
hPA
hPA
hVA
hVA
Guest page table
Guest page...
Host page table
Host page table
Guest physical mappings
Guest physical m...
host CR3
host CR3
guest CR3
guest CR3
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /hypervisor/x86_64/figures/4-shadow2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Guest VM
Guest VM
gVA
gVA
gPA
gPA
hPA
hPA
hVA
hVA
Shadow page table
Shadow page t...
host CR3
host CR3
Host page table
Host page table
Guest page table
Guest page...
guest CR3
guest CR3
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /hypervisor/x86_64/figures/5-cpuid-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/x86_64/figures/5-cpuid-failed.png -------------------------------------------------------------------------------- /hypervisor/x86_64/figures/5-wrmsr-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/x86_64/figures/5-wrmsr-failed.png -------------------------------------------------------------------------------- /hypervisor/x86_64/figures/6-apic-timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/x86_64/figures/6-apic-timer.png -------------------------------------------------------------------------------- /hypervisor/x86_64/figures/6-iommu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Physical Memory
Physical Memory
CPU
CPU
Device
Device
MMU
MMU
IOMMU
IOMMU

host physical address

host physical...
host/guest virtual address
host/guest virt...
guest physical address
guest physical...
DMA
DMA
load/
store
load/...
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /hypervisor/x86_64/figures/6-prob2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arceos-hypervisor/2024-virtualization-campus/167c47cb8fd2e1678b1c604f291ef9cd25147f94/hypervisor/x86_64/figures/6-prob2.png -------------------------------------------------------------------------------- /hypervisor/x86_64/figures/6-serial-io.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
read Line Status Register
read Line Status Register
Yes
Yes
No
No
bit 5 = 1?
bit 5 = 1?
write Data Register
write Data Register
read Line Status Register
read Line Status Register
Yes
Yes
No
No
bit 0 = 1?
bit 0 = 1?
read Data Register
read Data Register
write success
write success
read success
read success
read failed
read failed
serial port write
serial port write
serial port read
serial port read
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /tasks/README.md: -------------------------------------------------------------------------------- 1 | # 任务列表 2 | 3 | * [多核支持](https://github.com/arceos-hypervisor/2023-virtualization-campus/blob/master/tasks/multi_core_support.md) 4 | 5 | * [多VM支持](https://github.com/arceos-hypervisor/2023-virtualization-campus/blob/master/tasks/multi_vm_support.md) 6 | 7 | * [内存动态管理](https://github.com/arceos-hypervisor/2023-virtualization-campus/blob/master/tasks/dynamic_memory_management.md) 8 | 9 | * [设备和中断虚拟化](https://github.com/arceos-hypervisor/2023-virtualization-campus/blob/master/tasks/device_and_interrupt_virtualization.md) 10 | 11 | * [虚拟机迁移](https://github.com/arceos-hypervisor/2023-virtualization-campus/blob/master/tasks/vm_migration.md) 12 | 13 | * [实时虚拟机](https://github.com/arceos-hypervisor/2023-virtualization-campus/blob/master/tasks/real_time_vm.md) 14 | 15 | 16 | -------------------------------------------------------------------------------- /tasks/device_and_interrupt_virtualization.md: -------------------------------------------------------------------------------- 1 | # 设备和中断虚拟化 2 | 3 | ## 任务描述: 4 | 5 | 向虚拟机提供各种不同的虚拟设备是Hypervisor的重要工作。从I/O类型上分,虚拟设备可以分为端口映射 I/O(PMIO) 设备、内存映射I/O(MMIO)设备、DMA设备等;从虚拟机访问设备的方式上分,可以分为直通设备、模拟设备和半虚拟化设备等。 6 | 7 | PMIO是一种较古老的I/O方式,每个设备有一个或多个端口号,CPU通过专用的I/O指令读写数据;由于其CPU占用率高等缺点,许多新的架构已经不支持此种I/O方式。MMIO是相对较为主流的I/O方式,设备接入总线,并在地址空间中有一段地址,CPU使用访存指令访问此段地址即可完成和设备的数据交互。DMA更进一步允许设备直接读写主存。 8 | 9 | “直通”指将某个物理设备分配给某台虚拟机独占使用,其他虚拟机和Hypervisor都不能访问;这种方式性能最好,实现简单,但是设备利用率可能不高,也无法给虚拟机提供不存在的设备。“模拟”指Hypervisor向虚拟机提供物理上不存在的设备,拦截虚拟机对该设备的所有访问操作,并且用代码模拟设备的功能;这种方式最为灵活,但实现复杂,性能不高。半虚拟化指虚拟机内的系统和Hypervisor按照约定好的接口进行通信,而不需要遵循真实设备的访问方式;这种方式性能相比于纯粹模拟更好,但需要虚拟机内的系统知道自己处于虚拟机中并安装相应的驱动程序;常见的半虚拟化规范有virtio等。 10 | 11 | 在大部分情况下,访问设备离不开中断机制。中断是设备主动通知CPU的重要途径。 12 | 13 | ## 任务要求: 14 | 15 | 1.【基础】理解和实现x86下PMIO虚拟化 16 | 1. 阅读理解现有的PMIO虚拟方案;如果没有完成“6. I/O 和中断虚拟化”一节的所有习题,现在完成; 17 | 2. 补全现有的虚拟16550UART实现,或者再添加一个新的虚拟PMIO设备; 18 | 19 | 2.【基础,和3二选一】MMIO虚拟化 20 | 1. 了解mmio相关的知识; 21 | 2. 选取一个MMIO设备(如PCIE设备等)并实现; 22 | 23 | 3.【基础,和2二选一】virtio虚拟化 24 | 1. 了解virtio相关的知识; 25 | 2. 实现一个virtio网络设备和一个virtio磁盘设备。 26 | 27 | ## 任务考核 28 | -------------------------------------------------------------------------------- /tasks/dynamic_memory_management.md: -------------------------------------------------------------------------------- 1 | # 内存动态管理 2 | 3 | ## 任务描述: 4 | 5 | Hypervisor的的内存动态管理是指在虚拟化环境中有效管理和分配物理内存资源,以支持多个虚拟机(VM)和虚拟机实例的运行。内存动态管理主要需要实现内存分配、内存回收、内存共享等功能。内存分配能够在虚拟机启动或需要更多内存时,能够动态地为其分配物理内存。内存回收则是在虚拟机有空余内存时能够回收内存以便提供给其他虚拟机使用。内存共享让虚拟机之间能够共享一定的内存,从而提高虚拟机之间交互性能。 6 | 7 | ## 任务要求: 8 | 9 | 基于ArceOS,完成其中一个架构(aarch64, x86, riscv)Hypervisor支持内存的动态管理,要求代码可扩展性强(如果有余力,鼓励完成多个架构的实现)。实现VM运行过程中,Hypervisor能够动态地给VM分配内存和回收内存。在此基础上,还可以继续实现多VM之间的内存共享。 10 | 11 | ## 任务考核: 12 | 13 | 1. 详细的开发和设计文档,代码有清晰的注释。 14 | 15 | 2. 现场演示:演示各个功能的展示效果,打印出相关信息。 16 | 17 | 3. 测试:需要测例的功能应提供不少于两个测例。 18 | -------------------------------------------------------------------------------- /tasks/multi_core_support.md: -------------------------------------------------------------------------------- 1 | # 多核支持 2 | 3 | ## 任务描述 4 | 5 | 现代计算机通常具有多个处理核心,这被称为多核架构。每个核心都可以执行独立的计算任务,因此多核处理器可以同时处理多个任务,提高了计算机的性能。多核支持此处指为虚拟机管理软件(如Hypervisor)添加对多核处理器的支持,能够充分利用多核处理器的并行计算能力,以确保虚拟机在多核环境中获得良好的性能。 6 | 7 | 关于多核支持,首先需要了解在客户机视角的CPU实际上是由Hypervisor虚拟化出的VCpu。通常,一个物理CPU可与一个或多个VCpu绑定。多核支持可以被分解为以下子任务:1. 多核启动。在多核支持的环境中,Hypervisor需要实现多核处理器的启动和初始化。这意味着每个核心都需要被正确地配置和准备,以便可以执行虚拟化操作。这包括启动和配置每个核心上的VCpu,以确保它们能够协同工作,共同支持客户机的运行。 2. 多核同步。多核处理器的核心是并行执行的,因此Hypervisor需要实现多核同步机制。这包括确保各个核心上运行的VCpu之间可以互相通信和同步,以维护虚拟机的稳定性和隔离性。虚拟机的核间通信通常需要触发核间中断来保持一致性,所以hypervisor需要对核间中断进行处理,以保证虚拟核间通信的顺利进行。 8 | 9 | ## 任务要求 10 | 11 | 1. 【基础】基于ArceOS,完成其中一个架构(aarch64, x86, riscv)hypervisor支持多核心客户机的运行,要求代码可扩展性强(如果有余力,鼓励完成多个架构的实现)。此处的实现令一个物理CPU只与一个VCpu绑定,即一一对应,其上只需运行单个客户机。例如:4个物理CPU,每个CPU分别对应1个VCpu,共4个VCpu。单个客户机使用这4个VCpu。 12 | 13 | 2. 【进阶】基于【基础】的实现,令一个物理CPU与多个VCpu绑定,其上仍然只需运行单个客户机。一个物理CPU与多个VCpu绑定会涉及到调度算法以及上下文的切换,可以参考现有常用的调度算法(CFS、FIFO、RR)进行实现,或对其进行改进。例如:2个物理CPU,每个CPU分别对应2个VCpu,共4个VCpu。单个客户机使用这4个VCpu。 14 | 15 | ## 任务考核 16 | 17 | 1. 现场演示:运行多核客户机,并在其中运行利用多核执行的程序,打印出相关信息。 18 | 19 | 2. 测试:首先为每个实现的功能提供合适的测试样例,其次进行性能测试,评估Hypervisor在多核环境中的性能和资源利用率。 20 | 21 | 3. 详细的代码文档。 22 | 23 | 24 | -------------------------------------------------------------------------------- /tasks/multi_vm_support.md: -------------------------------------------------------------------------------- 1 | # 多VM支持 2 | 3 | # 任务描述: 4 | 5 | 虚拟化的一个重要作用在于:允许用户在一台物理设备上同时运行多台虚拟机,并使其协同工作。在这类场景中,Hypervisor的工作内容要明显多于运行单台虚拟机时:不仅要负责内存、CPU、物理设备等物理资源的隔离、分配与动态调度,也负责虚拟设备的模拟,虚拟机间通信的处理等。 6 | 7 | 目前arceos的Hypervisor仅支持单台虚拟机和单个虚拟CPU运行。这限制了arceos的能力,本任务要求在arceos的Hypervisor中实现对多虚拟机的支持。 8 | 9 | 为了支持多虚拟机的正常运行,至少要解决下列问题:首先作为一切的基础,需要实现多台虚拟机多个虚拟CPU的管理和调度;同时,目前对虚拟设备的访问是不区分虚拟机的,为了正常运行多个虚拟机,需要让不同虚拟机拥有自己的虚拟设备;最后,目前虚拟机的运行参数(内存映射方案,虚拟设备配置,代码入口地址等)是硬编码在代码中的,要有效运行多个虚拟机,需要以配置文件等方式保存这些参数。 10 | 11 | # 任务要求: 12 | 13 | 1.【基础】至少在一个架构上实现:在单个物理核心上并发运行至少2台虚拟机,每台虚拟机1个虚拟CPU。要求实现虚拟机和虚拟CPU相关信息的管理和切换、虚拟CPU的调度、不同虚拟机间虚拟设备的隔离。 14 | 15 | 2.【进阶】支持从配置文件读取虚拟机的运行参数并依此创建虚拟机。 16 | 17 | a.虚拟设备的配置可以采用DTB;其他配置可以自定方案; 18 | 19 | b.配置文件如何传递给arceos? 20 | 21 | 3.【可选】实现2台虚拟机之间的虚拟串口通信; 22 | 23 | 4.【高难、合作】结合多核支持,实现多个虚拟机多个虚拟CPU在多个物理核心上的调度。 24 | 25 | # 任务考核 26 | 27 | 1.现场演示; 28 | 29 | 2.开发文档; 30 | -------------------------------------------------------------------------------- /tasks/real_time_vm.md: -------------------------------------------------------------------------------- 1 | # 实时虚拟机 2 | 3 | ## 任务描述: 4 | 5 | 实时虚拟机是一种虚拟化技术,专门设计用于在实时计算环境中运行。实时计算是指对任务响应时间有极高要求的计算领域,如工业自动化、机器人控制、医疗设备等。实时虚拟机旨在提供可预测性和可靠性,确保虚拟化环境中的应用能够满足实时性需求。具体来说可预测性是指实时虚拟机必须以可预测的方式对任务进行调度和执行。这意味着任务的执行时间、响应时间和延迟是已知的和可控的。开发者和系统管理员必须能够精确地预测系统如何在特定条件下执行。 6 | 7 | 实时虚拟机的实时性包括硬实时性和软实时性。硬实时性表示系统中的任务或事件必须在严格的截止时间内完成。如果未能在规定的时间内执行,将被视为失败。与硬实时性不同,软实时性允许在规定时间内执行任务,但超出时间限制的情况可能导致性能下降。这种情况下,性能是关键,但不是绝对的。 8 | 9 | ## 任务要求: 10 | 11 | 在实时虚拟机这个选题下,可以分为两个任务:一是设计并实现一种针对实时虚拟机实时性测试的方法。二是基于ArceOS中Hypervisor进行改造,使得Hypervisor能够支持实时虚拟机。 12 | 13 | ## 任务考核: 14 | 15 | 1. 详细的开发和设计文档,代码有清晰的注释。 16 | 17 | 2. 现场演示:演示各个功能的展示效果,打印出相关信息。 18 | 19 | 3. 测试:需要测例的功能应提供不少于两个测例。 20 | -------------------------------------------------------------------------------- /tasks/vm_migration.md: -------------------------------------------------------------------------------- 1 | # 虚拟机迁移 2 | 3 | ## 任务描述: 4 | 5 | 虚拟机迁移是指将虚拟机从一个平台迁移到另一个平台,可以依照停机时间分为静态迁移(冷迁移)和动态迁移(热迁移)。静态迁移虚拟机会被挂起或关机,之后通过网络服务将非运行状态的虚拟机信息发送到新的平台,由新平台完成虚拟机调度或启动流程。虚拟机动态迁移能够将运行状态的虚拟机从源平台迁移至新的平台,迁移期间虚拟机会持续产生状态变动,动态迁移程序需要有能力跟踪虚拟机的状态变化,并在有限的停机时延内,完成虚拟机状态信息的最终更新。 6 | 7 | 虚拟机迁移相关的功能可以分为多个子任务:虚拟机快照、虚拟机克隆、虚拟机静态迁移、虚拟机动态迁移。其中虚拟机快照应完成虚拟机状态冻结与保存、快照文件生成与管理、虚拟机状态还原等功能。虚拟机克隆可以基于快照来实现虚拟机的克隆,也可以通过创建虚拟机完全副本来实现虚拟机完全克隆。虚拟机静态迁移可以视为虚拟机克隆的功能扩展,后者通常是在同一机器上创建虚拟机副本,而前者更多是要将虚拟机从一个机器移动到另一个机器,并且前者可能还需要传输虚拟机的运行状态。相较于虚拟机静态迁移,动态迁移的虚拟机处于运行过程中,会持续产生新的状态从而修改内存数据,因此动态迁移需要追踪内存数据的变动,并在迁移的不同阶段将脏页(被修改了的内存页)传输到迁移的目的机器中,直至内存脏页数量收敛至设定的规模。 8 | 9 | ## 任务要求: 10 | 11 | 1. 【基础】基于ArceOS,完成其中一个架构(aarch64, x86, riscv)Hypervisor支持虚拟机快照、虚拟机克隆,要求代码可扩展性强(如果有余力,鼓励完成多个架构的实现)。 12 | 13 | 2. 【进阶】在完成【基础】要求功能的基础上,进一步让Hypervisor支持静态迁移。 14 | 15 | 3. 【挑战】在完成【进阶】要求功能的基础上,进一步让Hypervisor支持动态迁移。 16 | 17 | ## 任务考核: 18 | 19 | 1. 详细的开发和设计文档,代码有清晰的注释。 20 | 21 | 2. 现场演示:演示各个功能的展示效果,打印出相关信息。 22 | 23 | 3. 测试:需要测例的功能应提供不少于两个测例。 24 | 25 | --------------------------------------------------------------------------------