From f091682ed60c0bfeb921dba6f5f5ac0242f4aa62 Mon Sep 17 00:00:00 2001 From: sts Date: Mon, 9 Sep 2024 14:49:43 +0800 Subject: [PATCH 1/7] Add articles/20240905-stratovirt-riscv-part3 --- articles/20240905-stratovirt-riscv-part3.md | 220 ++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 articles/20240905-stratovirt-riscv-part3.md diff --git a/articles/20240905-stratovirt-riscv-part3.md b/articles/20240905-stratovirt-riscv-part3.md new file mode 100644 index 0000000..87ca488 --- /dev/null +++ b/articles/20240905-stratovirt-riscv-part3.md @@ -0,0 +1,220 @@ +> Author: Sunts
+> Date: 2024/09/05
+> Revisor: Falcon
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Sponsor: PLCT Lab, ISCAS + + +# Stratovirt 的 RISC-V 支持(三):KVM 模型 + +## 前言 + +KVM 模型用于展示 KVM 模块的简单使用。本节借助 KVM API,构建一个最小虚拟机,运行一段汇编指令。本文代码都运行在前文已经构建好的 RISC-V 架构下的 qemu-system-riscv64 以及用它引导的 RISC-V 架构的 Ubuntu 22.04 环境中。 + +## 项目准备 +下载 stratovirt 源码,从官方 edu 分支起点新建分支并切换到新的分支。新建项目。 +```shell +git clone https://gitee.com/openeuler/stratovirt.git +git checkout 0b2c26 +git checkout -b mini_riscv_edu +``` + +## 汇编指令定义 +最小模型给定一段汇编语言让 vCPU 运行,运行结束,vCPU 退出。 + +```rust +// src/main.rs + +fn main() { + let mem_size = 0x1000; + let guest_addr: u64 = 0x80000000; + + let asm_code: &[u8] = &[ + 0x93, 0x02, 0x80, 0x3f, // li t0, 1016 + 0x03, 0xb3, 0x02, 0x00, // ld t1, 0(t0) + ]; +} +``` +汇编代码逻辑为从地址 0x3f8 位置读取一个双字内容,而这个地址为非注册的 RAM 地址,vCPU会退出进而被捕获。 + +## 打开 KVM 模块,创建虚拟机 +打开 KVM 模块需要使用前文移植好的库:kvm-ioctls 和 kvm-bindings。将两个依赖库复制到项目目录下后修改依赖文件,具体依赖文件内容如下。 +```rust +// src/Cargo.toml + +[package] +name = "stratovirt" +version = "0.1.0" +edition = 2021 + +[dependencies] +libc = ">=0.2.39" +kvm-ioctls = { path = "kvm-ioctls" } +kvm-bindings = { path = "kvm-bindings" } +``` +调用 kvm_ioctls::Kvm 的构造函数打开 /dev/kvm 模块,拿到 Kvm 对象,调用其 create_vm 成员函数创建虚拟机,得到虚拟机句柄。 + +```rust +// src/main.rs + +use kvm_ioctls::Kvm; + +fn main() { + let mem_size = 0x1000; + let guest_addr: u64 = 0x80000000; + + let asm_code: &[u8] = &[ + 0x93, 0x02, 0x80, 0x3f, // li t0, 1016 + 0x03, 0xb3, 0x02, 0x00, // ld t1, 0(t0) + ]; + + let kvm = Kvm::new().expect("Failed to open /dev/kvm"); + let vm_fd = kvm.create_vm().expect("Failed to create a vm"); +} +``` + +## 初始化虚拟机内存 +用 libc 库中的 mmap 系统调用在宿主机中申请内存空间同时可以得到内存起始地址的指针。得到该宿主机的虚拟地址之后需要将宿主机的虚拟地址和客户机的物理地址以及地址空间的大小通知给 KVM。其中,映射关系和内存大小位于 kvm_userspace_memory_region 结构体。 +```rust +// src/main.rs + +use kvm_bindings::kvm_userspace_memory_region; +use kvm_ioctls::Kvm; + +fn main() { + ... + let host_addr: *mut u8 = unsafe { + libc::mmap( + std::ptr::null_mut(), + mem_size, + libc::PROT_READ | libc::PROT_WRITE, // 映射的内存可读可写 + libc::MAP_ANONYMOUS | libc::MAP_PRIVATE, + -1, // 不映射文件,不需要fd + 0 // 不映射文件,offset=0 + ) as *mut u8 + }; + let kvm_region = kvm_userspace_memory_region { + slot: 0, + guest_phys_addr: guest_addr, + memory_size: mem_size as u64, + userspace_addr: host_addr as u64, + flags: 0 + }; + unsafe { + vm_fd + .set_user_memory_region(kvm_region) + .expect("Failed to set memory region to KVM") + } +} +``` + +将汇编字节码写入 mmap 分配的虚拟内存中。 +```rust +// src/main.rs + +use kvm_bindings::kvm_userspace_memory_region; +use kvm_ioctls::Kvm; + +fn main() { + ... + unsafe { + let mut slice = std::slice::from_raw_parts_mut(host_addr, mem_size); + slice + .write_all(&asm_code) + .expect("Failed to load asm code to memory"); + } +} +``` + +## 创建虚拟机和 vCPU 并初始化寄存器 + +risc-v 中设置寄存器的值需要通过 VcpuFd 的 set_one_reg 方法,该方法需要传入一个代表某一特定寄存器的 ID 值,ID 值是 KVM 对该寄存器的唯一编码表示。在 risc-v 中,经过宏扩展之后通用寄存器的 ID 值计算方式如下: + +```c +#define RISCV_CORE_REG(name) KVM_REG_RISCV \ + | KVM_REG_RISCV_CORE \ + | KVM_REG_SIZE_U64 \ + | (offsetof(struct kvm_riscv_core, name) / sizeof(unsigned long)) +``` + +`kvm_riscv_core` 结构体具体定义见 Linux 内核文件。 +```c +# 源码目录 arch/riscv/include/uapi/asm/kvm.h +struct kvm_riscv_core { + struct user_regs_struct regs; + unsigned long mode; +}; + +# 源码目录 arch/riscv/include/uapi/asm/ptrace.h +struct user_regs_struct { + unsigned long pc; + unsigned long ra; + ... + unsigned long t6; +}; +``` + +对于 `RISCV_CORE_REG(regs.pc)` 的调用,宏最终扩展为 `KVM_REG_RISCV | KVM_REG_RISCV_CORE | KVM_REG_SIZE_U64 | (offsetof(struct kvm_riscv_core, regs.pc) / 64)`,`pc` 字段位于 `struct user_regs_struct` 首个成员,故偏移为 0。 + +```rust +// src/main.rs + +use kvm_bindings::{ kvm_userspace_memory_region, KVM_REG_RISCV, KVM_REG_RISCV_CORE, KVM_REG_SIZE_U64}; +use kvm_ioctls::Kvm; +use std::io::Write; + +const PC_ID: u64 = KVM_REG_RISCV as u64 + | KVM_REG_RISCV_CORE as u64 + | KVM_REG_SIZE_U64 as u64 + | 0; + +fn main() { + ... + let vcpu_fd = vm_fd.create_vcpu(0).expect("Failed to create vCPU"); + vcpu_fd.set_one_reg(PC_ID, guest_addr); +} +``` + +## 处理 vCPU 退出事件 + +执行汇编指令 `ld t1, 0(t0)` 时会访问地址 0x3f8,该地址未映射,vCPU 会退出,交由 KVM ,KVM 交由 stratovirt 来处理。根据 vCPU 退出事件类型分别处理。这里只简单处理端口读写和 MMIO 的读写。 +```rust +// src/main.rs + +use kvm_ioctls::{ Kvm, VcpuExit }; + +fn main() { + ... + loop { + match vcpu_fd.run().expect("vcpu run failed") { + VcpuExit::IoIn(addr, data) => { + println!("VmExit IO in : addr 0x{:x}, data is {}", addr, data[0]); + break; + } + VcpuExit::IoOut(addr, data) => { + println!("VmExit Out in : addr 0x{:x}, data is {}", addr, data[0]); + break; + } + VcpuExit::MmioRead(addr, _data) => { + println!("VmExit MMIO read: addr 0x{:x}", addr); + break; + } + VcpuExit::MmioWrite(addr, _data) => { + println!("VmExit MMIO write: addr 0x{:x}", addr); + break; + } + r => panic!("Unexpected exit reason: {:?}", r) + } + } +} +``` + +运行 `cargo run` 执行之后看到打印的信息 `VmExit MMIO read: addr 0x3f8` 后 vCPU 正常退出。 + +## 小结 +本文使用 KVM 的 API 通过 vCPU 运行一段汇编语言代码并成功捕获并处理 vCPU 的退出事件。 + +## 参考资料 + +- [Linux KVM 文档](https://docs.kernel.org/virt/kvm/api.html#api-description) +- [Using the KVM API](https://lwn.net/Articles/658511/) \ No newline at end of file -- Gitee From f65d613f0efde3d5211e7109535f6ad3da05bf1e Mon Sep 17 00:00:00 2001 From: sts Date: Mon, 9 Sep 2024 14:50:20 +0800 Subject: [PATCH 2/7] stratovirt-riscv-part3.md: commit correct result of tinycorrect-spaces Signed-off-by: sts --- articles/20240905-stratovirt-riscv-part3.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/articles/20240905-stratovirt-riscv-part3.md b/articles/20240905-stratovirt-riscv-part3.md index 87ca488..47da975 100644 --- a/articles/20240905-stratovirt-riscv-part3.md +++ b/articles/20240905-stratovirt-riscv-part3.md @@ -1,10 +1,10 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces]
> Author: Sunts
> Date: 2024/09/05
> Revisor: Falcon
> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
> Sponsor: PLCT Lab, ISCAS - # Stratovirt 的 RISC-V 支持(三):KVM 模型 ## 前言 @@ -139,7 +139,7 @@ risc-v 中设置寄存器的值需要通过 VcpuFd 的 set_one_reg 方法,该 `kvm_riscv_core` 结构体具体定义见 Linux 内核文件。 ```c -# 源码目录 arch/riscv/include/uapi/asm/kvm.h +# 源码目录 arch/riscv/include/uapi/asm/kvm.h struct kvm_riscv_core { struct user_regs_struct regs; unsigned long mode; @@ -163,7 +163,7 @@ use kvm_bindings::{ kvm_userspace_memory_region, KVM_REG_RISCV, KVM_REG_RISCV_CO use kvm_ioctls::Kvm; use std::io::Write; -const PC_ID: u64 = KVM_REG_RISCV as u64 +const PC_ID: u64 = KVM_REG_RISCV as u64 | KVM_REG_RISCV_CORE as u64 | KVM_REG_SIZE_U64 as u64 | 0; @@ -177,7 +177,7 @@ fn main() { ## 处理 vCPU 退出事件 -执行汇编指令 `ld t1, 0(t0)` 时会访问地址 0x3f8,该地址未映射,vCPU 会退出,交由 KVM ,KVM 交由 stratovirt 来处理。根据 vCPU 退出事件类型分别处理。这里只简单处理端口读写和 MMIO 的读写。 +执行汇编指令 `ld t1, 0(t0)` 时会访问地址 0x3f8,该地址未映射,vCPU 会退出,交由 KVM,KVM 交由 stratovirt 来处理。根据 vCPU 退出事件类型分别处理。这里只简单处理端口读写和 MMIO 的读写。 ```rust // src/main.rs @@ -217,4 +217,4 @@ fn main() { ## 参考资料 - [Linux KVM 文档](https://docs.kernel.org/virt/kvm/api.html#api-description) -- [Using the KVM API](https://lwn.net/Articles/658511/) \ No newline at end of file +- [Using the KVM API](https://lwn.net/Articles/658511/) -- Gitee From c79703d875ae7dacc8902a38bc3f652f0bfb6028 Mon Sep 17 00:00:00 2001 From: sts Date: Mon, 9 Sep 2024 14:50:37 +0800 Subject: [PATCH 3/7] stratovirt-riscv-part3.md: commit correct result of tinycorrect-toc Signed-off-by: sts --- articles/20240905-stratovirt-riscv-part3.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/articles/20240905-stratovirt-riscv-part3.md b/articles/20240905-stratovirt-riscv-part3.md index 47da975..cc311e6 100644 --- a/articles/20240905-stratovirt-riscv-part3.md +++ b/articles/20240905-stratovirt-riscv-part3.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces toc]
> Author: Sunts
> Date: 2024/09/05
> Revisor: Falcon
@@ -12,6 +12,7 @@ KVM 模型用于展示 KVM 模块的简单使用。本节借助 KVM API,构建一个最小虚拟机,运行一段汇编指令。本文代码都运行在前文已经构建好的 RISC-V 架构下的 qemu-system-riscv64 以及用它引导的 RISC-V 架构的 Ubuntu 22.04 环境中。 ## 项目准备 + 下载 stratovirt 源码,从官方 edu 分支起点新建分支并切换到新的分支。新建项目。 ```shell git clone https://gitee.com/openeuler/stratovirt.git @@ -20,6 +21,7 @@ git checkout -b mini_riscv_edu ``` ## 汇编指令定义 + 最小模型给定一段汇编语言让 vCPU 运行,运行结束,vCPU 退出。 ```rust @@ -38,6 +40,7 @@ fn main() { 汇编代码逻辑为从地址 0x3f8 位置读取一个双字内容,而这个地址为非注册的 RAM 地址,vCPU会退出进而被捕获。 ## 打开 KVM 模块,创建虚拟机 + 打开 KVM 模块需要使用前文移植好的库:kvm-ioctls 和 kvm-bindings。将两个依赖库复制到项目目录下后修改依赖文件,具体依赖文件内容如下。 ```rust // src/Cargo.toml @@ -74,6 +77,7 @@ fn main() { ``` ## 初始化虚拟机内存 + 用 libc 库中的 mmap 系统调用在宿主机中申请内存空间同时可以得到内存起始地址的指针。得到该宿主机的虚拟地址之后需要将宿主机的虚拟地址和客户机的物理地址以及地址空间的大小通知给 KVM。其中,映射关系和内存大小位于 kvm_userspace_memory_region 结构体。 ```rust // src/main.rs @@ -212,6 +216,7 @@ fn main() { 运行 `cargo run` 执行之后看到打印的信息 `VmExit MMIO read: addr 0x3f8` 后 vCPU 正常退出。 ## 小结 + 本文使用 KVM 的 API 通过 vCPU 运行一段汇编语言代码并成功捕获并处理 vCPU 的退出事件。 ## 参考资料 -- Gitee From 04082e5b4ee6dc43cc0712a7ffff48996faed8e9 Mon Sep 17 00:00:00 2001 From: sts Date: Mon, 9 Sep 2024 14:50:47 +0800 Subject: [PATCH 4/7] stratovirt-riscv-part3.md: commit correct result of tinycorrect-codeblock Signed-off-by: sts --- articles/20240905-stratovirt-riscv-part3.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/articles/20240905-stratovirt-riscv-part3.md b/articles/20240905-stratovirt-riscv-part3.md index cc311e6..dc7970f 100644 --- a/articles/20240905-stratovirt-riscv-part3.md +++ b/articles/20240905-stratovirt-riscv-part3.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces toc]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces toc codeblock]
> Author: Sunts
> Date: 2024/09/05
> Revisor: Falcon
@@ -14,6 +14,7 @@ KVM 模型用于展示 KVM 模块的简单使用。本节借助 KVM API,构建 ## 项目准备 下载 stratovirt 源码,从官方 edu 分支起点新建分支并切换到新的分支。新建项目。 + ```shell git clone https://gitee.com/openeuler/stratovirt.git git checkout 0b2c26 @@ -37,11 +38,13 @@ fn main() { ]; } ``` + 汇编代码逻辑为从地址 0x3f8 位置读取一个双字内容,而这个地址为非注册的 RAM 地址,vCPU会退出进而被捕获。 ## 打开 KVM 模块,创建虚拟机 打开 KVM 模块需要使用前文移植好的库:kvm-ioctls 和 kvm-bindings。将两个依赖库复制到项目目录下后修改依赖文件,具体依赖文件内容如下。 + ```rust // src/Cargo.toml @@ -55,6 +58,7 @@ libc = ">=0.2.39" kvm-ioctls = { path = "kvm-ioctls" } kvm-bindings = { path = "kvm-bindings" } ``` + 调用 kvm_ioctls::Kvm 的构造函数打开 /dev/kvm 模块,拿到 Kvm 对象,调用其 create_vm 成员函数创建虚拟机,得到虚拟机句柄。 ```rust @@ -79,6 +83,7 @@ fn main() { ## 初始化虚拟机内存 用 libc 库中的 mmap 系统调用在宿主机中申请内存空间同时可以得到内存起始地址的指针。得到该宿主机的虚拟地址之后需要将宿主机的虚拟地址和客户机的物理地址以及地址空间的大小通知给 KVM。其中,映射关系和内存大小位于 kvm_userspace_memory_region 结构体。 + ```rust // src/main.rs @@ -113,6 +118,7 @@ fn main() { ``` 将汇编字节码写入 mmap 分配的虚拟内存中。 + ```rust // src/main.rs @@ -142,6 +148,7 @@ risc-v 中设置寄存器的值需要通过 VcpuFd 的 set_one_reg 方法,该 ``` `kvm_riscv_core` 结构体具体定义见 Linux 内核文件。 + ```c # 源码目录 arch/riscv/include/uapi/asm/kvm.h struct kvm_riscv_core { @@ -182,6 +189,7 @@ fn main() { ## 处理 vCPU 退出事件 执行汇编指令 `ld t1, 0(t0)` 时会访问地址 0x3f8,该地址未映射,vCPU 会退出,交由 KVM,KVM 交由 stratovirt 来处理。根据 vCPU 退出事件类型分别处理。这里只简单处理端口读写和 MMIO 的读写。 + ```rust // src/main.rs -- Gitee From 8ce51262c77c2aecc0dedb4685be4849fb79ea3d Mon Sep 17 00:00:00 2001 From: sts Date: Mon, 9 Sep 2024 14:51:07 +0800 Subject: [PATCH 5/7] stratovirt-riscv-part3.md: commit correct result of tinycorrect-urls Signed-off-by: sts --- articles/20240905-stratovirt-riscv-part3.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/articles/20240905-stratovirt-riscv-part3.md b/articles/20240905-stratovirt-riscv-part3.md index dc7970f..b063c2f 100644 --- a/articles/20240905-stratovirt-riscv-part3.md +++ b/articles/20240905-stratovirt-riscv-part3.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces toc codeblock]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces toc codeblock urls]
> Author: Sunts
> Date: 2024/09/05
> Revisor: Falcon
@@ -229,5 +229,8 @@ fn main() { ## 参考资料 -- [Linux KVM 文档](https://docs.kernel.org/virt/kvm/api.html#api-description) -- [Using the KVM API](https://lwn.net/Articles/658511/) +- [Linux KVM 文档][001] +- [Using the KVM API][002] + +[001]: https://docs.kernel.org/virt/kvm/api.html#api-description +[002]: https://lwn.net/Articles/658511/ -- Gitee From 59b3e0bf6bb683480242f19426a7026769b7e17a Mon Sep 17 00:00:00 2001 From: sts Date: Mon, 9 Sep 2024 14:51:29 +0800 Subject: [PATCH 6/7] stratovirt-riscv-part3.md: commit correct result of tinycorrect-pangu Signed-off-by: sts --- articles/20240905-stratovirt-riscv-part3.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/articles/20240905-stratovirt-riscv-part3.md b/articles/20240905-stratovirt-riscv-part3.md index b063c2f..dc3ae59 100644 --- a/articles/20240905-stratovirt-riscv-part3.md +++ b/articles/20240905-stratovirt-riscv-part3.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces toc codeblock urls]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [spaces toc codeblock urls pangu]
> Author: Sunts
> Date: 2024/09/05
> Revisor: Falcon
@@ -39,7 +39,7 @@ fn main() { } ``` -汇编代码逻辑为从地址 0x3f8 位置读取一个双字内容,而这个地址为非注册的 RAM 地址,vCPU会退出进而被捕获。 +汇编代码逻辑为从地址 0x3f8 位置读取一个双字内容,而这个地址为非注册的 RAM 地址,vCPU 会退出进而被捕获。 ## 打开 KVM 模块,创建虚拟机 @@ -98,7 +98,7 @@ fn main() { mem_size, libc::PROT_READ | libc::PROT_WRITE, // 映射的内存可读可写 libc::MAP_ANONYMOUS | libc::MAP_PRIVATE, - -1, // 不映射文件,不需要fd + -1, // 不映射文件,不需要 fd 0 // 不映射文件,offset=0 ) as *mut u8 }; -- Gitee From 771059d63947f13bf0542cab7771509a0a681d80 Mon Sep 17 00:00:00 2001 From: sts Date: Thu, 24 Oct 2024 16:24:57 +0800 Subject: [PATCH 7/7] Correct articles/20240905-stratovirt-riscv-part3 --- articles/20240905-stratovirt-riscv-part3.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/articles/20240905-stratovirt-riscv-part3.md b/articles/20240905-stratovirt-riscv-part3.md index dc3ae59..08be7dd 100644 --- a/articles/20240905-stratovirt-riscv-part3.md +++ b/articles/20240905-stratovirt-riscv-part3.md @@ -33,13 +33,13 @@ fn main() { let guest_addr: u64 = 0x80000000; let asm_code: &[u8] = &[ - 0x93, 0x02, 0x80, 0x3f, // li t0, 1016 + 0x93, 0x02, 0x80, 0x3f, // li t0, 0x3f8 0x03, 0xb3, 0x02, 0x00, // ld t1, 0(t0) ]; } ``` -汇编代码逻辑为从地址 0x3f8 位置读取一个双字内容,而这个地址为非注册的 RAM 地址,vCPU 会退出进而被捕获。 +汇编代码逻辑为从地址 0x3f8 位置读取一个双字内容,而这个地址为非注册的 RAM 地址,vCPU 会退出进而被 host 捕获。 ## 打开 KVM 模块,创建虚拟机 @@ -82,7 +82,7 @@ fn main() { ## 初始化虚拟机内存 -用 libc 库中的 mmap 系统调用在宿主机中申请内存空间同时可以得到内存起始地址的指针。得到该宿主机的虚拟地址之后需要将宿主机的虚拟地址和客户机的物理地址以及地址空间的大小通知给 KVM。其中,映射关系和内存大小位于 kvm_userspace_memory_region 结构体。 +用 libc 库中的 mmap 系统调用在宿主机中申请内存空间同时可以得到内存起始地址的指针。得到该宿主机的虚拟地址之后需要将宿主机的虚拟地址和客户机的物理地址以及地址空间的大小通知给 KVM。其中,映射关系和内存大小通过 kvm_userspace_memory_region 结构体传递。 ```rust // src/main.rs @@ -221,7 +221,7 @@ fn main() { } ``` -运行 `cargo run` 执行之后看到打印的信息 `VmExit MMIO read: addr 0x3f8` 后 vCPU 正常退出。 +运行 `cargo run` 执行之后 vCPU 退出,进而被 host 捕获,打印信息 `VmExit MMIO read: addr 0x3f8` 后程序退出。 ## 小结 -- Gitee