reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
# ACRN:Service VM / pre-launched / post-launched + Device Model
<CORE RULE>
ACRN 里"谁在服务这个 I/O"有三个互斥的答案,**必须先判定是哪一个**:
| 路径 | 谁在服务 |
|---|---|
| **直通(passthrough)** | 硬件直接给 VM,hypervisor 只做映射与中断重映射 |
| **hypervisor 模拟** | hypervisor 自己实现设备 |
| **Service VM 的 Device Model** | 由 Service VM 里的用户态设备模型模拟 |
判定错了,后面所有"这个寄存器为什么这样写"的推理都会跑偏。
拓扑基本形是:
```
Hypervisor
├─ Service VM(直接拥有大量真实硬件,并为 post-launched VM 提供设备模拟/共享)
├─ Pre-launched VM(不依赖 Service VM 启动)
└─ Post-launched User VM(依赖 Service VM 的设备模型)
```
</CORE RULE>
## 一、guest 看到的 BAR 不是物理 BAR
直通时 hypervisor 会做 **虚拟 BAR ↔ 物理 BAR** 之间的映射:在两者之间建立 EPT 映射让 VM 直接访问 MMIO——**但 MSI-X 表所在的那几页必须被 trap**(不建立 EPT 映射),否则无法在虚拟向量与物理向量之间做重映射。
同时还有两件事:
- **VT-d DMA remapping**:把 GPA 翻译成 HPA 供设备 DMA 使用
- **VT-d 中断重映射**:ACRN 出于安全考虑要求它;**如果 VT-d 硬件不支持中断重映射,ACRN 会拒绝启动 VM**
**所以:**
```
Guest BAR0 = 0x80000000
Host 物理设备 BAR = 0xD2000000
```
**完全正常**,不要因为地址不一致就认为抓错了设备或驱动写错了。
## 二、中断:不是每次都有等量的 VM-exit
- 常规情况下虚拟中断由 hypervisor 注入;在支持时使用 **VT-d posted interrupt(PI)** 以获得更好的中断性能
- 因此:
```
guest ISR 明明执行了
hypervisor trace 里却找不到预期的完整中断注入路径
```
**不能直接判 trace 缺失**——先确认该设备/该 VM 是否走了 posted-interrupt 一类旁路。
## 三、ivshmem:dm-land 与 hv-land 是两套机制
ACRN 的共享内存设备(ivshmem)有两种实现,**可以同时存在,但不同实现的 VM 之间无法通过同一机制互通**:
| 实现 | 位置 | 共享内存取自 | 适用 VM |
|---|---|---|---|
| **dm-land** | Service VM 的 Device Model(`acrn-dm`) | Service VM 的内存空间 | **仅 post-launched User VM** |
| **hv-land** | hypervisor 内部 | hypervisor 的内存空间 | pre-launched 与 post-launched 都可 |
- 区分方式看名字前缀:`dm:/`(早期补丁里为 `sos:/`)与 `hv:/`;**前缀不对会导致 ivshmem 初始化失败**
- dm-land 由 `acrn-dm` 启动参数 `-s <slot>,ivshmem,<shm_name>,<shm_size>` 定义;hv-land 在构建期配置(`IVSHMEM_ENABLED` + `IVSHMEM_REGION`,格式 `名字,大小,VM ID 列表`),**最多八个 hv-land 区域**
- 设备是标准虚拟 PCI 设备:**BAR0 中断相关寄存器、BAR1 MSI-X 表、BAR2 共享内存**
- 寄存器语义:`IRQ_MASK(0x0)` 与 `IRQ_STA(0x4)` 保留(不支持传统中断)、`IV_POS(0x8)` 为跨 VM 位置/VM ID(当前为 0)、**`DOORBELL(0xC)` 用于触发对端中断**
- **通知(doorbell)只在 hv-land 支持**:发送方把目标 VM ID 与向量号写入 doorbell 寄存器,由 hypervisor 侧查表并**向目标 VM 注入 MSI**(最多 8 个 MSI-X 向量);**dm-land 没有通知机制**(列为待支持)
- 已知坑:dm-land 下 **guest 若重新编程 BAR2,共享内存会变得不可用**(该场景的 GPA/HPA 重映射未处理)
**所以"两个 VM 各自 ivshmem 都正常、就是互相看不见"时,第一件事是查前缀与实现是否一致**,而不是怀疑设备模型。
## 四、VM 类型决定 MSI-X 的 trap 路径
ACRN 分别定义三类 VM 的 MSI-X 访问路径:
| VM 类型 | 路径 |
|---|---|
| Service VM | hypervisor 的 MMIO handler |
| **Post-launched VM** | **Device Model 的 MMIO handler,再涉及 hypervisor** |
| Pre-launched VM | hypervisor handler |
**所以不要做"所有 MSI-X 写都走同一条 hypervisor 路径"的假设。**
## 五、设备归属会在 VM 之间迁移
- Post-launched VM 启动时,物理设备的所有权**从 Service VM 移交给该 VM**;VM 关闭时再**移回 Service VM**
- 因此"Service VM 里某个 PCI 设备突然消失、过一会又回来"**不一定是热插拔故障**
- **pre-launched VM 由 hypervisor 创建**,不经过 Service VM 的 Device Model——**看不到设备模型进程不能推出启动过程缺了一层**
- passthrough 下 guest BAR 与 host BAR 不同属正常:guest GPA 经 EPT/VT-d 到 HPA,**DMA 还需 VT-d 的 GPA→HPA 转换**
## 决策树
```
拿到 ACRN 目标
├─ 先分清 VM 类型:Service VM / pre-launched / post-launched
│ └─ post-launched 才可能经过 Service VM 的 Device Model
├─ 一个 I/O 行为是谁在服务?
│ ├─ 直通 → 查虚拟 BAR ↔ 物理 BAR 映射、VT-d DMA/中断重映射
│ ├─ hypervisor 模拟 → 查 hypervisor 侧设备实现
│ └─ Device Model → 查 Service VM 里的设备模型
├─ guest BAR 与物理 BAR 不一致
│ └─ 正常(直通本来就做 MMIO remapping);注意 MSI-X 表页必须被 trap
├─ 中断发生但 trace 里没有预期的注入路径
│ └─ 查是否走了 posted interrupt 等旁路,别判 trace 缺失
└─ ivshmem 通不了
├─ 查前缀:dm:/ 还是 hv:/(混用永不通)
├─ pre-launched 只能用 hv-land
├─ dm-land 无 doorbell → 只能共享数据,不能互相通知
└─ dm-land 下 guest 重编程 BAR2 → 共享内存不可用(已知未处理)
```
## 工具与验证
- 拓扑:VM 类型(Service VM / pre-launched / post-launched)与设备归属
- 直通:虚拟 BAR 与物理 BAR 的 EPT 映射、MSI-X 表页是否被 trap、VT-d DMA/中断重映射是否启用
- 共享内存:ivshmem 的名字前缀(`dm:/` / `hv:/`)、BAR 布局、doorbell 寄存器与 MSI-X 向量
- 验证:能同时说清「VM 类型 + 该 I/O 的服务路径(直通/hypervisor/Device Model)+ 地址与中断经过了几层重映射」
## 该平台的坑(汇总)
- **不先分清 pre-launched 与 post-launched**:设备可用性和服务路径完全不同
- **把 guest 的 PCI BAR 当物理 BAR**:直通本来就有虚拟↔物理映射
- **以为 MSI-X 表页也能被 guest 直接访问**:那几页必须被 trap 才能做向量重映射
- **中断 trace 里没有 VM-exit 就判 trace 缺失**:posted interrupt 等旁路会减少甚至避免常规 VM-exit
- **把 dm-land 与 hv-land 的 ivshmem 混用**:两种实现之间永远不通,且前缀写错会直接初始化失败
- **在 pre-launched VM 上指望 dm-land**:dm-land 只支持 post-launched
- **指望 dm-land 有 doorbell 通知**:通知只在 hv-land 实现
- **忽略 dm-land 下 BAR2 被 guest 重编程的影响**:共享内存会变得不可用
- **忽略 VT-d 中断重映射的硬性要求**:不支持该硬件的平台上 ACRN 会拒绝启动 VM

View File

