ARM64 虚拟化实践教程:从手写 VMM 到 KVM/QEMU 源码精读。
面向具备 Linux 与 C 语言基础、希望深入 ARM64 虚拟化底层的开发者。 教程以「先运行、再溯源」为原则组织:每个概念都对应一个可复现的实验, 每个实验产生的数据都能在源码中找到出处。
| 阶段 | 主题 | 产出 |
|---|---|---|
| 一 | 搭建可运行 KVM 的 ARM64 环境(嵌套虚拟化) | docs/01-环境搭建.md |
| 二 | 用 QEMU 启动虚拟机并观察其硬件构成 | docs/02-QEMU启动虚拟机.md |
| 三 | 基于 KVM ioctl API 手写约 200 行 VMM | docs/03-手写VMM.md · labs/lab02-tiny-vmm/ |
| 四 | 精读 KVM 内核源码主干 | docs/04-KVM源码.md · notes/ |
| 五 | 建立 QEMU 用户态侧的系统性认知 | docs/05-QEMU源码.md |
| 六 | 全景串联:完整描述一次虚拟机启动 | notes/day6-终极作业-一次虚拟机启动的全过程.md |
零基础读者建议先阅读 docs/00-从零理解虚拟化.md,
其中系统讲解了虚拟化原理、ARM 特权级模型与三层嵌套的成因。
git clone https://github.com/<user>/kvm-arm-lab.git
cd kvm-arm-lab
bash scripts/check-env.sh # 环境自检:/dev/kvm、CPU 特性、依赖工具环境就绪后,从阶段三的实验入手最能建立直观认识:
cd labs/lab02-tiny-vmm && make && ./tiny_vmm该程序在约 200 行内完成 VM 创建、内存注册、vCPU 初始化与主循环, 运行后输出客户机通过 MMIO 打印的字符,并统计陷出次数。
文档中的实测数据均来自以下环境。不同硬件与内核版本下数值会有差异, 但数量级与相互关系保持一致:
| 项目 | 配置 |
|---|---|
| 宿主机 | Apple Silicon M3 / macOS 15 |
| L1 虚拟机 | Lima + vz + 嵌套虚拟化,Ubuntu 24.04 |
| L2 客户机 | qemu-system-aarch64 -machine virt -accel kvm |
| 内核源码 | Linux v6.8(arch/arm64/kvm/) |
| QEMU 源码 | master 分支 |
嵌套虚拟化依赖 ARMv8.4 的 FEAT_NV2,需要 M3 及以上芯片与 macOS 15+。 M1/M2 无法运行 L2,但阶段一至阶段五的源码阅读部分不受影响。
完整路线约 6 周,每周 10–15 小时。各阶段可独立阅读, 但阶段三的手写 VMM 是理解后续源码的基础,建议不要跳过。
在执行任何命令之前,先建立以下三条认知,后续所有源码分析都是它们的展开:
-
客户机的"物理内存",就是 VMM 进程里一段普通的
mmap匿名内存。 GPA → HVA 的对应关系由用户态通过KVM_SET_USER_MEMORY_REGION告诉内核; 真正让硬件自动完成 GPA → HPA 翻译的是 ARM 的 stage-2 页表,由 KVM 按需填充。 -
"运行虚拟机"就是一个线程在
ioctl(KVM_RUN)里进进出出。 进去 = 硬件eret到客户机;出来 = 客户机干了件自己搞不定的事,陷入 EL2。 整台虚拟机的性能,本质就是「陷出次数 × 每次陷出的代价」。 -
KVM 只做"必须在内核里做"的事,其余全丢给 QEMU。 CPU 指令、内存翻译、中断控制器(GIC)、定时器 → KVM 内核态。 磁盘、网卡、串口、PCI 拓扑、固件加载 → QEMU 用户态。 两者的边界,就是
KVM_RUN返回时kvm_run->exit_reason那个枚举值。
| 维度 | x86 (VMX) | ARM64 |
|---|---|---|
| 特权级 | root/non-root × ring0-3 | EL0/EL1/EL2/EL3,Hypervisor 跑在 EL2 |
| 进出客户机 | vmlaunch/vmresume → #VMEXIT |
eret 进入 / 异常陷入 EL2 |
| 二级页表 | EPT | stage-2 translation(stage-1 由客户机自己管) |
| 陷出信息 | VMCS exit reason 字段 | ESR_EL2 异常综合寄存器(EC 字段是分类关键) |
| 中断控制器 | APIC/APICv | GIC(v2/v3/v4),vGIC 在内核里模拟 |
| 主机内核位置 | 始终 ring0 | 分 VHE(内核直接跑 EL2,现代主流)和 nVHE(EL1+EL2 stub) |
| 关机/启动协议 | ACPI | PSCI(hvc 调用) |
VHE 特别重要:ARMv8.1 引入 VHE 后,Linux 内核直接运行在 EL2, 避免了 nVHE 下每次陷出都要在 EL1/EL2 之间搬运寄存器的巨大开销。 现代 Apple Silicon 上运行的几乎均为 VHE 模式,读源码时优先读
hyp/vhe/而不是hyp/nvhe/。
qemu-system-aarch64 在 macOS 上能用 HVF 加速,但那是苹果的 Hypervisor.framework,
不是 KVM,无法访问 /dev/kvm,也无法在 macOS 上读 KVM 源码做实验。
所以路线是:macOS → 一台 Linux VM(L1)→ 在里面用 KVM 跑 L2。
这需要嵌套虚拟化,而这正好是 M3+ / macOS 15+ 才有的能力(依赖 ARMv8.4 的 FEAT_NV2)。
# 在 macOS 上
brew install lima
# 关键:开启嵌套虚拟化 + 使用 Apple Virtualization 框架(vz)
limactl create --name=kvmlab \
--vm-type=vz \
--nested-virt \
--cpus=4 --memory=8 --disk=60 \
template://ubuntu-24.04
limactl start kvmlab
limactl shell kvmlab老版本 Lima 没有
--nested-virt这个别名,等价写法是:limactl create --set '.nestedVirtualization = true' --vm-type=vz ...
进去之后第一件事就是验证:
ls -l /dev/kvm # 必须存在
sudo apt install cpu-checker && kvm-ok看到 /dev/kvm exists 才算过关。看不到就先别往下走。
UTM 在使用 Apple Virtualization 后端时,M3+ / macOS 15 上默认开启嵌套虚拟化, 适合喜欢图形界面的人。注意必须选 Apple Virtualization 后端,不能选 QEMU 后端。
若本地环境始终无法就绪(或需要一个不受 macOS 更新影响的稳定环境), 用一台 ARM 裸金属/云主机,KVM 是原生的,没有嵌套的任何坑:
- Oracle Cloud Ampere A1:有长期免费额度,4 核 24G,性价比最高
- AWS Graviton:
c7g.metal等裸金属实例可直接用 KVM - 阿里云倚天 / 华为鲲鹏
- 物理开发板:树莓派 4/5(内存小,但 KVM 完全可用)、RK3588
建议:本地 Lima 做日常代码阅读和小实验,云主机做内核编译这类重活。
sudo apt update
sudo apt install -y build-essential git bpftrace trace-cmd \
qemu-system-arm qemu-utils cloud-image-utils \
gdb-multiarch libncurses-dev flex bison libssl-dev bc \
libglib2.0-dev libpixman-1-dev ninja-build python3-venv跑一下自检脚本确认:
bash scripts/check-env.sh产出:docs/01-环境搭建.md —— 记录环境选型、遇到的问题及其解决过程。
x86 有 BIOS/CSM 那套历史包袱,从实模式 0xFFFF0 开始。
ARM 没有 BIOS,virt 机型上有两条路:
- 直接内核启动:QEMU 把 kernel Image + DTB 直接塞进内存,PC 指向内核入口。适合学习。
- UEFI 启动:加载 EDK2(
QEMU_EFI.fd)到 pflash,走标准 UEFI 流程。适合跑发行版。
先玩第一种,因为它没有中间层,最容易看清"谁把什么放到了哪个地址"。
# 下载一个 ARM64 内核和 initrd(也可以自己编译,见阶段五)
# Ubuntu cloud image 的 kernel/initrd 就够用
qemu-system-aarch64 \
-machine virt,gic-version=host \
-cpu host -accel kvm \
-smp 2 -m 2G -nographic \
-kernel vmlinuz \
-initrd initrd.img \
-append "console=ttyAMA0 rdinit=/bin/sh"逐个参数问自己为什么(这是本阶段的核心作业):
-machine virt:QEMU 虚构的机型,没有任何真实硬件对应。为什么 ARM 需要它而 x86 用pc?gic-version=host:让 vGIC 版本匹配宿主机。改成gic-version=2在 GICv3 宿主机上会怎样?-cpu host:把宿主机 CPU 特性直接透传。用 KVM 时能不能写-cpu cortex-a57?为什么不行?-accel kvm:不加会怎样?(会退化成 TCG 纯软件模拟,慢 10–100 倍,但也能跑)console=ttyAMA0:为什么是 ttyAMA0 而不是 ttyS0?(PL011 vs 16550)
同一个镜像,分别用 -accel tcg 和 -accel kvm 启动,用 time 量启动耗时。
time qemu-system-aarch64 -machine virt -cpu max -accel tcg ...
time qemu-system-aarch64 -machine virt -cpu host -accel kvm ...这是理解"硬件辅助虚拟化到底帮了什么"最直观的一课。 TCG 是逐条翻译客户机指令;KVM 是让客户机指令直接在物理 CPU 上跑,只在必要时陷出。
在客户机里:
cat /proc/cpuinfo # CPU 型号是什么?和宿主机一样吗?
cat /proc/interrupts # 中断控制器是 GICv2 还是 GICv3?
ls /sys/firmware/devicetree/base/ # 设备树 —— QEMU 生成的
lspci # virtio 设备
dmesg | grep -i virtio用 QEMU monitor(Ctrl-A c)从宿主机视角看:
(qemu) info qtree # 完整设备树
(qemu) info mtree # 内存映射 —— 重点看 GPA 空间怎么划分的
(qemu) info registers # vCPU 寄存器
对比 info mtree 的输出和客户机里的 /proc/iomem,可以看到同一份内存布局的两个视角。
产出:docs/02-QEMU启动虚拟机.md —— 完整命令、参数含义、TCG/KVM 性能对比数据、info mtree 输出分析。
这是整个学习路线里信息密度最高的一步。 读一万行 QEMU 源码,不如自己写 200 行 VMM 跑起来一次。 做完这一步,再回头阅读 QEMU 的
accel/kvm/kvm-all.c,会发现"啊,它也就是在做这些事"。
代码已经准备好并验证过可以干净编译(labs/lab02-tiny-vmm/):
tiny_vmm.c—— VMM 主体,带详细中文注释guest.S—— 运行在虚拟机里的裸机固件(打印字符串然后关机)Makefile
cd labs/lab02-tiny-vmm
make
./tiny_vmm guest.bin预期输出:
[vmm] 1. /dev/kvm 已打开, API version = 12
[vmm] 2. 虚拟机已创建, vm_fd = 4
[vmm] 3. 已加载 68 字节固件到 GPA 0x40000000
[vmm] 4. vCPU 已创建, target = 5
[vmm] 5. 进入客户机...
--------- guest output ---------
Hello from the guest!
--------- guest output ---------
[vmm] 客户机请求关机 (type=2), 共发生 24 次用户态退出
那句 Hello from the guest! 是这样出现的:客户机执行 strb w3, [x1] 往
0x09000000 写一个字节 → 该地址没有 stage-2 映射 → 硬件产生 data abort 陷入 EL2 →
KVM 判定为 MMIO、自己处理不了 → KVM_RUN 返回用户态 → VMM 执行 putchar →
再次 KVM_RUN 继续。每一个字符都是一次完整的 VM Exit 往返。
| 步骤 | ioctl | 内核入口 | 干了什么 |
|---|---|---|---|
| 1 | open("/dev/kvm") |
kvm_chardev_ops |
拿到系统级句柄 |
| 2 | KVM_CREATE_VM |
kvm_create_vm() → kvm_arch_init_vm() |
分配 stage-2 页表、VMID |
| 3 | KVM_SET_USER_MEMORY_REGION |
kvm_set_memory_region() |
建立 GPA→HVA 的 memslot |
| 4 | KVM_CREATE_VCPU + KVM_ARM_VCPU_INIT |
kvm_arch_vcpu_create() / kvm_vcpu_set_target() |
造 vCPU、定 CPU 型号和特性 |
| 5 | KVM_RUN |
kvm_arch_vcpu_ioctl_run() |
进入客户机,直到陷出 |
arm64 独有的坑:
KVM_CREATE_VCPU之后必须调KVM_ARM_VCPU_INIT, 否则KVM_RUN直接返回ENOEXEC。x86 没有这一步。 用KVM_ARM_PREFERRED_TARGET由内核返回应使用的 target,别硬编码。
- 改字符串,重新
make,确认改动生效 —— 建立"我真的在控制这台虚拟机"的信心。 - 统计退出次数:代码里已有
exits计数。字符串多长,退出多少次?关系是什么? - 加一个"寄存器读取":客户机关机后,用
KVM_GET_ONE_REG把x0–x3读出来打印。 - 实现一个新 MMIO 设备:比如在
0x09001000放一个"计数器寄存器",客户机读它返回递增值。 - 故意制造错误:把 PC 设成一个没映射的地址,看
KVM_EXIT_FAIL_ENTRY长什么样。 - 把 PSCI 关掉(去掉
KVM_ARM_VCPU_PSCI_0_2),观察hvc变成什么退出原因。 - 进阶:加载真正的 Linux kernel Image,替代当前的最小固件。
这需要自行构造 DTB、按 ARM64 boot protocol 设置
x0指向 DTB —— 难度陡增,但收获巨大。
# 看 KVM 内核态的所有事件
sudo trace-cmd record -e kvm ./tiny_vmm guest.bin
sudo trace-cmd report | head -50
# 重点关注这几个 tracepoint:
# kvm_entry / kvm_exit —— 每次进出客户机
# kvm_mmio —— MMIO 访问
# kvm_arm_setup_debugkvm_exit 事件里的 ESR_EL2 值是 ARM 虚拟化的核心线索,它的 EC 字段(bit 31:26)
指示陷出原因:0x24 = data abort(MMIO),0x16 = HVC,0x18 = 系统寄存器访问。
产出:docs/03-手写VMM.md —— 五步 API 详解、改造实验、trace 输出分析、ESR_EL2 解码笔记。
📄 详细分日计划见
docs/04-KVM源码.md(4 天路线) 📁 源码阅读笔记(源码精读 + 校订注记)见notes/—— Day 1 架构与核心组件、Day 2KVM_RUN主循环、Day 3 世界切换与eret
# 用当前运行的内核版本,别用 master,避免代码和行为对不上
uname -r
git clone --depth=1 --branch v6.12 \
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git建议配 VSCode + clangd,或者 make ARCH=arm64 tags 用 vim/ctags。
不建议裸看 GitHub 网页,跳转太痛苦。
所有文件都在 arch/arm64/kvm/,核心是 arm.c。
第一遍:跟着 ioctl 走一遍主干
| 对应实现 | 读这里 | 关注 |
|---|---|---|
KVM_CREATE_VM |
arm.c: kvm_arch_init_vm() |
stage-2 页表初始化 kvm_init_stage2_mmu() |
KVM_CREATE_VCPU |
arm.c: kvm_arch_vcpu_create() |
vcpu 结构体初始化 |
KVM_ARM_VCPU_INIT |
arm.c: kvm_arch_vcpu_ioctl_vcpu_init() → reset.c: kvm_reset_vcpu() |
target/features 怎么落到寄存器上 |
KVM_SET_USER_MEMORY_REGION |
mmu.c: kvm_arch_prepare_memory_region() |
memslot 的建立 |
KVM_RUN ★ |
arm.c: kvm_arch_vcpu_ioctl_run() |
整个 KVM 最重要的函数 |
第二遍:精读 kvm_arch_vcpu_ioctl_run() 的主循环
这个函数是一个 while (ret > 0) 循环,每轮做:
kvm_vcpu_load() 加载 vCPU 上下文到物理 CPU
↓
检查是否有 pending signal / 需要调度 / 有中断要注入
↓
kvm_arm_vcpu_enter_exit() ← 进入客户机
↓ (调 __kvm_vcpu_run,最终 __guest_enter,执行 eret)
↓ ......客户机在跑......
↓ (异常陷入 EL2,__guest_exit 保存现场并返回)
↓
handle_exit() ← handle_exit.c,根据 ESR_EL2 分发
↓
返回 > 0 就继续循环;返回 0 则退出到用户态(ioctl 返回)
第三遍:按主题深入
| 主题 | 文件 | 核心函数 |
|---|---|---|
| 世界切换(最硬核) | hyp/vhe/switch.c |
__kvm_vcpu_run_vhe()、__guest_enter(hyp/entry.S) |
| 陷出分发 | handle_exit.c |
handle_exit()、arm_exit_handlers[] 数组 |
| stage-2 缺页 ★ | mmu.c |
kvm_handle_guest_abort() → user_mem_abort() |
| 系统寄存器模拟 | sys_regs.c |
kvm_handle_sys_reg()、sys_reg_descs[] 表 |
| vGIC 中断 | vgic/vgic*.c |
vgic_v3_*、kvm_vgic_inject_irq() |
| 虚拟定时器 | arch_timer.c |
kvm_timer_vcpu_run() |
| PSCI(关机流程经由此处) | psci.c |
kvm_psci_call() |
| 嵌套虚拟化(参考环境使用此机制) | nested.c、emulate-nested.c |
FEAT_NV2 相关 |
hyp/entry.S 里的 __guest_enter 是整个 KVM 的物理奇点:
- 把宿主机寄存器压到
host_ctxt - 把客户机寄存器从
guest_ctxt恢复到物理寄存器 - 执行
eret—— 这条指令之后,CPU 就在跑客户机代码了 - 客户机出事 → 硬件跳到 EL2 异常向量 →
__guest_exit→ 反向做 1、2 步
一个绝妙的视角:从内核角度,客户机是被
__guest_enter"调用"的一个函数; 从客户机角度,是它自己被中断了然后 hypervisor 处理完eret回来。 同一件事的两种叙述,理解这个对偶性,ARM 虚拟化就通了。
静态读代码容易迷路,配合动态追踪效果翻倍:
# 跟踪 kvm_arch_vcpu_ioctl_run 的完整调用图,深度 3 层
sudo trace-cmd record -p function_graph \
-g kvm_arch_vcpu_ioctl_run --max-graph-depth 3 \
./tiny_vmm guest.bin
sudo trace-cmd report | less可直接观察到函数调用顺序,与静态阅读的推断是否一致一目了然。
# 或者用 bpftrace 统计各类陷出的频率
sudo bpftrace -e 'tracepoint:kvm:kvm_exit { @[args->esr_ec] = count(); }'产出:docs/04-KVM源码.md —— 用自己的语言画出 KVM_RUN 的完整流程图,
标注每个函数的文件:行号,附上 ftrace 实际调用链做印证。
📄 详细计划见
docs/05-QEMU源码.md(3 天路线) —— Day 1 职责分界、Day 2 客户机的硬件从哪来、Day 3 为什么虚拟机不慢
定位说明:本阶段面向以 KVM 为主要方向的读者,
所以这一阶段只建立心智模型,不深入源码,也不需要编译 QEMU。
用系统自带的 qemu-system-aarch64 就能做完全部实验。
三天回答三个问题:
-
QEMU 承担什么? 分界线在
handle_exit的返回值:>0= KVM 自己搞定,=0= 交给 QEMU -
客户机的"硬件"从哪来? QEMU 生成设备树塞进客户机内存, 内核启动时解析 —— ARM 没有 x86 那种固定端口约定,必须有人明说
-
为什么虚拟机不慢? 三条 IO 路径,就是性能优化的全部套路:
设备 路径 出内核 出用户态 arch_timer vGIC 硬件注入 ❌ ❌ uart-pl011 MMIO 陷出 ✅ ✅ virtio 共享内存 + irqfd ✅ ❌
三个零编译实验(全部用现象代替读源码):
# ① 看见 vCPU 线程 —— 它就是 while(1){ioctl(KVM_RUN)} 所在
ps -T -p $(pgrep -f qemu-system-aarch64) -o spid,comm,pcpu
# 预期看到 "CPU 0/KVM"、"CPU 1/KVM"
# ② 看见地址空间树 —— 在 QEMU monitor 里(Ctrl-A 松开再按 C 切过去)
(qemu) info mtree
# 和客户机 /proc/iomem 逐行对照,地址完全一致
# ③ 看见设备树 —— 客户机"知道有什么硬件"的原因
qemu-system-aarch64 -machine virt,gic-version=host,dumpdtb=/tmp/virt.dtb \
-cpu host -accel kvm -smp 2 -m 2G -nographic
dtc -I dtb -O dts /tmp/virt.dtb -o /tmp/virt.dts
grep -A 8 "pl011@9000000" /tmp/virt.dts
# reg = 0x9000000 → 对上阶段二的 /proc/iomem
# interrupts SPI 1 → 32+1=33 → 对上阶段二的 /proc/interrupts产出:notes/day6-QEMU宏观理解.md —— 职责分界图、一个字符的完整旅程
(从 printk 到宿主机终端,覆盖三次边界穿越)、三条 IO 路径对照表 + 实测数据。
🔧 以后真需要改 QEMU 时,源码级地图没有丢,在
docs/05-QEMU源码.md的附录里: 四个入口函数、编译参数、gdb 读法,配套bash ~/kvm-arm-lab/scripts/qemu-src-map.sh在本地源码树上生成准确行号。
✅ 已完成 →
notes/day6-终极作业-一次虚拟机启动的全过程.md12 幕全景 + 名词详解 + 8 个自检问答。这份就是整个学习文档的总纲。
把从 qemu-system-aarch64 敲回车到客户机 shell 出现,中间发生的事
从头到尾讲一遍。分两大阶段,分界点是第一次 ioctl(KVM_RUN):
准备期(只有 QEMU,客户机还不存在)
- 解析参数:
virt机型(专为虚拟化设计的假主板)+ KVM 加速 +-cpu host - 创建设备:GIC 必须最早(后面设备要往它上面接中断线)→ 串口 → 时钟 → PCIe → virtio 每个设备做两件独立的事:注册内存区域(QEMU 自己用)+ 写设备树节点(客户机用)
open("/dev/kvm")→KVM_CREATE_VMmmap2 GB →KVM_SET_USER_MEMORY_REGION★ 只注册 RAM,故意不注册设备地址 —— 这正是 MMIO 能工作的原理- 加载内核(按 Image 头的
text_offset,现代内核落在 2 MiB 对齐处 ≈0x40200000, 实测0x40210000;0x40080000是 3.17 以前的老行为)+ 生成 DTB 塞进客户机内存 KVM_CREATE_VCPU→KVM_ARM_VCPU_INIT(ARM 独有)→ 设PC= 内核入口、x0= DTB 地址(ARM64 boot protocol,全部硬件信息的交接点)
运行期(QEMU + KVM + 客户机三方交替)
- 首次
KVM_RUN→__guest_enter→eret→ 内核第一条指令 ★ VHE 下这是全程唯一一次 EL 切换 - 每访问一个新页 → stage-2 缺页 →
user_mem_abort()填页表(几千次,不出用户态) - 初始化 GIC → vGIC 在内核处理,基本不陷出(实测 arch_timer 5333 次全在内核内)
printk→ MMIO → 4 次边界穿越 → 宿主机终端(一个字符一次往返,几微秒)- 探测 virtio → 共享内存 + irqfd,中断绕过用户态
- init 启动 → shell 提示符
三条 IO 路径(性能优化的全部套路:让操作「少出一层」):
| 设备 | 实测 | 出内核 | 出用户态 |
|---|---|---|---|
| arch_timer | 5333 | ❌ | ❌ |
| uart-pl011 | 187 | ✅ | ✅ |
| virtio | 0 | ✅ | ❌ |
- 内存虚拟化深挖:stage-2 页表格式、大页(THP/hugetlbfs)、
内存热插拔、脏页跟踪与热迁移(
KVM_MEM_LOG_DIRTY_PAGES) - 中断虚拟化:GICv3 架构、LPI/ITS、GICv4 直接注入、irqfd/ioeventfd
- 设备直通:VFIO、SMMU(ARM 的 IOMMU)、
vfio-pci - 性能优化:vhost-net/vhost-user、多队列、CPU 绑核、
用
perf kvm stat分析陷出热点 - 安全:pKVM(Protected KVM,Android 在用)、机密计算 CCA/Realm
- 嵌套虚拟化:本参考环境即为此情形,读
arch/arm64/kvm/nested.c和 FEAT_NV2 规范
订阅 kvmarm@lists.linux.dev 和 qemu-devel,看真实的 patch 讨论。
从修文档 typo 开始提第一个 patch,是进入这个领域最好的敲门砖。
规范文档(权威,遇到不确定就查它)
- ARM Architecture Reference Manual for A-profile (ARM DDI 0487) —— D 章讲 EL2 和虚拟化
- ARM Generic Interrupt Controller Architecture Spec (GICv3/v4)
- ARM Power State Coordination Interface (PSCI) —— 关机所遵循的协议
内核文档(源码树里就有)
Documentation/virt/kvm/api.rst★ —— KVM 用户态 API 完整手册,写 VMM 时常备Documentation/virt/kvm/arm/—— ARM 特有部分Documentation/arch/arm64/booting.rst—— ARM64 内核启动协议
书
- 《深入浅出系统虚拟化》/《QEMU/KVM 源码解析与应用》(中文,适合入门)
- Hardware and Software Support for Virtualization(Bugnion 等,理论扎实)
在线
- KVM Forum 历年演讲(YouTube)—— Marc Zyngier(ARM KVM 维护者)的分享必看
- LWN.net 的 KVM/虚拟化专题文章
kvm-arm-lab/
├── README.md / README.en.md
├── docs/ # 学习文档,按阶段编号
│ ├── 00-从零理解虚拟化.md # 零基础原理教程(建议先读)
│ ├── 01-环境搭建.md
│ ├── 02-QEMU启动虚拟机.md
│ ├── 03-手写VMM.md
│ ├── 04-KVM源码.md
│ ├── 05-QEMU源码.md
│ └── _template/ # 学习笔记模板
├── notes/ # 源码精读笔记 + 校订注记
│ ├── day1-架构与核心组件.md
│ ├── day2-KVM_RUN主循环.md
│ ├── day3-世界切换.md
│ ├── day5-stage2缺页与MMIO.md
│ └── day6-终极作业-一次虚拟机启动的全过程.md
├── labs/
│ └── lab02-tiny-vmm/ # 约 200 行的可运行 VMM
│ ├── tiny_vmm.c
│ ├── guest.S
│ └── Makefile
├── scripts/
│ ├── check-env.sh # 环境自检
│ ├── setup-source.sh # 获取内核与 QEMU 源码
│ ├── kvm-src-map.sh # 在本地源码树生成 KVM 阅读地图
│ ├── qemu-src-map.sh # 在本地源码树生成 QEMU 阅读地图
│ ├── run-vm.sh # 启动 L2 客户机(KVM / TCG 对照)
│ ├── fix-kvm-perm.sh # 修复 /dev/kvm 权限
│ └── setup-vscode-mac.sh # 配置 VSCode 远程开发
└── tools/ # 离线分发(自解压安装脚本)
docs/ 与 notes/ 的分工:docs/ 是按阶段推进的学习路径,
说明学什么、如何验证;notes/ 是源码事实的详细记录,用作查阅手册。
笔记末尾的「校订注记」标注了与实际源码不符或仅在 nVHE 下成立的表述。
-
永远"先跑再读"。先让东西动起来,看到输出,再去源码里找"刚才那个输出是从哪行打出来的"。 反过来(先通读源码再动手)极易中途放弃。
-
每个概念都要有一个能亲手复现的实验对应。 说"stage-2 缺页"较为抽象;跑一次
trace-cmd record -e kvm:*看到user_mem_abort被调用了 3000 次,就具体了。 -
文档边学边写,不要攒到最后。每个阶段结束立刻写完对应的
docs/文件, 趁记忆新鲜。写不出来的地方,就是尚未真正理解之处 —— 这是最好的自检。