Hypercall 接口
虚拟机监控程序为来宾提供调用机制。 此类调用称为超calls。 每个 hypercall 定义一组输入和/或输出参数。 这些参数根据基于内存的数据结构指定。 输入和输出数据结构的所有元素都填充到最多 8 个字节的自然边界, (也就是说,双字节元素必须位于两字节边界上,依此) 。
第二个超调用约定可以选择用于超calls 的子集,尤其是具有两个或更少的输入参数且没有输出参数的调用约定。 使用此调用约定时,输入参数将传入常规用途寄存器。
第三个超调用约定可用于一部分超卡,其中输入参数块最多为 112 字节。 使用此调用约定时,输入参数将传入寄存器,包括可变 XMM 寄存器。
输入和输出数据结构必须同时放置在 8 字节边界上的内存中,并填充到大小为 8 字节的倍数。 虚拟机监控程序会忽略填充区域中的值。
对于输出,虚拟机监控程序允许 (但不能保证) 覆盖填充区域。 如果覆盖填充区域,它将写入零。
Hypercall 类
有两类超卡:简单和代表 (短为“repeat”) 。 简单的 Hypercall 执行单个操作,并且具有固定大小的输入和输出参数集。 rep hypercall 的行为类似于一系列简单的超卡。 除了固定大小的输入和输出参数集外,rep hypercalls 还涉及固定大小的输入和/或输出元素的列表。
当调用方最初调用 rep hypercall 时,它指定一个代表计数,该值指示输入和/或输出参数列表中的元素数。 调用方还指定代表开始索引,该索引指示应使用的下一个输入和输出元素。 虚拟机监控程序按列表顺序处理代表参数,即通过增加元素索引。
对于 rep hypercall 的后续调用,rep start 索引指示已完成的元素数 ,以及与代表计数值结合使用 – 剩余的元素数。 例如,如果调用方指定代表计数 25,且在时间约束内仅完成 20 次迭代,则 Hypercall 在将代表开始索引更新为 20 后将控制权返回给调用虚拟处理器。 重新执行 hypercall 时,虚拟机监控程序将在元素 20 处恢复并完成其余 5 个元素。
如果在处理元素时遇到错误,则会随代表完成计数一起提供适当的状态代码,指示在遇到错误之前已成功处理的元素数。 假设指定的超控件字有效, (看到以下) 并且输入/输出参数列表可访问,虚拟机监控程序保证至少尝试一个代表,但在将控件返回给调用方之前,不需要处理整个列表。
Hypercall 延续
超集可以被视为需要许多周期的复杂指令。 虚拟机监控程序在将控制返回到调用 hypercall 的虚拟处理器之前,尝试将 hypercall 执行限制为 50μs 或更少。 某些超超操作非常复杂,无法保证 50μs 保证。 因此,虚拟机监控程序依赖于某些超卡的超卡延续机制,包括所有代表超卡形式。
超通延续机制对调用方而言大多是透明的。 如果 hypercall 无法在规定的时间限制内完成,则控制权将返回给调用方,但指令指针不会超过调用 hypercall 的指令。 这允许处理挂起的中断,并计划其他虚拟处理器。 当原始调用线程恢复执行时,它将重新执行 hypercall 指令,并向前推进完成操作。
大多数简单的超卡保证在规定的时限内完成。 但是,少量简单的超卡可能需要更多时间。 这些超calls 使用 hypercall 延续以类似方式重新调用超calls。 在这种情况下,该操作涉及两个或更多内部状态。 第一次调用将对象 (,例如分区或虚拟处理器) 进入一种状态,在重复调用后,状态最终转换为终端状态。 对于遵循此模式的每个超集,将描述中间内部状态的可见副作用。
Hypercall Atomicity 和 Ordering
除了所指出的,Hypercall 执行的操作都是原子操作,例如,在来宾) 中执行的指令以及系统上执行的所有其他超函数 (。 简单的 Hypercall 执行单个原子操作;rep hypercall 执行多个独立原子操作。
使用 hypercall 延续的简单超卡可能涉及外部可见的多个内部状态。 此类调用包括多个原子操作。
每个超操作都可以读取输入参数和/或写入结果。 每个操作的输入可以在任何粒度下读取,并在执行操作之前,随时读取。 结果 (,即,与每个操作关联的输出参数) 可以以任何粒度写入,并在执行操作后和超call 返回之前随时写入。
来宾必须避免检查和/或操作与执行 hypercall 相关的任何输入或输出参数。 虽然执行 hypercall 的虚拟处理器无法执行 (,因为它的来宾执行被挂起,直到 hypercall 返回) ,但不会阻止其他虚拟处理器执行此操作。 来宾的行为可能会崩溃或导致分区中的损坏。
法律 Hypercall 环境
只能从最特权的来宾处理器模式调用 Hypercalls。 在 x64 platfom 上,这意味着具有当前特权级别的受保护模式, (CPL) 为零。 尽管实际模式代码以零的有效 CPL 运行,但不允许在实际模式下使用超卡。 尝试在非法处理器模式下调用 hypercall 将生成#UD (未定义的操作) 异常。
所有超卡都应通过体系结构定义的超call 接口调用, (请参阅以下) 。 尝试通过任何其他方式调用 hypercall (,例如,将代码从 hypercall 代码页复制到备用位置,并从该位置执行该代码) 可能会导致未定义的操作 (#UD) 异常。 无法保证虚拟机监控程序提供此异常。
对齐要求
调用方必须指定输入和/或输出参数 (GPA) 的 64 位来宾物理地址。 GPA 指针必须对齐 8 字节。 如果 hypercall 不涉及任何输入或输出参数,虚拟机监控程序将忽略相应的 GPA 指针。
输入和输出参数列表不能重叠或跨页边界。 Hypercall 输入和输出页应为 GPA 页,而不是“覆盖”页。 如果虚拟处理器将输入参数写入覆盖页并指定此页面中的 GPA,则未定义对输入参数列表的虚拟机监控程序访问。
虚拟机监控程序将在执行请求的 hypercall 之前验证调用分区是否可以从输入页读取。 此验证由两个检查组成:指定的 GPA 已映射,GPA 标记为可读。 如果其中任一测试失败,虚拟机监控程序将生成内存拦截消息。 对于具有输出参数的超calls,虚拟机监控程序将验证分区是否可以写入输出页。 此验证由两个检查组成:指定的 GPA 已映射,GPA 标记为可写。
Hypercall 输入
调用方通过名为 hypercall 输入值的 64 位值指定超值。 格式如下:
字段 | Bits | 提供的信息 |
---|---|---|
调用代码 | 15-0 | 指定请求的 hypercall |
迅速 | 16 | 指定 hypercall 是否使用基于寄存器的调用约定:0 = 基于内存,1 = 基于寄存器 |
可变标头大小 | 26-17 | QWORDS 中变量标头的大小。 |
RsvdZ | 30-27 | 必须是零 |
嵌套 | 31 | 指定应由嵌套环境中的 L0 虚拟机监控程序处理 hypercall。 |
Rep Count | 43-32 | 代表呼叫 (代表总数必须为零;) 否则为零 |
RsvdZ | 47-44 | 必须是零 |
Rep Start Index | 59-48 | 代表调用的起始索引 (必须为零;) 否则为零 |
RsvdZ | 63-60 | 必须是零 |
对于 rep hypercalls,代表计数字段指示代表总数。 代表开始索引指示相对于列表开头的特定重复, (零指示列表中第一个元素) 进行处理。 因此,代表计数值必须始终大于代表开始索引。
当快速标志为零时,注册 hypercall 输入的映射:
X64 | x86 | 提供的信息 |
---|---|---|
RCX | EDX:EAX | Hypercall 输入值 |
RDX | EBX:ECX | 输入参数 GPA |
R8 | EDI:ESI | 输出参数 GPA |
Hypercall 输入值与指向输入和输出参数的 GPA 一起传入寄存器。
在 x64 上,寄存器映射取决于调用方是在 32 位 (x86) 还是 64 位 (x64) 模式下运行。 虚拟机监控程序根据 EFER 的值确定调用方模式。LMA 和 CS.L. 如果设置了这两个标志,则调用方假定为 64 位调用方。
当快速标志为 1 时,注册 Hypercall 输入的映射:
X64 | x86 | 提供的信息 |
---|---|---|
RCX | EDX:EAX | Hypercall 输入值 |
RDX | EBX:ECX | 输入参数 |
R8 | EDI:ESI | 输出参数 |
Hypercall 输入值与输入参数一起传入寄存器。
可变大小的 Hypercall 输入标头
大多数超高输入标头具有固定大小。 因此,Hypercall 代码隐式指定了从来宾传递到虚拟机监控程序的标头数据量,因此不需要单独指定。 但是,某些超卡需要可变的标头数据量。 这些超卡通常具有固定大小的输入标头和可变大小的附加标头输入。
可变大小的标头类似于固定的超高输入 (对齐为 8 个字节,大小为 8 个字节) 的倍数。 调用方必须指定它作为输入标头提供的数据量。 此大小作为 hypercall 输入值的一部分提供, (请参阅上表中的“可变标头大小”) 。
由于固定标头大小是隐式的,而不是提供总标头大小,因此在输入控件中只提供变量部分:
Variable Header Bytes = {Total Header Bytes - sizeof(Fixed Header)} rounded up to nearest multiple of 8
Variable HeaderSize = Variable Header Bytes / 8
为未显式记录为接受可变大小的输入标头的超call 指定非零变量标头大小是非法的。 在这种情况下,hypercall 将导致返回代码。HV_STATUS_INVALID_HYPERCALL_INPUT
对于 Hypercall 的给定调用,可以接受所有标头输入完全适合固定大小的标头的可变大小的输入标头。 在这种情况下,可变大小的输入标头大小为零,并且 hypercall 输入中的相应位应设置为零。
在所有其他方面,接受可变大小的输入标头的超calls 在调用约定方面与其他固定大小的输入标头超卡类似。 变量大小的标头超call 还可以额外支持代表语义。 在这种情况下,rep 元素以平常的方式位于标头后面,但标头的总大小包括固定部分和可变部分。 所有其他规则保持不变,例如,第一个 rep 元素必须是 8 字节对齐。
XMM 快速 Hypercall 输入
在 x64 平台上,虚拟机监控程序支持使用 XMM 快速超卡,这允许一些超卡利用快速超卡接口的改进性能,即使它们需要两个以上的输入参数。 XMM 快速超集接口使用 6 个 XMM 寄存器来允许调用方传递最大大小为 112 字节的输入参数块。
XMM 快速超集接口的可用性通过“虚拟机监控程序功能标识”CPUID 叶 (0x40000003) 指示:
- 位 4:支持通过 XMM 寄存器传递 hypercall 输入。
请注意,有一个单独的标志指示对 XMM 快速输出的支持。 当虚拟机监控程序未指示可用性时,任何尝试使用此接口都将导致#UD错误。
仅) 注册映射 (输入
X64 | x86 | 提供的信息 |
---|---|---|
RCX | EDX:EAX | Hypercall 输入值 |
RDX | EBX:ECX | 输入参数块 |
R8 | EDI:ESI | 输入参数块 |
XMM0 | XMM0 | 输入参数块 |
XMM1 | XMM1 | 输入参数块 |
XMM2 | XMM2 | 输入参数块 |
XMM3 | XMM3 | 输入参数块 |
XMM4 | XMM4 | 输入参数块 |
XMM5 | XMM5 | 输入参数块 |
Hypercall 输入值与输入参数一起传入寄存器。 寄存器映射取决于调用方是在 32 位 (x86) 还是 64 位 (x64) 模式下运行。 虚拟机监控程序根据 EFER 的值确定调用方模式。LMA 和 CS.L. 如果设置了这两个标志,则调用方假定为 64 位调用方。 如果输入参数块小于 112 字节,则忽略寄存器中的任何额外字节。
Hypercall 输出
所有超calls 返回一个名为 hypercall 结果值的 64 位值。 格式如下:
字段 | Bits | 评论 |
---|---|---|
结果 | 15-0 | HV_STATUS 指示成功或失败的代码 |
Rsvd | 31-16 | 调用方应忽略这些位中的值 |
代表已完成 | 43-32 | 代表数成功完成 |
RsvdZ | 63-40 | 调用方应忽略这些位中的值 |
对于 rep hypercalls,代表完成字段是代表完成总数,而不是代表开始索引。 例如,如果调用方指定了代表开始索引为 5,而代表计数为 10,则代表完成字段将在成功完成时指示 10。
hypercall 结果值在寄存器中传回。 寄存器映射取决于调用方是在 32 位 (x86) 还是 64 (位 x64) 模式下运行, (请参阅上述) 。 Hypercall 输出的寄存器映射如下所示:
X64 | x86 | 提供的信息 |
---|---|---|
RAX | EDX:EAX | Hypercall 结果值 |
XMM 快速 Hypercall 输出
与虚拟机监控程序如何支持 XMM 快速超安装输入类似,可以共享相同的寄存器来返回输出。 这仅在 x64 平台上受支持。
通过 XMM 寄存器返回输出的功能通过“虚拟机监控程序功能标识”CPUID 叶 (0x40000003) 指示:
- 位 15:支持通过 XMM 寄存器返回超聚集输出。
请注意,有一个单独的标志指示对 XMM 快速输入的支持。 当虚拟机监控程序未指示可用性时,任何尝试使用此接口都将导致#UD错误。
(输入和输出) 注册映射
未用于传递输入参数的寄存器可用于返回输出。 换句话说,如果输入参数块小于 112 字节, (向上舍入到最接近的 16 字节对齐区块) ,剩余寄存器将返回超小输出。
x64 | 提供的信息 |
---|---|
RDX | 输入或输出块 |
R8 | 输入或输出块 |
XMM0 | 输入或输出块 |
XMM1 | 输入或输出块 |
XMM2 | 输入或输出块 |
XMM3 | 输入或输出块 |
XMM4 | 输入或输出块 |
XMM5 | 输入或输出块 |
例如,如果输入参数块的大小为 20 字节,虚拟机监控程序将忽略以下 12 个字节。 剩余的 80 个字节将包含超 ((如果适用) )。
易失性寄存器
Hypercalls 将仅在以下条件下修改指定的寄存器值:
- RAX (x64) 和 EDX:EAX (x86) 始终使用超总结果值和输出参数(如果有)覆盖。
- Rep hypercalls 将使用新的 rep start 索引修改 RCX (x64) 和 EDX:EAX (x86) 。
- HvCallSetVpRegisters 可以修改该 hypercall 支持的任何寄存器。
- RDX、R8 和 XMM0 到 XMM5(用于快速超高输入)保持未修改。 但是,可以修改用于快速超集输出的寄存器,包括 RDX、R8 和 XMM0 到 XMM5。 Hyper-V 将仅修改这些寄存器以用于快速超集输出,限制为 x64。
Hypercall 限制
Hypercalls 可能有与其关联的限制,以便他们执行其预期函数。 如果未满足所有限制,Hypercall 将终止并出现适当的错误。 如果适用,将列出以下限制:
- 调用分区必须具有特定特权
- 要处理的分区必须处于特定状态 (,例如“Active”)
Hypercall 状态代码
每个 hypercall 都记录为返回包含多个字段的输出值。 ) 类型的 HV_STATUS
状态值字段 (用于指示调用是成功还是失败。
失败的 Hypercalls 上的输出参数有效性
除非另有明确说明,否则,如果 hypercall 失败 (,则 hypercall 结果值的结果字段包含除) 以外的 HV_STATUS_SUCCESS
值时,所有输出参数的内容都是不确定的,不应由调用方检查。 仅当 hypercall 成功时,所有适当的输出参数才会包含有效的预期结果。
错误条件的排序
虚拟机监控程序检测到并报告错误条件的顺序是未定义的。 换句话说,如果存在多个错误,虚拟机监控程序必须选择要报告的错误条件。 应优先考虑提供更高的安全性的错误代码,目的是防止虚拟机监控程序向缺乏足够权限的调用方透露信息。 例如,状态代码是首选状态代码 HV_STATUS_ACCESS_DENIED
,它只会根据特权显示某些上下文或状态信息。
常见 Hypercall 状态代码
多个结果代码适用于所有超函数,因此不会单独记录每个超函数。 其中包括:
状态代码 | 添加状态 |
---|---|
HV_STATUS_SUCCESS |
调用成功。 |
HV_STATUS_INVALID_HYPERCALL_CODE |
无法识别 hypercall 代码。 |
HV_STATUS_INVALID_HYPERCALL_INPUT |
例如,代表计数不正确, (,将非零代表计数传递给非代表呼叫,或者零代表计数传递给代表调用) 。 |
代表开始索引不小于代表计数。 | |
指定的 hypercall 输入值中的保留位为非零。 | |
HV_STATUS_INVALID_ALIGNMENT |
指定的输入或输出 GPA 指针不与 8 个字节对齐。 |
指定的输入或输出参数列表跨越页面。 | |
输入或输出 GPA 指针不在 GPA 空间的边界内。 |
返回代码 HV_STATUS_SUCCESS
指示未检测到错误条件。
报告来宾 OS 标识
在分区中运行的来宾 OS 必须通过将其签名和版本写入 MSR () HV_X64_MSR_GUEST_OS_ID
,才能调用超卡,从而将其标识为虚拟机监控程序。 此 MSR 是分区范围的,在所有虚拟处理器之间共享。
此寄存器的值最初为零。 必须在启用 hypercall 代码页之前将非零值写入来宾 OS ID MSR, (请参阅 “建立 Hypercall 接口) ”。 如果此寄存器随后为零,将禁用 hypercall 代码页。
#define HV_X64_MSR_GUEST_OS_ID 0x40000000
专有操作系统的来宾 OS 标识
下面是此 MSR 的建议编码。 某些字段可能不适用于某些来宾 OS。
Bits | 字段 | 说明 |
---|---|---|
15:0 | 生成号 | 指示 OS 的内部版本号 |
23:16 | 服务版本 | 指示服务版本 (,例如“service pack”号) |
31:24 | 次要版本 | 指示操作系统的次要版本 |
39:32 | 主要版本 | 指示 OS 的主版本 |
47:40 | OS ID | 指示 OS 变体。 编码对供应商是唯一的。 Microsoft 操作系统编码如下:0=Undefined、1=MS-DOS®、2=Windows ® 3.x、3=® Windows 9x、4=Windows ® NT (和派生) ,5=Windows ® CE |
62:48 | 供应商 ID | 指示来宾 OS 供应商。 保留值 0。 请参阅下面的供应商列表。 |
63 | OS 类型 | 指示 OS 类型。 值为 0 表示专有的封闭源 OS。 值为 1 表示开放源代码 OS。 |
供应商值由 Microsoft 分配。 若要请求新供应商,请在GitHub虚拟化文档存储库 (https://aka.ms/VirtualizationDocumentationIssuesTLFS) 提交问题。
Vendor | 值 |
---|---|
Microsoft | 0x0001 |
HPE | 0x0002 |
LANCOM | 0x0200 |
开放源代码操作系统的来宾 OS 标识 MSR
以下编码作为开放源代码操作系统供应商打算符合此规范的指导。 建议开放源代码操作系统适应以下约定。
Bits | 字段 | 说明 |
---|---|---|
15:0 | 生成号 | 其他信息 |
47:16 | 版本 | 上游内核版本信息。 |
55:48 | OS ID | 其他供应商信息 |
62:56 | OS 类型 | OS 类型 (,例如 Linux、FreeBSD 等) 。 请参阅下面的已知 OS 类型列表 |
63 | 开放源 | 值为 1 表示开放源代码 OS。 |
OS 类型值由 Microsoft 分配。 若要请求新的 OS 类型,请在GitHub虚拟化文档存储库 (https://aka.ms/VirtualizationDocumentationIssuesTLFS) 提交问题。
OS 类型 | 值 |
---|---|
Linux | 0x1 |
FreeBSD | 0x2 |
Xen | 0x3 |
伊鲁莫斯 | 0x4 |
建立 Hypercall 接口
使用特殊操作码调用 Hypercalls。 由于此操作码在虚拟化实现之间有所不同,因此虚拟机监控程序必须抽象化此差异。 这是通过特殊的超高页面完成的。 此页面由虚拟机监控程序提供,显示在来宾的 GPA 空间中。 来宾需要通过编程来宾 Hypercall MSR 来指定页面的位置。
#define HV_X64_MSR_HYPERCALL 0x40000001
Bits | 说明 | 属性 |
---|---|---|
63:12 | Hypercall GPFN - 指示 hypercall 页面的来宾物理页码 | 读取/写入 |
11:2 | RsvdP。 读取时应忽略位,并在写入时保留。 | 保留 |
1 | “已锁定”。 指示 MSR 是否不可变。 如果已设置,则锁定此 MSR,从而阻止重定位 hypercall 页面。 设置后,只有系统重置可以清除位。 | 读取/写入 |
0 | 启用 hypercall 页面 | 读取/写入 |
Hypercall 页面可以放置在来宾的 GPA 空间中的任意位置,但必须页面对齐。 如果来宾尝试将 hypercall 页面移到 GPA 空间的边界之外,则写入 MSR 时将产生#GP错误。
此 MSR 是分区范围的 MSR。 换句话说,它由分区中的所有虚拟处理器共享。 如果一个虚拟处理器成功写入 MSR,另一个虚拟处理器将读取相同的值。
在启用 hypercall 页面之前,来宾 OS 必须通过将版本签名写入单独的 MSR (HV_X64_MSR_GUEST_OS_ID) 来报告其标识。 如果未指定来宾 OS 标识,则尝试启用 hypercall 将失败。 即使写入了启用位,启用位仍将为零。 此外,如果在启用 hypercall 页面后将来宾 OS 标识清除为零,则会禁用该标识。
Hypercall 页面显示为 GPA 空间的“覆盖”;也就是说,它涵盖映射到 GPA 范围的任何其他内容。 其内容可由来宾读取和可执行。 尝试写入 hypercall 页将导致保护 (#GP) 异常。 启用 hypercall 页面后,调用 hypercall 只需调用页面开头即可。
下面是有关建立 hypercall 页面的步骤的详细列表:
- 来宾读取 CPUID 叶 1,并通过检查寄存器 ECX 的位 31 来确定虚拟机监控程序是否存在。
- 来宾读取 CPUID 叶0x40000000以确定注册 EAX) 和 CPUID 叶0x40000001中返回的最大虚拟机监控程序 CPUID 叶 (,以确定在注册 EAX) 中返回 (的接口签名。 它验证最大叶值是否至少0x40000005,并且接口签名是否等于“Hv#1”。 此签名表示
HV_X64_MSR_GUEST_OS_ID
并HV_X64_MSR_HYPERCALL
HV_X64_MSR_VP_INDEX
实现。 - 如果注册为零,来宾会将其 OS 标识写入 MSR
HV_X64_MSR_GUEST_OS_ID
。 - 来宾读取 Hypercall MSR ()
HV_X64_MSR_HYPERCALL
。 - 来宾检查“启用 Hypercall 页面位”。 如果已设置,接口已处于活动状态,应省略步骤 6 和 7。
- 来宾在其 GPA 空间中找到一个页面,最好是 RAM、MMIO 等不占用的页面。 如果占用了页面,来宾应避免出于其他目的使用基础页面。
- 来宾将新值写入 Hypercall MSR ()
HV_X64_MSR_HYPERCALL
,其中包含步骤 6 中的 GPA,并将“启用 Hypercall 页面位”设置为启用接口。 - 来宾创建到 hypercall 页面 GPA 的可执行 VA 映射。
- 来宾会咨询 CPUID 叶0x40000003以确定哪些虚拟机监控程序设施可供其使用。 建立接口后,来宾可以启动 hypercall。 为此,它会根据 hypercall 协议填充寄存器,并向 hypercall 页面开头发出调用。 来宾应假定 hypercall 页面执行等效的接近返回 (0xC3) 以返回到调用方。 因此,必须使用有效的堆栈调用 hypercall。
扩展的 Hypercall 接口
具有上述调用代码的 Hypercalls 0x8000 称为扩展超卡。 扩展超卡使用与普通超卡相同的调用约定,并从来宾 VM 的角度显示相同的调用约定。 扩展 Hypercalls 在 Hyper-V 虚拟机监控程序中以不同的方式在内部处理。
可以使用 HvExtCallQueryCapabilities 查询扩展的 hypercall 功能。