@@ -0,0 +1,103 @@
# Bao:静态资源分区 / shmem_id / cache coloring
<CORE RULE>
Bao 是**纯粹的静态分区** hypervisor:
- 内存按两阶段翻译**静态分配**;I/O **只做直通**;**虚拟中断直接映射到物理中断**;vCPU 与物理 CPU **1:1**,**没有调度器**
- **没有运行时内存分配、没有完整设备模拟**
- **CPU、内存区、中断默认独占、不共享**;只有配置里明确声明的共享内存/IPC 才跨 VM
因此第一条判断是:
```
两个 VM 都能访问同一段物理 RAM
→ 若它不是 IPC/共享资源配置的一部分 → 先怀疑配置错,或你把地址层级看错了
```
**不要用"地址相同/不同"判断两个 VM 是不是在看同一个共享对象。**
</CORE RULE>
## 一、identity 来自配置,不来自地址
配置是一个 C 源文件,定义全局 `struct config`:
- **`.shmemlist`**:共享内存区列表。每个 `struct shmem` 含 `size`、`place_phys`、`base`/`phys` 联合体,以及 **`colors`**(L2 共享缓存的颜色位图;默认 `0x0` 表示使用全部颜色)
- **`.vmlist`**:VM 列表(必需)。每个 `struct vm_config` 含 guest 镜像(基址/装载地址、大小、`separately_loaded`、`inplace`)、`entry`、**`cpu_affinity`**(期望的物理 CPU 位图,**约定在各 VM 之间互斥**)、`colors`、`platform`
- 每个 VM 的 `struct vm_platform` 含:`cpu_num`、内存区(`regions`)、**IPC 通道(`ipcs`)**、MMIO 设备区(`devs`)、`mmu` 等
**IPC 通道 `struct ipc` 的关键字段**:
| 字段 | 含义 |
|---|---|
| `base` | 该 IPC 内存区**在本 VM 中的虚拟基址** |
| `size` | 大小,**应小于或等于其 `shmem_id` 所指向共享区的大小** |
| **`shmem_id`** | **关联到 `.shmemlist` 中某个共享区的 ID——这才是共享对象的 identity** |
| `interrupt_num` / `interrupts` | 分配给该 IPC 通道的中断号数组(doorbell 机制) |
**所以:**
```
VM A: 0x70000000 → shmem_id 2
VM B: 0x90000000 → shmem_id 2
```
**两个地址不同,底层却是同一个通信对象**——按"地址相等 == 同一 buffer"推理会直接出错。(反过来,两个 VM 用了相同虚拟地址也**不**说明是同一对象。)
## 二、中断与设备:直通、独占、1:1
- 设备在配置时**独占分配给某个 VM**,中断**直接路由到该 VM**,以避免虚拟化开销
- vCPU 按配置的 affinity 分配到物理 CPU(未定义 affinity 时顺序分配)
- **Bao 自己只需要一个中断**,用于核间通信/同步的 IPI
- 初始化分七个阶段(early boot → CPU init → memory init → platform init → interrupt init → VMM init → VM launch/run),**之后不再有调度或动态重配置**
## 三、cache coloring:干扰抑制,但不是万能
- 通过 `colors` 位图把 LLC 分成互不相同的颜色分给各 VM,减少互相干扰
- **默认未启用**;取值在运行时会**按平台可用颜色数截断**,所以**效果是平台相关的**
- 实测(16 色 × 32 KiB 的平台上):每 VM 分 7 色(224 KiB),另留 2 色给 hypervisor,干扰可降低近一半——**但不能完全消除**
- **代价**:coloring 会让**超大页无法使用**(Bao 本来用大页降低 TLB 压力),从而**增加 TLB 压力与开销**
**做延迟/性能归因时不能假设所有 VM 天然共享整个 LLC。**
## 四、MPU 目标上的额外细节
在无 MMU(MPU)目标上,共享内存区需要**跨核同步**:某核映射共享区时会与共享该区的其他核交换消息(类型/ID 与区域配置),以 IPI 通知;**early boot 尚未启用 IPI 时,非主核以轮询方式等待**。vMPU 用内存保护条目替代页表,Arm PMSAv8 的粒度是 64 字节。
## 决策树
```
拿到 Bao 目标
├─ 先取配置(struct config:shmemlist + vmlist)
│ ├─ CPU 亲和、内存区、设备、中断各自归属哪个 VM
│ └─ 是否存在跨 VM 共享:看 shmem 条目与 ipc 的 shmem_id
├─ 两个 VM 访问同一物理内存
│ ├─ 是 IPC/共享配置的一部分?→ 正常(按 shmem_id 对齐,不看地址)
│ └─ 不是?→ 先怀疑配置错误或地址层级判断错误
├─ 共享区地址在两个 VM 里不同
│ └─ 正常:identity 是 shmem_id,base 只是本 VM 的映射地址
├─ 性能/延迟归因
│ ├─ 是否启用 cache coloring(colors 位图)
│ ├─ coloring 生效会牺牲大页 → TLB 压力上升
│ └─ 效果随平台可用颜色数截断,不能跨平台照搬结论
└─ 无 MMU(MPU)目标
└─ 共享区映射靠跨核消息 + IPI(early boot 期非主核轮询)
```
## 工具与验证
- 配置:`struct config`(`.shmemlist` / `.vmlist`)、`vm_platform`(regions / ipcs / devs)、`struct ipc`(base / size / shmem_id / interrupts)
- 共享:按 `shmem_id` 对齐不同 VM 的映射,而不是按地址
- 隔离:CPU 亲和与设备/中断的独占归属;七个初始化阶段之后的运行期无重配置
- 验证:能同时说清「共享对象的 identity(shmem_id)+ 各 VM 的映射地址 + 中断归属」
## 该平台的坑(汇总)
- **用地址相等判断共享对象 identity**:identity 是 `shmem_id`,各 VM 的 `base` 可以不同
- **看到两个 VM 访问同一物理内存就判"配置泄漏"**:先看它是不是 IPC/共享配置的一部分
- **以为资源可以运行时申请**:静态分区,初始化七阶段之后没有动态重配置
- **期待有调度器**:vCPU 与物理 CPU 1:1,没有调度器
- **以为中断会被虚拟化/模拟**:中断直接路由给所属 VM,I/O 只做直通
- **忽略 IPC 的 `size ≤ 对应 shmem 大小` 约束**:配置不满足时行为不可预期
- **在性能分析里假设 VM 共享整个 LLC**:cache coloring 会改变这一点,且受平台颜色数限制
- **为了 coloring 忽略大页损失**:会抬高 TLB 压力,可能抵消收益
- **在 MPU 目标上忽略跨核共享同步**:共享区映射需要跨核消息与 IPI

View File

