先建立系统心智模型
AxVisor 是运行在最高控制层的 Hypervisor。它不直接“运行 Linux 程序”,而是创建并运行 Linux 客体;Linux 完成自己的内核启动后,再由用户态 init 启动 App A、App B。RTOS 则作为另一个客体,拥有独立入口、内存、定时器和任务调度器。
必须区分的四个对象
| 对象 | 含义 | 典型问题 |
|---|---|---|
| Hypervisor | 控制真实 CPU、内存、中断与客体生命周期 | VM 如何创建?何时进入客体? |
| VM | 一组虚拟硬件与资源的容器 | 内存和设备如何划分? |
| vCPU | 客体所看到的虚拟处理器及其上下文 | 寄存器如何保存、恢复和调度? |
| Guest workload | Linux、RTOS,以及它们内部的任务或程序 | 镜像入口和启动参数是否正确? |
ARM 虚拟化基础:为什么需要 EL2
AArch64 使用异常级控制权限。普通应用通常运行在 EL0,客体操作系统运行在 EL1,Hypervisor 运行在 EL2。EL2 配置哪些 EL1 操作可以直接执行、哪些操作必须陷入 Hypervisor。
关键 EL2 寄存器
| 寄存器 | 作用 | 调试价值 |
|---|---|---|
HCR_EL2 | 虚拟化总开关与陷入策略,如是否启用 Stage-2 | 客体异常行为不符合预期时首先检查 |
VTTBR_EL2 | 当前 VM 的 Stage-2 页表根和 VMID | 确认当前运行的是哪个地址空间 |
VTCR_EL2 | Stage-2 地址宽度、粒度和页表级数 | 判断页表解释方式是否正确 |
ESR_EL2 | 异常类别 EC 和具体原因 ISS | 定位系统寄存器、MMIO、指令等 Exit |
ELR_EL2 | 异常返回后继续执行的客体 PC | 确认发生异常的指令地址 |
FAR_EL2 / HPFAR_EL2 | 故障虚拟地址和中间物理地址信息 | 定位 Stage-2 地址故障 |
ARM 文档通常称其为“从低异常级同步异常进入 EL2”。工程中常用 VM Exit 泛指客体因敏感操作、地址故障、中断或主动调用而把控制权交回 Hypervisor。
从上电到两个客体运行
- 01固件交接
CPU 从复位入口执行。固件准备内存和启动参数,将主核置于合适异常级,再跳转到 AxVisor 镜像入口。
- 02早期运行环境
入口汇编建立栈、清零 BSS、保存启动参数;随后初始化页表、MMU、堆、串口和 per-CPU 数据。
- 03解析 VM 配置
读取客体类型、CPU、内存、镜像地址、入口、DTB、设备和中断分配。
- 04装载客体镜像
把 Linux Image、initramfs、DTB 或 RTOS 镜像复制/映射到约定的 Guest Physical Address。
- 05建立 Stage-2
为每个 VM 建立 IPA→PA 映射,并设置读写执行与设备内存属性。
- 06构造 vCPU
设置客体入口 PC、PSTATE、通用寄存器、虚拟定时器和虚拟中断状态。Linux arm64 通常通过 x0 接收 DTB 地址。
- 07首次进入客体
写入当前 VM 的 VTTBR_EL2,恢复 vCPU 上下文并通过异常返回进入 EL1。
- 08Exit 与恢复
客体陷入 EL2 后,读取 ESR_EL2 分类处理,必要时模拟设备并推进 PC,然后恢复客体。
- 09客体内部启动
Linux 挂载根文件系统并执行 init;RTOS 初始化时钟、中断和调度器。最终用户程序和实时任务开始运行。
// 概念性伪代码:真实接口以当前 AxVisor 源码为准
hypervisor_main() {
init_memory_and_percpu();
init_interrupt_controller();
for cfg in vm_configs {
vm = create_vm(cfg);
load_guest_images(vm, cfg);
build_stage2_page_table(vm);
setup_vcpus(vm);
register_vm(vm);
}
schedule_first_vcpu();
}
Stage-2:虚拟机隔离的核心
客体内部仍然可以使用自己的 Stage-1 页表,把虚拟地址 VA 转换为中间物理地址 IPA。硬件随后使用 Hypervisor 建立的 Stage-2 页表,把 IPA 转换为真实物理地址 PA。
Guest OS
AxVisor
建立映射时检查四件事
- 范围:IPA 起点、PA 起点和长度必须按页对齐,且不同 VM 的 PA 区域不可重叠。
- 权限:代码段通常可读可执行,数据段可读写;不应把所有区域长期映射成 RWX。
- 内存类型:普通 RAM 与 UART/GIC 等 Device Memory 必须使用正确属性,否则可能出现乱序或缓存问题。
- TLB:修改活动映射后,需要按架构要求执行屏障与 TLB invalidation。
// 教学用映射示意,不代表 AxVisor 的真实 API
map_guest(vm_linux,
ipa = 0x4000_0000,
pa = 0x9000_0000,
len = 512 * MiB,
attr = NORMAL | READ | WRITE | EXEC);
map_guest(vm_rtos,
ipa = 0x4000_0000,
pa = 0xB000_0000,
len = 64 * MiB,
attr = NORMAL | READ | WRITE | EXEC);
两个客体可以拥有相同 IPA 布局,因为它们使用不同的 VTTBR_EL2/VMID;但最终 PA 区域必须隔离。示例地址仅用于理解,必须根据实际平台内存图修改。
vCPU 上下文与 VM Exit 循环
vCPU 不只是一个线程对象,而是一份“能够恢复客体处理器状态”的完整快照。最少包含通用寄存器、PC/PSTATE、必要的 EL1 系统寄存器、虚拟定时器,以及与虚拟中断控制器相关的状态。
loop {
install_vm_stage2(vcpu.vm);
restore_guest_context(vcpu);
enter_guest(); // ERET
save_guest_context(vcpu); // 从 EL1 陷入 EL2 后继续
syndrome = read(ESR_EL2);
match decode(syndrome) {
MMIO => emulate_or_forward_device(vcpu),
SYSREG => emulate_system_register(vcpu),
HVC => handle_hypercall(vcpu),
STAGE2 => handle_or_report_page_fault(vcpu),
other => inject_or_stop_guest(vcpu, other),
}
}
调试时应同时记录:VM ID、vCPU ID、ESR_EL2、ELR_EL2、FAR_EL2、HPFAR_EL2 和本次处理结果。只打印“guest fault”通常不足以定位问题。
中断和设备为什么最容易卡住
客体需要看到“虚拟硬件”。设备可以完全模拟、半虚拟化、直通或静态分区。学习阶段建议先从串口和虚拟定时器入手,因为日志与调度都依赖它们。
| 方式 | 工作方式 | 特点 |
|---|---|---|
| Trap-and-emulate | 客体 MMIO 访问触发 Stage-2 fault,由 Hypervisor 模拟 | 易控制但 Exit 开销较高 |
| 半虚拟化 | 客体使用约定接口,如 virtio 或 hypercall | 性能与通用性平衡 |
| 设备直通 | 把物理设备及中断直接分配给一个 VM | 性能好,但隔离与 IOMMU 更复杂 |
| 静态分区 | 设备固定归属某个 VM,不动态共享 | 适合混合关键系统 |
虚拟定时器中断路径
物理/虚拟计时器到期 → EL2/GIC 获得事件 → 标记目标 vCPU 的 pending vIRQ → vCPU 运行时注入 → 客体 EL1 中断处理 → 调度器 tick
如果 RTOS 已输出启动日志但任务不切换,优先检查计时器频率、比较值、虚拟 IRQ 号、GIC 使能位以及中断是否注入到正确 vCPU。
ArceOS:组件化内核如何支撑 Hypervisor
ArceOS 采用组件化设计:应用或上层系统按 feature 组合所需能力,而不是固定包含所有内核服务。AxVisor 可以复用其中的平台抽象、运行时、内存、任务与驱动能力,把精力集中在虚拟机管理上。
| 常见组件 | 职责 | 阅读重点 |
|---|---|---|
axhal | 架构与平台硬件抽象 | 入口、页表、中断、定时器、CPU 操作 |
axruntime | 组织系统早期初始化 | 初始化顺序和主入口交接 |
axalloc | 页和堆内存分配 | 可用物理内存从何而来 |
axtask | 任务、调度与同步 | vCPU 是否映射为任务、如何调度 |
axdriver | 设备探测与驱动框架 | UART、块设备、网络等如何初始化 |
axstd | 面向应用的类 std 接口 | 应用入口与底层组件如何连接 |
推荐源码阅读方法
- 从运行命令确定目标架构、平台与启用 feature。
- 找到链接脚本和入口符号,确认镜像装载地址。
- 追踪“第一条串口日志”之前的调用路径。
- 为内存、驱动、调度器初始化分别增加一对 begin/end 日志。
- 最后才深入单个组件内部算法,避免一开始陷入细节。
组件名和入口函数可能调整。遇到文档与源码不一致时,优先使用 rg "no_mangle|entry|rust_main|main"、链接脚本和构建日志重新建立调用链。
AxVisor:从配置到 vCPU 运行
理解 AxVisor 时,不要只按目录浏览。沿一台 VM 的生命周期追踪:配置解析 → VM 对象 → 内存区域 → 镜像装载 → vCPU 创建 → 架构上下文 → 首次运行 → Exit 分发。
建议建立的源码索引
- 系统入口:AxVisor 主入口、主核/次级核初始化、启动第一个 VM 的位置。
- VM 管理:VM ID 分配、配置结构、内存 region、设备与 vCPU 集合。
- 架构层:EL2 初始化、Stage-2 页表、vCPU 寄存器上下文和异常向量。
- 运行循环:进入客体、保存现场、异常解码与处理分发。
- 设备与 IRQ:MMIO 区域注册、虚拟 IRQ 注入、直通设备归属。
第一次改代码应做什么
不要先实现新设备。先为 VM 创建和 Exit 路径增加结构化日志,例如:
[vm-create] vm=0 ipa=0x40000000 pa=0x90000000 size=512M
[vcpu-enter] vm=0 cpu=0 pc=0x40200000 vttbr=...
[vm-exit] vm=0 cpu=0 ec=0x24 elr=... far=... hpfar=...
[vcpu-resume] vm=0 cpu=0 next_pc=...
当你能解释这些日志,就已经建立了后续调试 Linux、RTOS 和设备的骨架。
Linux 与 RTOS 客体的启动差异
通用操作系统启动
- 准备内核 Image、DTB、可选 initramfs
- arm64 通常在 x0 传入 DTB IPA
- 内核建立自己的 Stage-1 页表
- 初始化驱动、挂载 rootfs、执行 init
- init 再启动 App A 与 App B
板级实时系统启动
- 准备 ELF/raw image 与入口地址
- 匹配客体内存图和链接地址
- 初始化 BSS、堆、向量表、时钟和中断
- 创建任务并启动调度器
- 验证 tick 与任务切换持续发生
Linux 必须对齐的启动契约
- 内核镜像装载地址、入口 PC 与 DTB 地址必须落在有效 IPA 映射中。
- DTB 的 memory、CPU、chosen、timer、interrupt-controller、UART 节点必须与虚拟硬件一致。
- 命令行优先开启合适的
earlycon与console=,否则“无输出”不等于内核没运行。 - initramfs 中必须存在可执行的
/init,并包含程序依赖的动态库;学习阶段优先使用静态链接程序。
#!/bin/sh
# initramfs 中的 /init 概念示例
mount -t proc proc /proc
mount -t sysfs sysfs /sys
/bin/app-a &
/bin/app-b &
echo "[PASS] linux apps started"
exec /bin/sh
RTOS 移植检查表
- 链接地址与 AxVisor 配置的 guest load/entry 地址一致。
- 串口基址、中断号、计时器频率与 DTB/板级配置一致。
- 异常向量和栈位于已映射、可写且不与镜像重叠的区域。
- 如果使用 SMP,先完成单核启动,再逐步加入次级核。
多客体资源规划与隔离
混合关键系统首先追求“资源边界清晰”,再考虑动态共享。学习阶段建议使用静态 CPU 和内存分区。
| 资源 | Linux VM 示例 | RTOS VM 示例 | 检查方法 |
|---|---|---|---|
| 物理 CPU | CPU 0–1 | CPU 2 | 打印 vCPU→pCPU 绑定 |
| 物理内存 | 0x9000_0000 / 512 MiB | 0xB000_0000 / 64 MiB | 排序所有 PA region,检查交集 |
| UART | 虚拟/共享控制台 | 独立或日志复用 | 日志增加 VM 前缀 |
| 中断 | Linux 设备 IRQ 集合 | Timer + RTOS 设备 IRQ | 打印 IRQ owner 路由表 |
最终故障实验:让 Linux 客体主动 panic 或访问未映射 IPA,AxVisor 应捕获并停止/重启该 VM;RTOS 的周期任务应继续输出。反向也要测试一次。
分层调试:只追踪最后一个成功边界
| 现象 | 优先检查 | 关键证据 |
|---|---|---|
| AxVisor 完全无输出 | 镜像入口、当前 EL、栈、UART 基址 | PC/SP/CurrentEL 与第一条串口写 |
| 创建 VM 后卡住 | 镜像地址、入口 PC、Stage-2 region | VTTBR_EL2、vCPU 初始寄存器 |
| 立即 Stage-2 fault | IPA 是否映射、权限、内存类型 | ESR_EL2、FAR_EL2、HPFAR_EL2 |
| Linux 无 earlycon | DTB UART、console 参数、MMIO 映射 | ELR_EL2 与 UART access exit |
| Linux 找不到 rootfs | initramfs 地址、bootargs、/init 权限 | 内核日志最后一个 mount/init 错误 |
| RTOS 有日志但不调度 | timer、vIRQ 注入、GIC 使能 | tick 计数与 IRQ pending/ack |
| 双客体互相影响 | PA/IRQ/设备是否重叠 | 资源表与故障注入结果 |
建议的 GDB 观察点
# 符号名需按当前版本替换
b <hypervisor_entry>
b <vm_create>
b <vcpu_first_run>
b <lower_el_sync_handler>
info registers pc sp
p/x $CurrentEL
x/8i $pc
bt
每次只改变一个变量,并保存:启动命令、commit、配置文件、完整串口日志和期望/实际结果。这样才能区分代码问题、配置问题和镜像问题。
六个必须完成的编码实验
异常解码器
读取 ESR_EL2 的 EC/ISS,打印可读 Exit 原因及 ELR/FAR/HPFAR。
验收:能区分 HVC、系统寄存器和数据中止。Stage-2 映射检查器
遍历 VM 内存 region,检查对齐、重叠、权限和设备内存属性。
验收:故意制造重叠时拒绝启动并指出区域。vCPU 生命周期日志
为 create、enter、exit、resume、stop 增加 VM/vCPU 标识。
验收:可从日志还原一次完整运行循环。Linux 双应用 initramfs
编写两个静态程序并由 /init 后台启动,加入健康心跳。
验收:启动后可持续看到两个不同 PID 的日志。RTOS 双任务
创建不同周期的两个任务,记录 tick 和任务切换。
验收:两个任务运行频率符合设计。隔离故障注入
让一个客体访问未映射地址或主动 panic,记录 Hypervisor 处理。
验收:另一个客体连续运行且资源状态正确。