@@ -0,0 +1,156 @@
# Hyper-V / VMBus:VSC·VSP / GPADL / SR-IOV 数据面切换
<CORE RULE>
Hyper-V 的合成设备(synthetic device)不走"硬件 MMIO + 中断",而走 VMBus:
```
子分区: VSC(guest 侧驱动,如 netvsc / storvsc)
↕ VMBus(双向通道 = 两个 ring buffer)
根分区: VSP(宿主侧实现合成设备)+ 硬件
```
所以:
```
设备 I/O 看不到明显的硬件 MMIO
→ 可能只是合成设备流量走 VMBus
```
第二条:**GPADL 不是内存地址**——它是"描述并映射一片客户机缓冲区"的句柄。
</CORE RULE>
## 一、VMBus 的机制
- VMBus 通过**映射到多个分区的内存页**实现——数据经总线传递**不需要 hypervisor 拷贝**,减少上下文切换
- 一条通道是**双向**的,由**两个 ring buffer** 组成(进/出);高速设备(合成 SCSI、网卡)可用**多条通道**提高并行度
- 在 Linux 侧它是总线模型(`/sys/bus/vmbus`),主要用户是合成设备:存储、网络、视频、工具服务
## 二、GPADL:客户机物理地址描述符列表
- ring buffer 空间有限,性能关键路径用 **GPADL**:让根分区把**额外的客户机内存页直接映射进自己的地址空间**
- ring buffer 本身是 guest 分配、由 guest 把 GPA 列表**经 VMBus 控制路径**告知宿主;更大量数据传输另用独立的 GPA 列表描述数据缓冲区
- Hyper-V 对**经 GPADL 共享的客户机内存总量有限制**(Windows Server 2019+ 约 1280 MB,更早约 384 MB),防止恶意 guest 耗尽宿主资源
**RE 含义**:
```
GPADL = 37
→ 不是 GPA 0x25000
→ 是一个句柄,背后是"一片被描述并映射的客户机缓冲区"
```
## 三、SR-IOV:数据面会"中途换路"
带 SR-IOV 的网卡上:
- **数据面**(VF):从 VM 内的 VF **直接走硬件到网卡**——**不经过 VMBus,也不经过 hypervisor**
- **控制面**:VF 的控制仍然**经 VMBus** 回到根分区的 PF 驱动
**VF 拆除时的迁移顺序**:
```
虚拟化栈通知 VPCI VSP(根分区管理 OS)
→ 经 VMBus 通知 guest 内的 VPCI VSC
→ NetVSC 从 VF 网卡驱动解绑
→ 子分区流量从 VF 数据面迁移到软件合成数据面
→ 后续:过滤规则搬到 PF 上的默认 VPort、删除 VF 的 VPort、请求函数级复位、释放 VF 资源
```
**所以**:
```
一开始 VMBus 很忙 → 后来包捕获里流量突然消失 → 网络却仍然正常
```
**不一定是数据丢失**——很可能已经切到 SR-IOV VF 直通路径(或反向迁回)。
## 四、GPADL 的删除会阻塞
- **若该 GPADL 当前仍被 server 映射,删除调用会被阻塞,直到它被解除映射**
- 从 buffer 创建 GPADL 时,**该 buffer 会被 probe 并锁定,直到 GPADL 被销毁**
- 所以 teardown 里"调用删除之后线程卡住"**先查远端映射的生命周期**,而不是马上判死锁
## 五、packet 的生命周期:回调返回 ≠ packet 结束
- **完成调用可以在处理回调中立即调用,也可以稍后调用**
- **直到完成调用被发起**,系统才会:清理未完成的映射、释放占用的缓冲区、在**对端要求完成通知时**发送完成 packet
- 因此:
```
callback returned ≠ packet lifetime ended
```
**逆向资源生命周期时不能把"回调返回"当释放边界。**
## 六、大块 payload 不在 ring packet 里
- 一个 packet 可以只有 **inline 控制缓冲**,大块 payload 通过**外部内存描述**提供
- 所以只 dump ring 时看到:
```
header / command / 几个字段
但找不到真正的数据
```
**很正常**——要继续解析外部的内存描述。
## 七、取外部数据可能返回"待决",而且会再次回调
- 若为描述这段内存需要把对应区域**分页进来**,取外部数据的调用会返回 **STATUS_PENDING**
- 此时**必须从 packet 处理回调返回**;**稍后回调可能以不同的 IRQL(例如 DPC)再次被调用**,届时再调一次就会成功并返回数据
- 于是 packet handler 的控制流实际是:
```
callback #1 → 取数据 → STATUS_PENDING → return
…稍后,可能在不同 IRQL…
callback #2 → 再次取数据 → 成功 → 继续处理
```
**普通的同步 CFG 很容易漏掉这条边。**
返回的 MDL 有两个易错点:
- **它描述的内存已被锁定**——不需要再 probe/lock
- **它既没有用户态也没有内核态虚拟地址**——需要虚拟地址时必须显式映射(并在之后解除映射),或者直接把 MDL 交给做 DMA 的驱动
- **MDL 在"完成"被调用时失效**,之后由内核库释放;**驱动不需要自己释放它**
需要把处理放到被动级别时,可用"延迟到被动级"的接口把 packet 交过去。
## 决策树
```
拿到 Hyper-V 环境目标
├─ 设备 I/O 找不到硬件 MMIO
│ └─ 先按合成设备(VSC/VSP 经 VMBus)解释
├─ trace 里出现小整数句柄
│ └─ 可能是 GPADL(客户机缓冲区描述),不是地址
├─ 网络流量中途"消失"但功能正常
│ ├─ 查是否启用 SR-IOV:VF 数据面不过 VMBus
│ └─ 查 VF 拆除/迁移路径(VPCI VSP → VMBus → VPCI VSC → NetVSC 解绑)
├─ 通道拥塞 / 性能异常
│ └─ 看是否使用了多通道(高速设备常有)
└─ 共享内存用量相关
└─ GPADL 总量存在上限(宿主侧限制),不是无限共享
```
## 工具与验证
- 通道与设备:VMBus 总线视图、VSC/VSP 配对关系、合成设备类型
- 缓冲区:GPADL 建立与使用点;对照"这是句柄不是地址"
- 网络路径:VF 是否存在、控制面是否仍在 VMBus、数据面当前走 VF 还是合成路径
- 验证:能同时说清「这条 I/O 走合成路径还是 VF 直通 + 共享内存经 GPADL 如何描述 + 控制面在谁那里」
## 该平台的坑(汇总)
- **把 GPADL 当内存地址**:它是缓冲区描述符句柄
- **在合成设备上找硬件 MMIO**:VMBus 走共享 ring,不走寄存器窗口
- **把 SR-IOV 切换当数据丢失或抓包失败**:数据面可以从 VMBus 切到 VF 直通(并可能切回)
- **以为 VF 的一切都绕开 VMBus**:数据面绕开,**控制面仍走 VMBus**
- **忽略多通道**:高速设备可能并行使用多条通道
- **以为 GPADL 共享无上限**:宿主对共享总量有限制
- **把删除 GPADL 时的阻塞当死锁**:它会等 server 解除映射;从 buffer 创建的 GPADL 还会锁定该 buffer 直到销毁
- **把处理回调返回当 packet 结束**:完成调用才是释放边界,且可能被延后
- **在 ring packet 里找大块 payload**:大块数据走外部内存描述
- **把取外部数据返回"待决"当失败**:需要分页时就是它会返回的,之后会在可能不同的 IRQL 再次回调
- **从外部数据 MDL 直接取虚拟地址**:它不带任何虚拟地址,需要显式映射并解除
- **自己去释放外部数据 MDL**:它在完成调用时失效,由内核库释放

View File

@@ -0,0 +1,107 @@
# Jailhouse:cell 配置 / park / loadable 内存 / ivshmem
<CORE RULE>
Jailhouse 是**静态分区** hypervisor:**配置文件比 inmate 的 ELF 本身更重要**。
它没有"VM 运行时申请资源"这回事——`.cell` 配置直接规定 CPU 集合、内存区、IRQ 芯片、PIO、PCI 与权限。
所以分析一个 inmate:
```
inmate 镜像 + .cell 配置 + root cell 配置 + 宿主 DT/ACPI
```
**缺了配置那一半,关于"它为什么能/不能访问某个地址"的判断基本都是猜。**
</CORE RULE>
## 一、资源归属在配置里,不在运行时协商
- cell 描述符包含:CPU set、内存区(含 `phys_start` / `virt_start` / `size` / 标志)、IRQ、PIO、PCI 归属与权限
- 内存区标志是判据:**`READ` / `WRITE` / `EXECUTE` / `DMA` / `COMM_REGION` / `LOADABLE`**(注意 **`DMA` 标志缺失会让非 root Linux cell 不把这些区识别为 RAM**)
- `jailhouse cell load` 会把镜像装进配置指定的内存区,**装载地址必须落在映射区内**(例如未设 `virt_start` 而镜像落在低内存之外,就会以 "Invalid argument" 失败——这是配置问题,不是镜像问题)
## 二、越权访问 → CPU 被 park(不是 guest 崩溃)
- 访问未映射给该 cell 的内存/外设时,hypervisor 记录 **"Unhandled data read" / "FATAL: unhandled trap"**,随后 **"Parking CPU n (Cell: ...)"**
- **被 park 的 CPU/cell 等于停止运行**
所以看到"程序突然不动了"、又找不到 page fault/总线错误的常规证据,**先对照 `cell.mem_regions`、PCI 与 IRQ 归属**:**非法访问被 hypervisor 拦下并停机**是一等公民的解释。
## 三、`JAILHOUSE_MEM_LOADABLE`:装载窗口是一次性的
- 标记为 `LOADABLE` 的内存区**可以映射进 root cell,用于装载/重装镜像**
- **cell 启动时,这些映射会被撤销**("对标记为 loadable 的区域的 root cell 访问被收回")
- 需要重装时走 **Cell Set Loadable** 超话:**关停运行中的 cell,并把其 loadable 区域重新映射回 root cell**
- 相关的错误码有区分度:非 root cell 发起或被目标 cell 拒绝关停 → `-EPERM`;cell 不存在 → `-ENOENT`;**root cell 不能被设为 loadable** → `-EINVAL`
- 后来还加入了在 Cell Set Loadable 时**复位 PCI 设备**的行为(此前活跃的设备先静默、停止 DMA,避免与装载冲突)
**典型现象**:
```
root Linux 启动前能直接访问 0xXXXX
inmate 启动后再访问 → fault
```
**这是设计如此**,不是"内存被保护起来了/被 hook 了"。
## 四、ivshmem:状态从共享内存协议里读,不要指望 pending 位
ivshmem v2 的寄存器语义与标准 PCI 设备**故意不同**:
- **Interrupt Control Register**:bit 0 使能中断;**若启用 one-shot 模式,设备在每次投递中断时把 bit 0 清零**——这是给 UIO 类驱动的自动节流,免去一次 VM-exit
- **设备没有 pending interrupt 的概念**:**读 MSI-X 的 Pending Bit Array 永远返回 0**;INTx 方面 **Status Register 的 Interrupt Status 位从不置位**(与 PCI 规范有偏差),没有"pending INTx"信息
- 规范的做法是:**事件状态从共享内存里的协议信息推导**
- **Doorbell Register**:只写;低 16 位是向量号,高 16 位是目标 ID;写一个目标未使能的向量没有效果,写不存在的目标也没有效果;**读该寄存器未定义**
- **State Register**:写入会更新本地状态,并在远端设备上触发 MSI-X 向量 0(或 INTx),前提是新值与旧值不同;对端通过比较本地副本区分"状态变化"与"doorbell 事件"
- **State Table**:共享内存起始处的只读区,每个 peer 一个 32 位状态值;复位/断开时清零
- 协议类型编码在 PCI Class Code 的 interface/sub-class 里(例如虚拟点对点以太网、用户定义区间、Virtio over shared memory)
**推论**:
- **"收到中断但 PBA 恒为 0" 不是 hypervisor bug**;**"没有 pending 位可查"也不能推导出"没有事件"**
- 排查要以**共享内存里的协议状态**为准,必要时结合 State Table/State Register
## 五、hypervisor 地址空间里扫不到 inmate 内存是正常的
Jailhouse **不会把整个 cell 永久映射进 hypervisor 地址空间**:只有显式共享页、以及处理 MMIO 等场景下临时映射的页才可见,而且临时映射**可能是 per-CPU 的**。
所以"用 hypervisor VA 直接遍历所有 VM RAM"的取证/调试工具会漏掉大量内容——**这不是内存被藏了**。
## 决策树
```
拿到 Jailhouse 目标
├─ 先拿配置:.cell / root cell config / 宿主 DT·ACPI
│ ├─ 内存区标志(含 DMA)与 virt_start 是否与镜像匹配
│ └─ PCI / IRQ / PIO 归属
├─ 访问某地址后程序停住
│ ├─ 查 hypervisor 日志:Unhandled trap → Parking CPU n
│ └─ 对照该地址是否属于本 cell(不是 → 违规,不是驱动 bug)
├─ root cell 能访问 → cell 启动后不能
│ └─ LOADABLE 映射在 cell 启动时被撤销;重装需 Cell Set Loadable
├─ ivshmem 中断相关
│ ├─ PBA 恒 0 / Status 位不置位 → 设备本就不维护 pending 语义
│ ├─ 事件状态去共享内存协议里读(State Table / State Register)
│ └─ one-shot 模式下 Interrupt Control bit0 会被设备清零
└─ 在 hypervisor VA 里找不到某 cell 的内存
└─ 只有共享页与临时映射可见,且临时映射可能是 per-CPU
```
## 工具与验证
- 配置:cell 描述符(内存区与标志、PCI/IRQ/PIO 归属)、root cell 配置、宿主 DT/ACPI
- 违规判定:hypervisor 控制台输出(Unhandled trap / Parking CPU)
- ivshmem:寄存器区(Interrupt Control / Doorbell / State)、State Table、共享内存协议状态
- 验证:能同时说清「资源归属(配置里怎么分的)+ 访问为何被拦(违规 vs 映射撤销)+ 事件状态从哪里读」
## 该平台的坑(汇总)
- **只看 inmate ELF 不看 cell 配置**:资源归属全在配置里
- **把"访问后停机"当 guest 崩溃/驱动 bug**:越权访问会被 hypervisor park 掉 CPU
- **把 cell 启动后 root cell 失去访问当异常**:`LOADABLE` 映射在启动时被撤销,重装走 Cell Set Loadable
- **忘了内存区要标 `DMA`**:否则非 root cell 不把该区当 RAM,症状像"内存不见了"
- **用 MSI-X PBA 判断有无事件**:ivshmem 不维护 pending 语义,PBA 恒 0
- **把 Status Register 的 Interrupt Status 位当 pending 依据**:该位从不置位
- **读 Doorbell 寄存器**:它只写,读未定义
- **用 hypervisor VA 遍历所有 VM 内存**:只有共享页与临时映射可见
- **把 one-shot 模式下 bit0 被清零当"中断被吞"**:那是设备端的自动节流设计

View File

@@ -0,0 +1,140 @@
# LynxSecure:静态分离内核 / 该恢复什么 / 边界怎么找
<CORE RULE>
这是一个**极端的静态分离**设计,而且**没有中央管理内核**:
```
boot 时定义、不可变的硬件分区
不可变的硬件强制隔离(从加电到断电持续保持)
内核空间功能仅限:资源分区、控制分区之间的数据流、调解对系统状态变更函数的访问
I/O 与应用支持【全部导出到用户态 guest】
```
于是最反直觉的一条是:
```
运行态找不到完整的"VM 配置器 / 动态 MMU 配置引擎"
→ 与该模型自洽
→ 不要得出"固件被裁剪 / 被破坏"
```
</CORE RULE>
**说明**:该系统多数实现细节不公开。下面区分两类内容——**【官方表述】**(可直接引用)与**【该恢复什么】**(方法论,用于指导分析,不是对实现的断言)。
## 一、官方表述(判断"缺什么算正常"的依据)
- **boot 时定义、不可变的硬件分区**;**不可变的硬件强制隔离**,从加电到断电持续保持
- **最小特权**:内核空间功能**有限**——**资源分区、控制分区之间的数据流、调解对系统状态变更函数的访问**
- **按开发者要求,把 I/O 与应用支持全部导出到用户态 guest**——**内核里没有驱动、I/O 栈与应用 API**
- 运行期约束(官方列举):
- **没有 hypervisor/管理控制台登录**
- **没有动态系统修改**
- 一个 guest 不能访问另一个 guest 的资源
- **hypervisor 自身也不能访问 guest 的资源**
- **可 DMA 设备不能访问 guest 资源**
(说明:公开资料**未直接证实**"用于初始配置的特权 setup code 随后被丢弃"这一具体机制;上面列的是官方明确表述的部分——判断"缺什么算正常"用它们即可。)
## 二、该恢复什么(逆向工作台层)
### 1. 配置是最重要的 artifact,优先于代码
资源在 **boot 时**被规定成固定、不可变的划分。所以第一优先级是**恢复"谁拥有什么"**:
```
CPU 核归属 / 内存区归属 / I/O 与设备归属
```
**做法**:配置本身(如果拿得到)是最高价值;拿不到就从**各 guest 可见的资源范围**反推——把每个 guest 能看到的物理范围并起来,**缺口就是别的分区**,重叠就是可疑点。
### 2. 启动阶段是唯一能看到"配置生效"的窗口
因为运行期**没有动态系统修改**,映射与归属都是**启动期建立**的:
```
追映射、追资源分配 → 在启动阶段做
运行期只应看到"已经确定"的状态
```
**所以**:如果运行期看到"某资源在动态重新分配",先怀疑你看到的是别的东西(例如 guest 内部的分配),而不是内核在重配置。
### 3. 边界怎么找:从"三项职责"反查
官方把内核功能限定为三件事,**这三件事出现在哪里,哪里就是分界**:
| 职责 | 观察点 |
|---|---|
| **资源分区** | 谁在读/写"划分表"一类的结构 |
| **控制分区之间的数据流** | 跨分区的数据通路在哪被拦截/校验 |
| **调解对系统状态变更函数的访问** | 系统状态变更入口的守门代码 |
**推论**:**内核里不应有驱动、I/O 栈与应用 API**——所以
```
在一个映像里看到完整驱动/I/O 栈
→ 那一块大概率不是内核
```
### 4. 跨分区通信在内存里长什么样
- 设计上是**无共享内存**的:硬件强制的隔离意味着**不存在"两个分区共享同一块物理内存"的常规通路**
- 跨分区的数据流由内核**控制**——所以找的是**受控通道**(数据流被拦截/校验的路径),**不是共享页**
- 这与"共享内存 + 通知"型系统(见 [[re-hypervisor/xen]]、[[re-hypervisor/hyperv-vmbus]])**方向相反**:先找控制点,再找数据通路
### 5. IOMMU / 设备归属怎么验证
官方约束可以直接当**验证判据**:可 DMA 设备不能访问 guest 资源、hypervisor 自身不能访问 guest 资源。
于是:
```
拿一条 DMA 通路问:它的源与目标分别属于哪个分区?
若答案是"跨了不该跨的边界" → 与模型矛盾,值得深挖
若答案是"同一分区内" → 与模型自洽
```
### 6. fault path 与"8 MB 里哪一段是 monitor"
- 最小内核意味着**异常入口很窄**:找"分区被 violated 之后控制权交给谁",那一段就是核心
- 结合第 3 节的三项职责定位:**读划分表 / 拦数据流 / 守系统状态变更入口**的代码块属于内核;周围的驱动与 I/O 栈属于 guest
## 决策树
```
拿到一份静态分离内核的映像
├─ 先问:配置拿得到吗
│ ├─ 拿得到 → 据它恢复 CPU/内存/I/O 归属,再解释代码
│ └─ 拿不到 → 从各 guest 可见范围反推,缺口=别的分区
├─ 找不到"VM 配置器/动态重配置"
│ └─ 与"静态配置 + 无动态系统修改 + 无管理登录"自洽
├─ 要追映射
│ └─ 在启动阶段追;运行期不应有重配置
├─ 要定位内核本体
│ ├─ 找三项职责:读划分表 / 跨分区数据流拦截 / 系统状态变更守门
│ └─ 内核里不该有驱动与 I/O 栈 → 有驱动的块大概率不是内核
├─ 跨分区通信
│ └─ 找受控通道,不是共享页(与共享内存型系统方向相反)
├─ 验证隔离
│ └─ 用官方约束当判据:DMA 设备 / hypervisor 自身都不应触及 guest 资源
└─ 运行期看到"资源被重新分配"
└─ 先怀疑那是 guest 内部行为,内核本不该做这件事
```
## 工具与验证
- 配置:CPU/内存/设备归属的静态划分(配置本身或从 guest 可见范围反推)
- 启动:启动阶段建立映射与归属的过程(唯一能看到"配置生效"的窗口)
- 边界:三项职责的落点;驱动/I/O 栈的存在与否
- 通信:受控通道而非共享页
- 验证:能同时说清「谁拥有哪块资源 + 这段代码属于内核还是 guest + 这条数据流是否跨了分区」
## 该平台的坑(汇总)
- **把"缺少集中管理代码"当代码被裁剪**:静态配置模型本来就如此
- **在运行期找"重配置"逻辑**:没有动态系统修改;该在启动阶段找
- **把 guest 内部的资源分配当成内核在重配**
- **在内核区域里期待驱动**:I/O 与应用支持被导出到 guest
- **按"共享内存 + 通知"找跨分区通信**:这里是受控通道、无共享内存
- **拿"hypervisor 能读 guest 内存"当默认假设做取证**:官方明确相反
- **把可 DMA 设备的中介缺失当漏洞**:隔离模型要求它不能触及 guest 资源
- **以为有管理控制台入口**:官方明确没有

View File

@@ -0,0 +1,118 @@
# QNX Hypervisor:三层地址 / vdev / 虚拟中断
<CORE RULE>
guest 里的"物理地址"根本不是宿主物理地址。地址至少三层:
```
guest virtual → guest physical(ARM 术语 IPA)→ host physical
```
而且 guest 看到的那台"设备"**可能是由配置人工制造出来的**:
```
guest driver 的 MMIO 地址 = 0x1c090000
≠ 真实 SoC 上那里就有该设备
```
如果那是 **vdev**,这个地址完全由 VM 配置决定。**先判"这是直通、还是 vdev",再谈驱动写得对不对。**
</CORE RULE>
## 一、地址模型
- guest RAM 由创建 VM 的进程(qvm)配置;对 guest 看起来是物理内存,**但仍需再翻译一层**
- **直通设备**:guest 看到的设备物理地址与宿主物理地址**没有直接对应关系**(与共享内存不同)——虚拟化层做转换
- **shmem vdev**:配置里那个"工厂页地址"(例如放在所分配 RAM 之外的某处)**明确是 guest physical,不是真实硬件地址**
**所以"guest 里的 MMIO 地址在真实 SoC 上找不到东西"不是错误**。
## 二、vdev:guest 驱动必须匹配被暴露出来的虚拟硬件
- hypervisor 配一个 PL011 vdev,guest 就**应当认为那里真的有一个 PL011**——匹配的对象是 vdev 暴露的硬件模型,不是板子上的原件
- 推论:**guest 的 device tree ≠ 物理板级 device tree**,完全可能不同
## 三、shmem vdev:工厂页 + 控制页
共享内存 vdev 在 guest 侧表现为"一组映射在 guest-physical 里的寄存器 + 能触发中断",可以是 MMIO(直接在配置里给 `loc`/`intr`),也可以是 PCI(默认;位置与中断由 hypervisor 分配)。
**工厂页(4 KB)** 是它的虚拟寄存器页:
| 字段 | 作用 |
|---|---|
| `name` | 区域名 |
| `size` | 大小——**写入即触发创建** |
| `shmem` | 返回控制页的 guest-physical 地址 |
| `vector` | 中断向量 |
| `status` | 状态 |
- **创建是首次使用时完成的**,因此**各 guest 可以任意顺序启动**——"先启动的那个也正常"不需要额外解释
- 每个共享区前面还有自己的**控制页**:`status`(活跃客户端 / notify 位)、`idx`(连接 ID)、`notify`(写位集以通知其他 guest)、`detach`
- guest 之间就是**通过写控制页**通信的
## 四、虚拟中断:`intr pass` 与 `intr vdev` 是两条不同的路
VM 配置里的 `intr` 关键字决定中断来源:
| 形式 | 含义 |
|---|---|
| `pass` | 由**宿主**把物理设备的中断路由给 guest(可给 guest 中断号,或引用一张把宿主向量映射到 guest 中断号的宿主 PIC) |
| `vdev` | 中断由**运行在宿主侧的 vdev 代码**产生(引用 guest vdev 向其发中断的宿主 PIC,或直接指定 guest 中断控制器与线号) |
- **x86 上 LAPIC 自动提供**(vdev 只写 `intr apic`,无需线号),例如 `vdev ioapic` + `intr apic`、`vdev ser8250` + `intr myioapic:4`
- **ARM 上 GIC 自动提供**(也可显式指定)
- shmem 的通知走 `notify` 位集:接收方 guest 的 IRQ 处理程序去读 `status`(低位表示谁通知了它,高位表示活跃连接)
**所以"driver 的 ISR 很短,但中断延迟明显变大"不一定是 driver 慢**:路径可能是
```
物理设备 → hypervisor(中断归属/路由) → 虚拟中断 → guest ISR
```
做延迟分析时必须把虚拟化层算进去。
## 五、guest exit 何时发生(决定了你能看到什么 trace)
Halt、**vdev 访问**(例如对 shmem 工厂页 size 寄存器的 MMIO 写会被 trap,用于创建/接入区域)、宿主侧中断、虚拟定时器、CPUID 类指令。
## 六、直通设备:一次只能有一个 resident 访问
- 直通时 **hypervisor 不需要设备驱动**:它只负责识别并放行物理设备到 guest 的中断、把 guest 的信号直接交给设备,其余交互都是 guest 对设备
- **同一时刻只允许一个 resident(host 或 guest)访问某个直通设备**——"host 与 guest 同时操作同一设备"不是可选项,是错误状态
## 决策树
```
拿到 QNX Hypervisor 目标
├─ 先判设备形态
│ ├─ 直通(pass)→ GPA 与 HPA 无直接对应;一次只能一个 resident 访问
│ └─ vdev → 地址/寄存器布局由 VM 配置制造,guest DT ≠ 板级 DT
├─ 地址对不上真实硬件
│ └─ 正常:至少三层地址;shmem 工厂页地址明确是 guest physical
├─ 共享内存相关
│ ├─ 找工厂页(name/size/shmem/vector/status),写 size 才触发创建
│ ├─ 首次使用时创建 → 各 guest 启动顺序无所谓
│ └─ 数据面写控制页(status / idx / notify / detach)
├─ 中断相关
│ ├─ 分清 intr pass(宿主路由)与 intr vdev(宿主侧 vdev 产生)
│ ├─ 延迟变大 → 把 hypervisor 的中断归属/路由算进路径
│ └─ shmem 通知 → 读 status 判定谁通知、谁活跃
└─ trace 里 VM-exit 很多
└─ 检查是否有 vdev 寄存器写(如工厂页 size)被 trap
```
## 工具与验证
- 配置:VM 配置里的 RAM 布局、`loc`/`intr` 关键字、vdev 类型(MMIO 还是 PCI)
- 共享内存:工厂页字段与控制页字段(`status` / `idx` / `notify` / `detach`)
- 中断:区分 `pass` 与 `vdev` 两条来源,再看 guest 侧中断控制器(x86 LAPIC / ARM GIC 自动提供)
- 验证:能同时说清「地址处于哪一层 + 设备是直通还是 vdev + 通知与中断来自哪条路径」
## 该平台的坑(汇总)
- **拿 guest 的 MMIO 地址去真实 SoC 上找设备**:三层地址;vdev 的地址完全由配置决定
- **把 guest device tree 当板级 device tree**:vdev 暴露的是虚拟硬件模型
- **以为共享区一定要先由某个 guest 创建**:工厂页写 size 时按需创建,启动顺序无关
- **把 shmem 当普通 PCI 设备**:它可以是 MMIO 也可以是 PCI,寄存器语义由工厂页/控制页定义
- **分不清 `intr pass` 与 `intr vdev`**:一个由宿主路由物理中断,一个由宿主侧 vdev 代码产生
- **把延迟归因于 driver**:先看中断是否经过 hypervisor 的归属与路由
- **以为 host 与 guest 能同时用同一台直通设备**:同一时刻只允许一个 resident 访问
- **把 vdev 寄存器写引发的 VM-exit 当异常**:那是 vdev 的正常工作机制(例如创建共享区)

View File

@@ -0,0 +1,126 @@
# Quest-V:多内核 sandbox / 每 sandbox 一个 monitor / 故障与在线恢复
<CORE RULE>
不是"一个 hypervisor 管所有 VM",而是**多个 sandbox kernel,每个配一个自己的 monitor**:
```
sandbox domain = 一个或多个 CPU + 一块主机物理内存
(内含【本地 monitor】+ sandbox kernel + 本地应用)
monitor 只为【一个】sandbox 维护 EPT 映射
→ 省掉传统 hypervisor 调度/切换 guest 地址空间的开销
```
于是两条容易误判成"缺失"的现象其实是设计:
```
没有大量 VM-exit → sandbox 自己管 I/O
没有中央调度器/全局时钟 → 每 sandbox 自己调度、自己的物理时钟
```
</CORE RULE>
## 一、monitor 什么时候才被调用
**只在四件事上介入**:
```
① 引导各内核(bootstrapping)
② 处理故障(fault)
③ 管理与发起影子页表(shadow page tables)
④ 建立 sandbox 间通信通道
```
其余时间 **sandbox 直接在硬件上运行**。
- monitor 可以按**每核**建立,也可以为一个**核的集群**建立("域/单元")——**不是天然一核一 monitor**
- monitor 跟踪**影子页表映射**,这些映射表达**不可变的内存访问能力**,所以 **sandbox 代码无法修改"管辖主机内存访问的 EPT 映射"**
## 二、EPT 用于内存隔离,不是 CPU 虚拟化
- 用 EPT/嵌套页表主要是为了**内存隔离**(额外一层逻辑保护环),**指令直接在硬件执行**
- **sandbox kernel 的代码段实际是只读的**——因为 sandbox 代码**没有访问自己 EPT 映射的途径**
- 每个 sandbox 做**自己的本地调度与设备管理**
## 三、中断与 I/O:直接交给 sandbox
- **中断被定向到 sandbox kernel**(I/O APIC 广播或 IPI),并在驱动里做**早期解复用**——避免 monitor 介入与复杂的 I/O 虚拟化
- **部分驱动数据结构跨 sandbox 共享**,另一些是复制或同步的
## 四、sandbox 间通信
- **显式消息传递 + 共享内存通道**,传**缓存行大小**的消息
- 一个 sandbox 里的服务可以访问另一个 sandbox 里的服务
- **IPI 用于 sandbox 间通信**;APIC 机制**也用于重路由中断以做远程故障恢复**
## 五、故障路径(定位 monitor 的第一线索)
- 故障被检出时会产生 **EPT violation → 控制权转移(VM-exit)到对应的 monitor**
- **若某故障不会自动触发 VM-exit,故障处理器可以强制触发它**(例如用某条必然陷入的指令)
- sandbox kernel **只读段**里的故障检测代码被**假定安全**
- **抢占超时可周期性强制陷阱到 monitor**——这样**检测逻辑就位于 monitor 内**,与被篡改的 sandbox 隔离
## 六、恢复:本地与远程,都是在线的
- **本地恢复**:monitor 释放故障组件的内存、**可能重新初始化整个 sandbox**、或用**替代实现**替换组件(功能/实现多样性),并**通过调整该 sandbox 的 EPT 映射**来激活替代实现
- **远程恢复**:本地 monitor 选一个目标 sandbox(随机、轮转、或按负载),**经 IPI 通知**;远端 monitor 执行恢复,**经消息传递从共享内存取状态**
- 恢复**在线完成,不重启整机**;未受影响的 sandbox 继续正常运行
## 七、实操:手里这份映像,哪一段是 monitor
几个可用的判据(按可靠性排序):
1. **谁持有并修改 EPT 映射/影子页表**——这是 monitor 独有的能力(sandbox 代码无法访问自己的 EPT 映射)
2. **故障入口**:VM-exit 的目标、以及"强制触发陷入"的那条路径
3. **抢占超时的定时逻辑**:它被刻意放在 monitor 内与被篡改的 sandbox 隔离
4. **引导/通道建立代码**:只在启动与建通道时出现
反之,**大量设备驱动与本地调度逻辑属于 sandbox kernel,不是 monitor**。
## 八、时间与迁移
- **没有全局时钟**:每个 sandbox 有自己的物理时钟,事件计时由**每核本地定时器**管理,**不保证同步**
- 既没有全局调度器也没有全局时钟 → **跨 sandbox 迁移与通信的时序分析必须把时钟偏差算进去**(相关研究专门推导过含时钟偏差的通信界限)
- 跨 sandbox 的时间戳**不能按单一时间线比较**
## 决策树
```
拿到多内核 sandbox 系统的映像
├─ 找不到中央 hypervisor
│ └─ 正常:每 sandbox 一个 monitor
├─ VM-exit 很少
│ └─ 正常:sandbox 自己调度与管 I/O,中断直接投递
├─ 要定位 monitor
│ ├─ 谁持有/修改 EPT 与影子页表
│ ├─ VM-exit 目标与"强制触发陷入"路径
│ ├─ 抢占超时的定时逻辑
│ └─ 引导/建通道代码
├─ 故障与恢复
│ ├─ EPT violation → VM-exit 到对应 monitor
│ ├─ 本地恢复可重初始化整个 sandbox 或换实现(改 EPT 激活)
│ └─ 远程恢复经 IPI + 共享内存传状态,全程在线
├─ 跨 sandbox 通信
│ └─ 显式消息 + 共享内存通道(缓存行大小消息)+ IPI
└─ 时间线对不上
└─ 无全局时钟:各 sandbox 时钟不保证同步,别按单一时间线断言
```
## 工具与验证
- 资源:sandbox domain 到 CPU/内存/设备的映射
- monitor:EPT/影子页表、故障入口、抢占超时、引导与通道建立代码
- 通信:共享内存通道的消息流与 IPI
- 时间:各 sandbox 的本地时钟与偏差
- 验证:能同时说清「这段代码属于 monitor 还是 sandbox kernel + 该故障经哪条路径到 monitor + 这条跨域数据的时序基准是谁的时钟」
## 该平台的坑(汇总)
- **把"没有中央 hypervisor"当系统缺失**:每 sandbox 一个 monitor 是设计
- **把"VM-exit 很少"当没有虚拟化隔离**:指令直接在硬件跑是设计
- **以为 monitor 参与每次 I/O**:它只在引导、故障、影子页表、建通道时介入
- **把设备驱动当 monitor 的一部分**:驱动与本地调度属于 sandbox kernel
- **以为能改 EPT 映射**:sandbox 代码无法访问自己的 EPT 映射
- **把不同 sandbox 的时间戳按单一时间线比较**:没有全局时钟
- **以为恢复一定重启整机**:本地/远程恢复都在线进行
- **把"替代实现替换组件"当异常**:那是设计内的恢复手段
- **假设一核一 monitor**:monitor 可以按核集群建立

View File

@@ -0,0 +1,213 @@
# Xen:PV 前后端 / grant table / event channel / XenStore
<CORE RULE>
**不要把 Xen 的 PV 设备当普通 PCI/MMIO 驱动。**
典型 PV I/O 没有寄存器窗口,只有四件套:
```
frontend
├─ XenStore :交换配置(含 ring-ref / event-channel)
├─ grant table :共享 page
├─ shared ring :请求/响应
└─ event channel :通知
│
backend
```
由此产生两类高频误判:**把 grant ref 当地址**、**把 event port 当 IRQ 号**。两者都只是**索引/句柄**,不是地址空间里的东西。
</CORE RULE>
## 一、XenStore:配置交换处,值以文本存放
- 前端在 XenStore 里记录后端域(`backend-id`,通常为 0),并把**共享 ring 的 grant 引用**写进去:`ring-ref`、`tx-ring-ref`、`rx-ring-ref`、`ctrl-ring-ref` 等
- 通知端口同样写进 XenStore:`event-channel`(网络控制环用 `event-channel-ctrl`)
- **值是字符串**(XenStore 是文本式的)——"ring-ref=42" 存的是文本 `"42"`,不是二进制字段
- **前端把内存共享给后端**(不是反过来):前端用类似 `xenbus_grant_ring()` 的方式把本地分配的 ring 共享出去,再把引用告诉后端;后端拿到引用后在自己内核地址空间里分配页并映射
**RE 含义**:拿到一个 PV 前端,**先读它在 XenStore 里写了什么**——那里有后端域、ring 引用、事件端口,是恢复拓扑的现成入口。
## 二、grant reference 是**条目索引**,不是物理地址
```
ring-ref = 42
≠ page 0x2a / 物理地址 0x2a000
ring-ref = 42
→ 前端 grant table 的第 42 号条目
→ { flags, domid, frame(MFN) }
→ 后端据条目去映射那一页
```
- 每个域有自己的 grant table(一块与 hypervisor 共享的内存,由 guest 分配/初始化后经 hypercall 交给 Xen)
- 条目三个字段:**`flags`(操作与权限)、`domid`(被授权的远端域)、`frame`(被授权页的 MFN,注意是 MFN 而非虚拟地址)**
- **grant reference 就是条目下标**;下标被传给远端域,远端用它"激活"该条目
- 引用可以内嵌在 ring 消息里传递(如块设备的 `blkif_request_segment.gref`、网络的 `netif_tx_request.gref`)——**在 ring 里看到一个小整数,很可能是 gref,不是偏移或长度**
**权限与状态位(决定"能不能撤销")**:
| 名称 | 含义 |
|---|---|
| `GTF_permit_access` | 允许远端域访问本域某页 |
| `GTF_accept_transfer` | 接受把一页转移进本域 |
| `GTF_readonly` | 只读导出/导入 |
| `GTF_reading` / `GTF_writing` | hypervisor 置位,表示该页**当前正被远端映射**(读/写) |
| `GTF_transfer_committed` | 转移已完成 |
- **映射期间 Xen 不支持撤销**:结束外来访问的调用**只是阻止后续映射**,不会撤销已建立的映射;发送方必须**等 reading/writing 标志清零**(用查询接口确认)之后才能真正回收
- 两种用法要分清:**保留所有权的 grant access**(块前端/后端:A 授权、B 映射、B 用完 unmap、A 再撤权)与**所有权的 transfer**(只适用于 PV;较老的网络前后端用它,新设计改为经 hypervisor 拷贝以避开 TLB 开销)
## 三、event-channel port 不是 IRQ number
```
event-channel = 17 ≠ hardware IRQ 17
```
- event channel 是 Xen 的通知原语(`event_channel_op` hypercall),类型包括 **VCPU/IPI、VIRQ、PIRQ、域间(interdomain)**
- 域间通道常用 `alloc_unbound` / `bind_interdomain` 建立,端口号通常经 XenStore 传给对端
- **端口整数绑定到每 guest `shared_info` 里的全局位掩码**(32 位 guest 为 1024 位,64 位 guest 为 4096 位)——这是"端口"在这个系统里的真实表示
- 用户态接口另有 `/dev/xen/evtchn`
**所以 trace 里出现 `port 17` 时,不要去 IOAPIC/GIC 里找 IRQ 17。**
## 四、迁移:**本地端口不稳定,远端端口才是稳定标识**
- 域间 event channel 本质是**一对端口**:本地端口 + 远端端口
- 持有者退出会关闭 `/dev/xen/evtchn`,内核随之关闭本地端口;**重新连接时是用原来的远端端口去绑定一个全新的本地端口**,新本地端口号**可能与旧值不同**
- 因此**要记录/对齐的是 remote port**;把本地端口号当作持久 object identity 会出错
- 更危险的一点:**本地端口号在解绑后可被重用**,残留引用会指向完全不同的通道
**这与 seL4 的 CPtr、Zircon 的 handle 是同一类坑**(见 [[re-kernel/sel4-kernel]]、[[re-kernel/zircon-kernel]]):**整数句柄不是全局标识**。
## 五、shared ring 不是普通共享内存
典型流程:
```
前端拥有页 → grant 给后端 → gref 写入 XenStore
→ 双方各自映射同一批页 → event channel 只负责 kick
```
所以看到 **ring 内存持续变化、却找不到 `memcpy`/socket IPC** 是正常的——这是共享页 + 生产者/消费者索引的模型,通知只是"踢一下"。每个 ring 配一个 event channel:环变非空时唤醒接收方,变非满时唤醒发送方。
## 六、XenStore 是控制面,不是数据面
- **`state = Connected` 只表示协议协商完成**,不代表数据经 XenStore 传
- 真正的数据面可以是 **grant 共享 ring + event channel**:
```
XenStore 几乎没流量,设备 I/O 却很大
→ 正常
```
- **backend 不一定在 Dom0**:不要硬编码 `/local/domain/0/...`;前端里提供 **`backend` 与 `backend-id`** 用于明确定位 service domain——**正常 backend 区域不应当靠假设推导**
## 七、通知抑制:ring 前进却没有 kick 是正常的
- 生产侧的推送宏会**判断对端是否真的需要通知**:**只有新推进的请求越过了对端设置的 `req_event` 阈值时才发通知**(响应侧对称,用 `rsp_event`)
- 所以:
```
ring producer 前进 + 没有 event 发送
→ 完全正常
→ 对端已有工作在飞,它之后会自己再查 ring
```
- 顺序要求:写请求 → 写屏障(对端先看到请求)→ 更新 producer 索引 → 全屏障(对端先看到新请求)→ 再判断 `req_event`
## 八、"睡前再检查一次"是协议核心,不是奇怪 busy-loop
消费侧在准备睡眠时的标准形态:
```
发现无工作
→ 设置"下一个事件通知我"(req_event = req_cons + 1)
→ 全屏障
→ 再查一次 ring
```
- 这个屏障覆盖的竞态是:**producer 已经读过 `req_event`(因此决定不通知),而 consumer 随后才更新它**——不重新检查就会**丢唤醒**
- 已知副作用:按原样使用会带来"每 2^32 次事件一次多余唤醒"(索引回绕),实现里常见把事件指针推到过去的修法
- 另一个副作用:该宏**会推进 ring 的事件指针**,在 NAPI 类场景里可能"提前招来下一次中断",所以有些路径只在真正切回中断模式时才用它
**所以不要把"双重检查"优化掉,也别把相关的强屏障当冗余。**
## 九、event channel 的掩码/待决语义:不是"使能位置 1"
- event channel 是**"一位事件状态"**模型;**unmask 不是简单地打开使能位**
- FIFO ABI 的 unmask 路径:清除 MASKED(连带 BUSY),**若 PENDING 置位则发起 unmask 调用**;该调用的语义要求:**若事件处于 pending,必须向对应 VCPU 投递通知**
- 因此"masked 期间事件到达、unmask 之后行为诡异"应从 **pending / masked / FIFO 语义**解释,而不是按普通中断使能理解
## 十、FIFO event ABI:优先级,以及两种 ABI 不能混
- **优先级 0 最高、15 最低**,默认 7;**每 VCPU 有 16 个事件队列**
- 每个事件是带标志的字:**PENDING / MASKED / LINKED / BUSY** 加上指向队列下一项的 LINK 字段;每 VCPU 控制块含 ready 字与 16 个队头
- 含义:**不同 event 的投递延迟可能来自 FIFO 优先级,而不是 vCPU 调度**
- **同一个 Xen build 里,事件记账布局可能完全不同**——存在 **2-level ABI 与 FIFO ABI**:
```
拿 shared_info 的 pending 位图解析器去解释 FIFO 控制块 → 全错
```
- 一个具体陷阱:**初始化 FIFO 控制块的操作不会重新投递当前 pending 的事件(它们会丢)**,因此应当在绑定任何事件之前完成
## 十一、迁移后的身份变化
迁移可能要求前后端**重新协商协议**,因为 **domid 与 event channel 状态不一定保留**:
```
迁移前:domid 7 / port 10
迁移后:domid 12 / 新连接
```
**不应当判设备劫持。(与 [[re-kernel/zircon-kernel]]、[[re-kernel/sel4-kernel]] 的句柄本地性同源。)**
## 十二、内存屏障是协议的一部分
ring 生产者索引更新的前后都有屏障,保证请求/响应数据**先对另一端可见**。**这些屏障不是编译器噪声,删除即破坏协议。**
## 决策树
```
拿到 Xen 目标
├─ 先判:PV 前端 / 后端 / 全虚拟化(HVM)设备?
│ ├─ PV → 找四件套:XenStore 条目 + grant ref + shared ring + event channel
│ └─ 找不到寄存器窗口/MMIO 循环 → 正常,PV 本来就没有
├─ 看到 `ring-ref = N`
│ └─ N 是 grant table 条目索引 → 去读条目 {flags, domid, frame},不是地址
├─ 看到 `event-channel = N`
│ └─ N 是 event channel 端口(绑定到 shared_info 位掩码),不是 IRQ 号
├─ 页被远端访问 / 想回收页
│ ├─ 有 GTF_reading / GTF_writing → 映射仍在,撤销未生效
│ └─ 结束外来访问只阻止后续映射,必须等标志清零
└─ 迁移 / 重连后端口号变了
└─ 正常:本地端口会重新分配,稳定的标识是 remote port
```
## 工具与验证
- 配置来源:XenStore 树(`~/device/...` 与 `~/backend/TYPE/DOMID/DEVID`)里的 ring 引用与事件端口;前端树里的 `backend-id`
- 内存路径:grant table 条目(flags / domid / frame)与 reading/writing 状态位
- 通知路径:event channel 类型(IPI / VIRQ / PIRQ / 域间)与端口号;用户态经 `/dev/xen/evtchn`
- 验证:能同时说清「拓扑(谁在前、谁在后、经哪个 XenStore 路径连接)+ 内存靠哪组 gref + 通知靠哪个端口」
## 该平台的坑(汇总)
- **把 grant ref 当物理地址/页码**:它只是本域 grant table 的条目下标,条目里才有 MFN
- **把 event-channel port 当 IRQ 号**:它是 Xen 的通知端口,绑定在 `shared_info` 的位掩码上
- **以为结束外来访问就能立刻回收页**:映射期间不支持撤销,必须等 `reading`/`writing` 清零
- **把 event port 整数当持久对象标识**:迁移/重连后本地端口会变;解绑后同一号码还会被重用
- **拿本地端口(而非远端端口)做对齐**:重连时稳定的是 remote port
- **在 ring 消息里把小整数当偏移或长度**:很可能是内嵌的 gref
- **看到 ring 内存变化却无 IPC 调用就认为分析有漏**:PV 的数据面本来就走共享页,通知只负责 kick
- **在前端/后端之间搞反共享方向**:是前端把 ring 共享给后端,再由后端映射
- **在 XenStore 里期待二进制字段**:它是文本式的,引用与端口都以字符串存放
- **把 XenStore 的 `Connected` 当数据面在传**:它只表示协商完成;数据在 grant 共享 ring 上
- **硬编码 backend 在 Dom0**:应读前端的 `backend` / `backend-id`
- **把"ring 前进却没通知"当丢事件**:通知抑制是协议设计
- **删掉消费侧的双重检查或强屏障**:那正是防丢唤醒的部分
- **按"中断使能位"理解 event unmask**:masked/pending 语义不同,pending 必须投递
- **用一套事件解析器解释两种 ABI**:2-level 与 FIFO 的记账布局不同
- **在绑定事件之后才初始化 FIFO 控制块**:当前 pending 的事件会丢
- **把 FIFO 优先级造成的延迟归因于 vCPU 调度**
- **把迁移后的 domid/端口变化判为劫持**:需要重新协商属正常

View File

@@ -0,0 +1,121 @@
# XtratuM:XM_CF 配置驱动 / 调度计划 / IPVI
<CORE RULE>
XtratuM 是**配置驱动**的分离 hypervisor:资源不是运行时协商的,而是由 **XM_CF 配置文件**静态规定:
```
分区 / 内存 / 中断 / I/O 端口 / 通信端口 / 调度
```
**运行期不能新增动态对象。** 所以看到"找不到运行时资源分配代码",先怀疑**你手上没拿到配置文件**,而不是代码被裁剪。
</CORE RULE>
## 一、XM_CF:系统集成者与分区开发者的契约
配置是 XML,经校验后**翻译成二进制形式**(配置表)并集成进最终系统镜像。
主要内容:
| 章节 | 内容 |
|---|---|
| **内存** | 板级内存布局、给 hypervisor 与各分区的内存;分区内存区可带标志(未映射 / 映射到某处 / 共享 / 只读 / 不可缓存 / IOMMU) |
| **处理器** | CPU 如何分配给分区,含**调度计划** |
| **外设** | 不由 XtratuM 管理的外设、I/O 端口范围、每个分区的**中断线**与设备标识 |
| **健康监控** | 检出错误后如何处理(直接动作 / 交给出错分区 / 记日志 / 复位…) |
| **分区间通信** | 端口与通道(**采样**与**队列**两类),连接源端口与目的端口 |
| **跟踪** | 跟踪消息存放位置与跟踪范围 |
配置解析器会做语法与语义双重校验:内存区重叠/包含关系、分区名与 ID 重复、CPU 分配、端口重复、**循环调度计划的一致性**(分区是否都被分配、时间槽是否重叠、槽的分区 ID 是否有效)、硬件中断分配、I/O 端口对齐与占用、通道分配等。
## 二、两种调度:循环与优先级
**循环调度(ARINC 653 风格)**:`(分区, 虚拟 CPU)` 对被**固定循环**调度;时间槽在配置里以**起始时间 + 时长**定义;**major frame(MAF)**周期性重复,通常取各分区/线程周期的最小公倍数。
**优先级调度**:按配置里给分区的优先级调度(**优先级 0 最高**),抢占式;配成固定优先级的 CPU **不受其他 CPU 循环计划影响**。
**多个调度计划**(关键细节):
- **Plan 0 保留给初始化**:系统从 Plan 0 启动,直到某系统分区请求切换
- **Plan 1 保留作维护计划**:健康监控检出处境时可立即切到它,或由系统分区请求
- 设计者可定义 **Plan X(X>1)**
- **计划切换发生在当前计划的剩余槽全部执行完之后**
- 多核下:只有**循环计划**与多计划关联;优先级 CPU 独立于计划;涉及多个处理器的计划**必须定义相同的 MAF**
调度可视为状态机:每个状态对应某分区按计划执行;**`next_slot` 定时器中断导致上下文切换**;槽时长定时器由 XtratuM 独占使用。
## 三、中断与分区间虚拟中断(IPVI)
- 配置规定分区可访问哪个设备、设备标识与该分区使用的中断线;XtratuM 负责映射设备所在的内存区
- **IPVI 模拟真实的处理器间中断**:任何分区都可通过 hypercall 发出,向目的分区投递一个虚拟中断;**每个分区最多 8 个 IPVI**,实现为最后八个扩展虚拟中断
- 分区事件通常由分区自己处理;hypervisor 按配置中的动作提供通知机制;向端口写入/发送新消息时也可触发扩展中断(XtratuM 在分区控制表里维护端口状态位图)
## 四、运行期权限
**只有系统分区**才被允许控制系统与其他分区的状态、以及查询它们的状态。看到"某分区想改全局状态却做不到",先看它是不是系统分区。
## 五、计划切换的**两种时机语义**
- **Plan 0 是复位后的初始计划**,而且**不能像普通用户计划那样再切回来**;Plan 1 是维护计划;用户定义的计划通常从 >1 开始
- **普通请求的切换不是立即的**:系统分区请求切换时,**当前计划的槽全部跑完后**(major frame 结束)才切到新计划
```
请求切换之后旧计划还跑了一段时间 → 完全正常
```
- **但健康监控切维护计划可以立即发生**:它属于"终止当前槽、立刻开始维护计划"一类动作
**所以同一个"计划变了"事件存在两种完全不同的时机语义——不能用一个模型解释全部。**
- **同一 major frame 内多次请求**:最终可以是**最后一次请求**指定的计划生效——**不要把中间计划没有出现标成"请求被丢弃"**
## 六、采样端口的刷新与失效
- 采样端口是 **latest value**:新消息覆盖旧消息、没有队列;**刷新周期决定数据的"新鲜度",旧数据可能被标记为无效**
- 队列端口才是真正的"带缓冲的单播消息"——两者必须严格区分
- **写端口之后 guest 没收到通知**:XtratuM 的对象通知使用扩展中断,而**对应扩展中断在分区启动时默认是掩蔽的**——**"消息存在但没有中断"先查掩蔽状态**
## 七、故障后的控制流不由出错分区决定
- 健康监控按 **事件 / 范围 / 分区标识 / 故障地址或设备** 映射到预定义动作
- 所以"fault 之后系统自动切了调度"必须从健康监控策略解释,**不能只看出错 guest 自己**
## 决策树
```
拿到 XtratuM 目标
├─ 先要配置文件(XM_CF)
│ ├─ 分区/内存/中断/I/O/通道/调度都在里面
│ └─ 没有它,关于"为什么访问不到"的判断都是猜
├─ 调度相关
│ ├─ 循环计划:MAF + 槽(起始 + 时长)+ 分区/vCPU 对
│ ├─ 优先级计划:优先级 0 最高,抢占式
│ └─ 计划切换在当前剩余槽执行完之后才发生
├─ 分区不运行
│ ├─ 查当前处于哪个 Plan(Plan 0 = 初始化,Plan 1 = 维护)
│ ├─ 查该分区是否在当前时间槽
│ └─ 查健康监控是否触发了切换
├─ 通信相关
│ ├─ 采样端口 vs 队列端口(覆盖语义不同)
│ └─ 通知可经 IPVI(每分区最多 8 个)
└─ 运行期改配置
└─ 不可能:无动态对象;只有系统分区能控制系统状态
```
## 工具与验证
- 配置:XM_CF 的章节与生成后的配置表;解析器的语义校验项(可当"这份配置合法吗"的检查清单)
- 调度:处理器章节里的循环计划表/固定优先级表;当前 Plan 与时间槽
- 通信:端口表与通道定义;IPVI 的源/目的与编号
- 验证:能同时说清「该分区被分配了什么 + 当前处于哪个计划与时间槽 + 通信走哪个端口/通道」
## 该平台的坑(汇总)
- **没拿到 XM_CF 就分析资源归属**:一切都在配置里
- **以为可以运行时新增对象**:静态配置,无动态对象
- **把计划切换当请求后立即生效**:要等当前计划剩余槽执行完
- **忽略 Plan 0(初始化)与 Plan 1(维护)的保留语义**
- **多核下把优先级 CPU 也套循环计划**:优先级 CPU 独立于计划
- **多处理器计划未定义相同的 MAF**:配置校验会失败
- **把采样端口当队列端口**:覆盖语义不同
- **以为任何分区都能控制系统状态**:只有系统分区可以
- **忽略健康监控的配置动作**:它可以直接导致计划切换或分区状态变化