<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Os on aarkegz 技术屋</title><link>https://aarkegz.com/os/</link><description>Recent content in Os on aarkegz 技术屋</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Wed, 18 Jun 2025 17:54:36 +0800</lastBuildDate><atom:link href="https://aarkegz.com/os/index.xml" rel="self" type="application/rss+xml"/><item><title>GICv3 基础知识与基于 GIC 部分直通的虚拟化中断支持</title><link>https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/</link><pubDate>Wed, 18 Jun 2025 17:54:36 +0800</pubDate><guid>https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/</guid><description>&lt;img src="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/20250421071610-4dd69bce-ieli-1280x720.png" alt="Featured image of post GICv3 基础知识与基于 GIC 部分直通的虚拟化中断支持" />&lt;h2 id="0-前言与基本概念">0. 前言与基本概念
&lt;/h2>&lt;h3 id="00-前言">0.0. 前言
&lt;/h3>&lt;p>GIC（Generic Interrupt Controller）是 ARM 架构中的中断控制器，负责处理中断管理、控制和分发等等。GIC 目前有四个版本，从 GICv1 到 GICv4，GICv3 是目前较为常用的版本。&lt;/p>
&lt;p>作者前段时间参考各种资料和 &lt;a class="link" href="https://github.com/syswonder/hVisor" target="_blank" rel="noopener"
>hVisor&lt;/a> 等项目的实现，实现了基于 GICv3 部分直通的虚拟化中断支持，可以在 Qemu 和 RK3588 上正常处理两个虚拟机的中断。在这个过程中也学到了不少关于 GIC 的知识，特记录在此，供后续参考。&lt;/p>
&lt;h3 id="01-gic-的简介和构成">0.1. GIC 的简介和构成
&lt;/h3>&lt;p>&lt;img src="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/gic-overall.png"
width="1046"
height="1088"
srcset="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/gic-overall_hu_c19660fef37ec742.png 480w, https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/gic-overall_hu_b97122a267cf993d.png 1024w"
loading="lazy"
alt="GICv3 整体的结构（来自 ARM 官方文档）"
class="gallery-image"
data-flex-grow="96"
data-flex-basis="230px"
>&lt;/p>
&lt;p>在逻辑上，GICv3 由以下几个主要组件构成：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Distributor（GICD）：全局共享一个的中断控制器，使用 MMIO 访问，负责处理设备中断（SPI）的分发和管理。GICD 维护着每一个设备中断的优先级、目标 PE 等等信息，并将中断分发给各个 GICR。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Redistributor（GICR）：每个 Cpu 核心（用术语来说是 PE，Processing Element）自身的中断控制器，负责处理从 GICD/ITS 分发来的中断、核间中断（SGI）以及 PE 私有的设备中断（PPI）。&lt;/p>
&lt;p>GICR 是 MMIO 访问的设备，需要注意每个 PE 的 GICR 是有自己独立的 MMIO 地址范围的。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Cpu Interface：每个 PE 内部的一套系统寄存器（CSR），命名为 &lt;code>ICC_*&lt;/code>，使用 &lt;code>MSR/MRS&lt;/code> 指令访问。用于处理中断的 ACK、EOI 等高频操作。&lt;/p>
&lt;p>GICv3 的 GICR 和 Cpu Interface 共同取代了 GICv2 中的 Cpu Interface（GICC）。GICv3 有可能出于向前兼容的目的保留 GICC 的访问接口，但程序应该尽量使用 GICv3 的标准接口。相比于使用 MMIO 的 GICC，使用 CSR 的新 Cpu Interface 在性能上有着优势。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Interrupt Translation Service（ITS）：接收基于消息的中断（Message-Signaled Interrupts, MSI），根据其设备 ID 和事件 ID 将 MSI 转换为另一种类型的设备中断（LPI），并将其分发给 GICR。&lt;/p>
&lt;p>ITS 是 GICv3 新增的组件，在 GICv2 中并不存在。一个系统中可能存在多个 ITS，每个 ITS 负责处理不同的设备中断。系统中也可能不存在 ITS，在这种情况下，LPI 将直接从设备发送到 GICR。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="02-中断的分类">0.2. 中断的分类
&lt;/h3>&lt;p>GICv3 将中断分为 4 个主要的类别，每个类别的中断有不同的路径，并且有不同的中断号范围：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>中断类别&lt;/th>
&lt;th>说明&lt;/th>
&lt;th>中断号范围&lt;/th>
&lt;th>路径&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>SGI&lt;/td>
&lt;td>Software Generated Interrupts；软件生成的核间中断&lt;/td>
&lt;td>0 – 15&lt;/td>
&lt;td>从发送者 GICR 到接收者 GICR&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PPI&lt;/td>
&lt;td>Private Peripheral Interrupts；PE 私有外设产生的中断，主要是时钟等&lt;/td>
&lt;td>16 – 31（扩展使用 1056 – 1119）&lt;/td>
&lt;td>从设备到 GICR&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SPI&lt;/td>
&lt;td>Shared Peripheral Interrupts；一般是普通非 PCIe 设备产生的中断&lt;/td>
&lt;td>32 – 1019 （扩展使用 4096 – 5119）&lt;/td>
&lt;td>从设备到 GICD 再到目标 GICR&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LPI&lt;/td>
&lt;td>Locality-specific Peripheral Interrupts；来自基于消息的中断，主要是 PCIe 设备产生的中断&lt;/td>
&lt;td>8191 –&lt;/td>
&lt;td>从设备到 ITS 再到目标 GICR&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>无论哪种类型的中断，最终都会被分发到 GICR 中进行处理。Cpu 响应这些中断的流程也是基本一致的。&lt;/p>
&lt;h3 id="03-affinity">0.3. Affinity
&lt;/h3>&lt;p>GICv3 相比于 GICv2 的另一项重要改进是引入了 Affinity 的概念。Affinity 是 PE 的一个唯一标识符，由 4 个 8 位整数组成，格式为 &lt;code>&amp;lt;Aff3&amp;gt;.&amp;lt;Aff2&amp;gt;.&amp;lt;Aff1&amp;gt;.&amp;lt;Aff0&amp;gt;&lt;/code>。较小的系统可能使用 &lt;code>0.0.0.x&lt;/code> 或者 &lt;code>0.0.x.0&lt;/code> 的编号，而较大的系统可能使用更加复杂的编号，例如 &lt;code>0.0.{0-3}.{0-3}&lt;/code> 甚至是 &lt;code>0.{1,3,5,7}.{0-3}.{0-3}&lt;/code>。不要假定 Affinity 的每一节都是连续的，也不要假定 Affinity 的每一节的含义和范围，这完全取决于芯片内部的设计。虽然相近的编号通常意味着 PE 位于同一个簇内，但具体的核心布局仍然需要参考芯片手册或者设备树中的信息。&lt;/p>
&lt;p>相比于只支持 8 个目标 ID 的 GICv2，GICv3 支持的 PE 数量大大增加，也允许更加灵活地配置中断的目标。&lt;/p>
&lt;h3 id="04-中断分组安全状态与-fiq">0.4. 中断分组、安全状态与 FIQ
&lt;/h3>&lt;p>中断分组、安全状态和 FIQ 是 GICv3 中相互交织的几个概念。虽然在实际使用中可以忽视，但是了解这些概念有助于更好地理解 GICv3 的设计，也能避免很多错误。&lt;/p>
&lt;p>安全状态（Security State）是 Aarch64 中引入的一个概念，是对异常等级（Exception Level, EL）的一种横向扩展，允许在同一 EL 中区分安全和非安全的状态。一般来说，实现了安全状态的 ARM 处理器会提供至少两个安全状态：Secure 和 Non-secure。Secure 状态下的代码可以访问 Secure 和 Non-secure 的地址空间，而 Non-secure 状态下的代码只能访问 Non-secure 的地址空间。EL0 到 EL2 都分为 Secure 和 Non-secure 两个状态，而 EL3 只存在 Secure 状态。&lt;/p>
&lt;p>&lt;img src="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/security-states.png"
width="1065"
height="435"
srcset="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/security-states_hu_5f3aa909ef21875a.png 480w, https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/security-states_hu_b3d5b76971253a43.png 1024w"
loading="lazy"
alt="ARM 安全状态（来自 ARM 官方文档）"
class="gallery-image"
data-flex-grow="244"
data-flex-basis="587px"
>&lt;/p>
&lt;p>对于 GICv3 来说，安全状态的概念主要体现在中断的分组上。在支持并开启了安全状态的系统中，GICv3 中的中断可以分为三个组别：&lt;/p>
&lt;ul>
&lt;li>Group 0：应该在 EL3 处理的中断。&lt;/li>
&lt;li>Group 1 Secure：应该在 Secure EL2 或 Secure EL1 处理的中断。&lt;/li>
&lt;li>Group 1 Non-secure：应该在 Non-secure EL2 或 Non-secure EL1 处理的中断。&lt;/li>
&lt;/ul>
&lt;p>每一个中断都可以被配置为这 3 个组别中的一个（除了 LPI，LPI 只能是 Group 1 Non-secure）。每一个组别在 Cpu Interface 中都有单独的一套 CSR（少数公用除外）。中断的组别决定了中断的处理路径，也决定了中断以 IRQ 的方式还是以 FIQ 的方式处理。&lt;/p>
&lt;p>FIQ（Fast Interrupt Request）原本是 ARM 处理器中的一个特殊的中断类型，具有更高的优先级和更快的响应时间，在物理上拥有独立于 IRQ 的信号线。在 Aarch64 中，FIQ 和 IRQ 已经没有本质上的区别，但仍然保留了独立的物理通路和单独的控制位，在异常向量表中也有单独的 FIQ 项。GICv3 利用 FIQ 和 IRQ 的区别来区分中断的安全状态。具体来说：&lt;/p>
&lt;ul>
&lt;li>当运行在 Secure EL3 时，Group 0/1S/1NS 的中断会以 FIQ 的方式通知。&lt;/li>
&lt;li>当运行在 Non-secure EL2/EL1/EL0 时，Group 1NS 的中断会以 IRQ 的方式通知，而 Group 0/1S 的中断会以 FIQ 的方式通知。&lt;/li>
&lt;li>当运行在 Secure EL2/EL1/EL0 时，Group 1S 的中断会以 IRQ 的方式通知，而 Group 0/1NS 的中断会以 FIQ 的方式通知。&lt;/li>
&lt;/ul>
&lt;p>也就是说，如果一个中断是应该在当前安全状态和异常等级处理的中断，那么它会以 IRQ 的方式通知；如果是应该在其他安全状态或者 EL3 处理的中断，那么它会以 FIQ 的方式通知。这样的好处是，通过配置 &lt;code>SCR_EL3&lt;/code> 系统寄存器，可以让 FIQ 进入 EL3 处理，而 IRQ 则进入 EL2/EL1 处理，这样可以实现 Secure/Non-secure 中断分离，同时不产生额外的异常等级切换。&lt;/p>
&lt;p>&lt;img src="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/interrupt-grouping-example.png"
width="2252"
height="916"
srcset="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/interrupt-grouping-example_hu_adee9809b2b0ca3f.png 480w, https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/interrupt-grouping-example_hu_48abd122f115187f.png 1024w"
loading="lazy"
alt="中断分组处理样例（来自 ARM 官方文档）"
class="gallery-image"
data-flex-grow="245"
data-flex-basis="590px"
>&lt;/p>
&lt;p>对于不支持安全状态（换而言之，只有一个安全状态）的系统，或者关闭了 GICv3 的安全状态支持（通过设置 &lt;code>GICD_CTLR&lt;/code> 寄存器的 &lt;code>DS&lt;/code> 位）的系统，GICv3 仍然可以正常工作。此时只剩下 2 个组别：Group 0 和 Group 1，并且它们的区别不再具有安全的意义，而是纯粹的分类。Group 0 的中断会以 FIQ 的方式通知，而 Group 1 的中断会以 IRQ 的方式通知。&lt;/p>
&lt;p>习惯上，不需要支持安全状态的系统应该将所有中断都配置为 Group 1。因为 FIQ 有可能被 EL3 保留自己使用（类似于 GICv2 中的情况），而 Group 1 的中断以 IRQ 的方式通知，可以正确地进入 EL2/EL1 处理。&lt;/p>
&lt;h3 id="05-中断的状态流转">0.5. 中断的状态流转
&lt;/h3>&lt;p>&lt;img src="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/interrupt-states.png"
width="2268"
height="1642"
srcset="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/interrupt-states_hu_f957e7a164afc2b.png 480w, https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/interrupt-states_hu_750184e472b2d6b5.png 1024w"
loading="lazy"
alt="中断的状态流转（来自 ARM 官方文档）"
class="gallery-image"
data-flex-grow="138"
data-flex-basis="331px"
>&lt;/p>
&lt;p>在 ARM 架构中，一个中断有 4 个可能的状态：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>状态&lt;/th>
&lt;th>描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Inactive&lt;/td>
&lt;td>中断未被触发，或者已经被处理完毕。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pending&lt;/td>
&lt;td>中断被触发，但还没有 PE 处理。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Active&lt;/td>
&lt;td>中断正在被 PE 处理。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Active and Pending&lt;/td>
&lt;td>中断正在被 PE 处理，另一个同样的中断已经被触发。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>LPI 只有 Pending 和 Inactive 两个状态，Active 和 Active and Pending 状态不适用于 LPI。&lt;/p>
&lt;h2 id="1-gicv3-的中断配置和处理流程">1. GICv3 的中断配置和处理流程
&lt;/h2>&lt;p>要正确实现 GICv3 的虚拟化，首先必然要了解 GICv3 的中断配置和处理流程。这一章将简略介绍操作系统启动时应该如何初始化 GICv3，如何配置中断，以及如何处理各种类型的中断。为了简便，这里以不启用安全状态支持的系统为例。&lt;/p>
&lt;h3 id="10-寻找当前-pe-的-gicr">1.0. 寻找当前 PE 的 GICR
&lt;/h3>&lt;p>在 GICv3 中，每个 PE 都有自己的 GICR，并且每个 GICR 都有自己独立的 MMIO 地址范围。因此，在访问当前核心的 GICR 之前，必须要找到当前 PE 的 GICR 地址。具体的做法是，每个 PE 都需要枚举系统中的每个 GICR，判断 &lt;code>GICR_TYPER&lt;/code> 寄存器中记录的 Affinity 是否和当前 PE 通过读取 &lt;code>MPIDR_EL1&lt;/code> 系统寄存器得到的 Affinity 相同，如果相同，则可以确定该 GICR 是当前 PE 的 GICR。&lt;/p>
&lt;p>每个 GICR 占用连续的 128KiB 地址空间（0x20000）；不同 PE 的 GICR 地址范围可能是一个或者多个连续的块；连续的两个 GICR 的基址差值可能等于 0x20000，也可能大于 0x20000从而产生空洞（例如为了兼容 GICv4 的 256KiB GICR 大小而选择 0x40000）。DTB 中的 &lt;code>gicv3&lt;/code> 节点会记录每块连续 GICR 地址范围的起始地址和大小，以及连续两个 GICR 的基址差值。同时，&lt;code>GICR_TYPER&lt;/code> 寄存器中也有一个 &lt;code>Last&lt;/code> 位，表示当前 GICR 是否是当前这块连续 GICR 地址范围的最后一个 GICR。有了这些信息，Cpu 就可以枚举 GICR 了，Rust 风格伪代码实例如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">fn&lt;/span> &lt;span style="color:#a6e22e">find_current_gicr_base&lt;/span>() -&amp;gt; &lt;span style="color:#66d9ef">usize&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 读取当前 PE 的 Affinity
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> affinity &lt;span style="color:#f92672">=&lt;/span> read_affinity_from_mpidr_el1();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 获取连续 GICR 的基址差值
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> stride &lt;span style="color:#f92672">=&lt;/span> gicr_stride();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 遍历每个 GICR 块
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> gicr_block_base &lt;span style="color:#66d9ef">in&lt;/span> gicr_blocks() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> &lt;span style="color:#66d9ef">mut&lt;/span> gicr_base &lt;span style="color:#f92672">=&lt;/span> gicr_block_base;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">loop&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 读取当前 GICR 的 GICR_TYPER 寄存器
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> typer &lt;span style="color:#f92672">=&lt;/span> read_gicr_typer(gicr_base);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 提取 GICR_TYPER 中的 Affinity
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> gicr_affinity &lt;span style="color:#f92672">=&lt;/span> extract_affinity_from_gicr_typer(typer);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> gicr_affinity &lt;span style="color:#f92672">==&lt;/span> affinity {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 找到了当前 PE 的 GICR
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">return&lt;/span> gicr_base;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> extract_last_from_gicr_typer(typer) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 到达当前 GICR 块的最后一个 GICR
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 移动到下一个 GICR
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> gicr_base &lt;span style="color:#f92672">+=&lt;/span> stride;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>实际上，大多数情况下，GICR 的地址范围是连续的，并且 GICR 之间的基址差值是 0x20000，在这种场景下只需要一个全局的 GICR 基址即可枚举出所有的 GICR。例如如果只想先在 Qemu 上运行，就可以先如此实现。&lt;/p>
&lt;h3 id="11-设置-gicd-控制寄存器">1.1. 设置 GICD 控制寄存器
&lt;/h3>&lt;p>GICD 控制寄存器（&lt;code>GICD_CTLR&lt;/code>）用于配置 GICD 的基本行为，其中需要关心的位有：&lt;/p>
&lt;ul>
&lt;li>控制 GICD 的安全状态支持的位（&lt;code>DS&lt;/code> 位）：置位这个位则 GICD 将关闭对安全状态的支持。&lt;/li>
&lt;li>控制 Affinity 路由的位（&lt;code>ARE_S&lt;/code>/&lt;code>ARE_NS&lt;/code> 位）：控制 GICD 使用 Affinity 还是旧有的目标 ID 进行中断路由。当支持安全状态时，Secure 和 Non-secure 状态有两个不同的位来控制 Affinity 路由。&lt;/li>
&lt;li>控制组别中断的位（&lt;code>EnableGrp0&lt;/code>/&lt;code>EnableGrp1S&lt;/code>/&lt;code>EnableGrp1NS&lt;/code> 位）：控制每一个组别中断的使能状态。每个组别中断的使能状态可以单独配置。&lt;/li>
&lt;/ul>
&lt;p>对于不需要支持安全状态的系统来说，应该置位 &lt;code>DS&lt;/code> 位（当硬件不支持多个安全状态时 &lt;code>DS&lt;/code> 位会被强制置位），并且置 &lt;code>EnableGrp1NS&lt;/code> 位使能 Group 1 中断（当然，也可以先不使能，而是将其作为控制中断的开关，不过使用 DAIF 寄存器来控制中断会更方便）。如果要启用 Affinity 路由（推荐），则还需要置 &lt;code>ARE_S&lt;/code> 位（注意在安全状态支持关闭时，控制 Affinity 路由的是 &lt;code>ARE_S&lt;/code> 位）。&lt;/p>
&lt;p>在修改完 &lt;code>GICD_CTLR&lt;/code> 寄存器后，应该等待 GICD 的状态同步（通过读取位于 &lt;code>GICD_CTLR&lt;/code> 寄存器第 31 位的 &lt;code>RWP&lt;/code> 位来判断，为 1 时表示 GICD 正在同步状态，0 时表示同步完成）。&lt;/p>
&lt;h3 id="12-通知-gicr-cpu-已启动">1.2. 通知 GICR Cpu 已启动
&lt;/h3>&lt;p>在启用高级电源管理功能时，有可能出现 Cpu 和 Cpu Interface 关闭，但 GICR 仍在运行的情况。为了避免 Bug，GICv3 规定在 Cpu 启动和关闭前都需要通知 GICR。具体来说：&lt;/p>
&lt;ul>
&lt;li>在 Cpu 启动时，应该将 &lt;code>GICR_WAKER&lt;/code> 寄存器的 &lt;code>ProcessorSleep&lt;/code> 位复位，表示当前 Cpu 已经启动。然后需要等待 &lt;code>GICR_WAKER&lt;/code> 寄存器的 &lt;code>ChildrenAsleep&lt;/code> 位变为 0。&lt;/li>
&lt;li>在 Cpu 关闭时，应该将 &lt;code>GICR_WAKER&lt;/code> 寄存器的 &lt;code>ProcessorSleep&lt;/code> 位置位，表示当前 Cpu 已经关闭。然后需要等待 &lt;code>GICR_WAKER&lt;/code> 寄存器的 &lt;code>ChildrenAsleep&lt;/code> 位变为 1。&lt;/li>
&lt;/ul>
&lt;p>显然，这里应该复位 &lt;code>ProcessorSleep&lt;/code> 位，然后等待 &lt;code>ChildrenAsleep&lt;/code> 位变为 0。&lt;/p>
&lt;h3 id="13-配置-sgippi-和-spi-中断">1.3. 配置 SGI、PPI 和 SPI 中断
&lt;/h3>&lt;p>接下来需要通过 GICR 和 GICD 配置 SGI、PPI 和 SPI 中断。LPI 中断的配置则在后文中单独介绍。&lt;/p>
&lt;h4 id="130-概述">1.3.0. 概述
&lt;/h4>&lt;p>对 SGI、PPI 和 SPI 中断的配置都是通过读写 GICD 和 GICR 中的寄存器来完成的。下面所述的寄存器（如无特殊说明）在 GICR 和 GICD 中都存在，并且功能和定义都基本一致；GICR 的寄存器用于控制 PPI 和 SGI 中断，GICD 的寄存器用于控制 SPI 中断。这里需要配置的项目包括：&lt;/p>
&lt;h4 id="131-打开关闭中断">1.3.1. 打开/关闭中断
&lt;/h4>&lt;p>通过 &lt;code>ISENABLER&lt;/code> 和 &lt;code>ICENABLER&lt;/code> 寄存器来打开和关闭中断。&lt;/p>
&lt;p>每个中断在这一对寄存器中都有一对对应的位，置位 &lt;code>ISENABLER&lt;/code> 中的对应位打开中断，置位 &lt;code>ICENABLER&lt;/code> 中的对应位关闭中断。&lt;/p>
&lt;h4 id="132-清除-pending-位">1.3.2. 清除 Pending 位
&lt;/h4>&lt;p>通过 &lt;code>ICPENDR&lt;/code> 寄存器来清除中断的 Pending 位。&lt;/p>
&lt;p>这是由于，在硬件复位时，每个中断的 Pending 位的值是未知的，因此在初始化时需要手动清除 Pending 位。对于每个中断都置位 &lt;code>ICPENDR&lt;/code> 的对应位即可。&lt;/p>
&lt;h4 id="133-配置分组">1.3.3. 配置分组
&lt;/h4>&lt;p>通过 &lt;code>IGROUPR&lt;/code> 和 &lt;code>IGRPMODR&lt;/code> 寄存器来配置中断的分组。&lt;/p>
&lt;p>在不启用安全状态支持的情况下，需要将所有中断都配置为 Group 1，因此需要将 &lt;code>IGROUPR&lt;/code> 的所有位都置为 1。&lt;code>IGRPMODR&lt;/code> 寄存器只有在启用安全状态支持的情况下才有意义，这时可以忽略。&lt;/p>
&lt;h4 id="134-配置触发方式">1.3.4. 配置触发方式
&lt;/h4>&lt;p>通过 &lt;code>ICFGR&lt;/code> 寄存器来配置每一个中断的触发方式，包括边沿触发和电平触发。&lt;/p>
&lt;p>在物理平台中，一个中断应有的触发方式取决于发送中断的物理设备的设计，DTB 中会记录这个信息，操作系统应该按照 DTB 中的信息来配置。&lt;/p>
&lt;h4 id="135-配置优先级">1.3.5. 配置优先级
&lt;/h4>&lt;p>通过 &lt;code>IPRIORITYR&lt;/code> 寄存器来设置每一个中断的优先级。优先级的范围是 0 到 255，0 是最高优先级，255 是最低优先级。&lt;/p>
&lt;p>需要注意具体的 GICv3 实现可能并不支持全部 256 个值，而是可能仅支持 2、4、8 或者 16 的倍数（亦即 128、64、32 或 16 个优先级）。GICv3 规定了至少支持 16 个优先级，因此在设置优先级时，应该优先使用 16 的倍数 0x00 ~ 0xF0。当前平台具体支持的优先级数量可以通过 &lt;code>ICC_CTLR_EL1&lt;/code> 寄存器的 &lt;code>PRIBits&lt;/code> 字段来查询。&lt;/p>
&lt;h4 id="136-设置目标-pe">1.3.6. 设置目标 PE
&lt;/h4>&lt;p>SGI 和 PPI 中断不需要设置目标 PE，因为它们是私有的中断，只会发送给当前 PE。如果没有特殊情况，SPI 中断的目标 PE 也不需要设置，因为它们会被 GICD 自动分发到某一个 GICR。只有在需要将 SPI 中断发送到特定的 GICR 时，才需要通过 &lt;code>GICD_ITARGETSR&lt;/code>/&lt;code>GICD_IROUTER&lt;/code> 寄存器来设置目标 PE，前者适用于未开启 Affinity 路由的情况，后者适用于开启 Affinity 路由的情况；前者使用用掩码指定目标 PE，后者则使用目标 PE 的 Affinity 值。&lt;/p>
&lt;h3 id="14-启用系统寄存器接口关闭-bypass">1.4. 启用系统寄存器接口、关闭 Bypass
&lt;/h3>&lt;p>在完成 GICD 和 GICR 的配置后，需要启用 GICv3 Cpu Interface 的系统寄存器接口（&lt;code>ICC_*&lt;/code> 寄存器），并关闭 FIQ 和 IRQ 的 Bypass 功能。&lt;/p>
&lt;p>FIQ 和 IRQ 的 Bypass 功能指的是绕过 GICD 和 GICR 内部对中断按组的和单独的使能控制，而直接将 FIQ 和 IRQ 的信号输入到处理器中。关闭 Bypass 功能可以确保 GICv3 的中断控制器能够正确地处理中断。Bypass 功能是可选的，显式地关闭 Bypass 功能可以确保 GICv3 的行为符合预期。&lt;/p>
&lt;p>以上两者都可以通过当前 EL 的 &lt;code>ICC_SRE_ELx&lt;/code> 寄存器来完成，将其 &lt;code>SRE&lt;/code> 位置位可以启用系统寄存器接口，将 &lt;code>DIB&lt;/code> 和 &lt;code>DFB&lt;/code> 位置位可以关闭 FIQ 和 IRQ 的 Bypass 功能。&lt;/p>
&lt;h3 id="15-启用中断">1.5. 启用中断
&lt;/h3>&lt;p>在完成以上步骤后，就可以启用中断了。在 GICv3 中，能够控制中断的有（作者已经发现的）：&lt;/p>
&lt;ul>
&lt;li>GICD/GICR 的 &lt;code>ISENABLER&lt;/code>/&lt;code>ICENABLER&lt;/code> 寄存器：用于打开和关闭单个中断。上面已经设置过了。&lt;/li>
&lt;li>&lt;code>GICD_CTLR&lt;/code> 寄存器的 &lt;code>EnableGrp0/1S/1NS&lt;/code> 位：用于打开和关闭中断组别。上面已经设置过了。&lt;/li>
&lt;li>&lt;code>ICC_IGRPEN0_EL1&lt;/code>/&lt;code>ICC_IGRPEN1_EL1&lt;/code> 寄存器：同样用于打开和关闭中断组别。要启用中断，必须将对应寄存器设置为 1。&lt;/li>
&lt;li>&lt;code>ICC_PMR_EL1&lt;/code> 寄存器：控制着当前 PE 的优先级屏蔽（Priority Mask），只有优先级高于该寄存器值的中断才能被处理。一般来说，应该将其设置为 0xFF（最低优先级），以允许所有中断。&lt;/li>
&lt;li>&lt;code>DAIF&lt;/code> 寄存器：用于控制中断的使能状态。复位 &lt;code>F&lt;/code> 位可以使能 FIQ，复位 &lt;code>I&lt;/code> 位可以使能 IRQ。对于操作系统，推荐使用 &lt;code>DAIF&lt;/code> 寄存器作为全局的中断控制开关。&lt;/li>
&lt;/ul>
&lt;p>按照上文的流程，这里应该将 &lt;code>ICC_IGRPEN1_EL1&lt;/code> 寄存器设置为 1，然后将 &lt;code>ICC_PMR_EL1&lt;/code> 寄存器设置为合适的值（例如 0xFF，表明允许所有中断），最后复位 &lt;code>DAIF&lt;/code> 寄存器的 &lt;code>I&lt;/code> 位来使能 IRQ。此时，中断就可以进入到 Cpu 当中被处理了。&lt;/p>
&lt;h3 id="16-中断处理流程">1.6. 中断处理流程
&lt;/h3>&lt;p>当中断到来时，Cpu 会进入到中断向量表中的对应入口。在上面的配置下，应该会进入到 Current EL 或者 Lower EL 的 IRQ 项之中。当进入中断处理例程后，操作系统应该从 GICR 的 &lt;code>ICC_IAR1_EL1&lt;/code> 寄存器中读取当前中断的 ID，这个读取的操作也向 GICv3 表示当前中断已经被响应（ACK）。然后，操作系统可以根据中断 ID 来判断当前中断的类型和来源，并进行相应的处理。当处理结束后，操作系统应该将中断 ID 写入到 GICR 的 &lt;code>ICC_EOIR1_EL1&lt;/code> 寄存器中，表示当前中断已经处理完毕（EOI），然后从中断处理例程中返回。&lt;/p>
&lt;h3 id="17-配置-lpi-中断">1.7. 配置 LPI 中断
&lt;/h3>&lt;p>LPI（Locality-specific Peripheral Interrupts）是 GICv3 中新增的一种中断类型，主要用于处理基于消息的中断（MSI），例如 PCIe 设备产生的中断。LPI 中断的处理和配置与 SGI、PPI 和 SPI 中断有所不同，并且相对而言更加复杂。这一节将介绍 LPI 中断的配置和处理流程，以及 ITS 的相关内容。&lt;/p>
&lt;h4 id="170-lpi-和-its-的内存区域">1.7.0. LPI 和 ITS 的内存区域
&lt;/h4>&lt;p>由于 LPI 中断数量可能非常高，具体数量也不确定，因此很多具体的 LPI 配置和状态信息无法像其他中断一样直接放到 GICD/GICR 的寄存器中，或是 ITS 的寄存器（&lt;code>GITS_*&lt;/code>）中，而是要放在操作系统分配的普通内存区域中，再由操作系统将该区域的地址写入到对应的寄存器中供 GICv3 使用。&lt;/p>
&lt;p>具体来说，需要分配的内存区域有：&lt;/p>
&lt;ul>
&lt;li>LPI 相关的区域：
&lt;ul>
&lt;li>LPI 配置表（LPI Configuration Table）：存储每个 LPI 的配置信息，包括优先级和控制位。&lt;/li>
&lt;li>LPI 等待表（LPI Pending Table）：存储每个 LPI 的 Pending 状态。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>ITS 相关的区域：
&lt;ul>
&lt;li>设备表（Device Table）：保存设备 ID 和中断翻译表的对应关系。&lt;/li>
&lt;li>中断翻译表（Interrupt Translation Table）：保存事件 ID 和中断号以及中断集合号的对应关系。&lt;/li>
&lt;li>中断集合表（Interrupt Collection Table）：保存中断集合号和目标 GICR 的对应关系。ITS 本身可能能够存储一定大小的中断集合表，因此在某些情况下，可能不需要分配额外的内存区域给中断集合表。&lt;/li>
&lt;li>ITS 命令队列（ITS Command Queue）：保存 ITS 命令的环形队列。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>以上内存区域的用法和配置方式会在后文中详细介绍。&lt;/p>
&lt;h4 id="171-its-的工作流程">1.7.1. ITS 的工作流程
&lt;/h4>&lt;p>&lt;img src="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/its-workflow.png"
width="1582"
height="892"
srcset="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/its-workflow_hu_e30ce3e436bbdb70.png 480w, https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/its-workflow_hu_3e67dafe7811b0a1.png 1024w"
loading="lazy"
alt="ITS 的工作流程（来自 ARM 官方文档）"
class="gallery-image"
data-flex-grow="177"
data-flex-basis="425px"
>&lt;/p>
&lt;p>如上文所说，ITS 是通过设备表、中断翻译表和中断集合表将 MSI 翻译为 LPI 的。ITS 的工作流程可以参考上图（最上方经过「vPE table」的是 GICv4 中引入的 LPI 虚拟化流程，可以忽略；最下方从中断源直接到 GICR 的是没有 ITS 的情况，也可以忽略），具体来说：&lt;/p>
&lt;ol>
&lt;li>设备发送 MSI 到 ITS。ITS 得到设备 ID 和事件 ID。&lt;/li>
&lt;li>ITS 根据设备 ID 在设备表中查找对应的中断翻译表地址。&lt;/li>
&lt;li>ITS 根据事件 ID 在上一步找到的中断翻译表中查找对应的中断号和中断集合号。&lt;/li>
&lt;li>ITS 根据中断集合号在中断集合表中查找对应的目标 GICR。&lt;/li>
&lt;li>ITS 将对应的中断号发送给目标 GICR。&lt;/li>
&lt;li>GICR 根据 LPI 配置表中的信息，处理 LPI。&lt;/li>
&lt;/ol>
&lt;p>需要注意的是，设备表、中断翻译表和中断集合表的格式都是实现定义的，也就是说操作系统无法直接访问这些表，只能通过向 ITS 发送命令的方式来配置这些表中存储的映射关系。&lt;/p>
&lt;h4 id="172-its-命令队列">1.7.2. ITS 命令队列
&lt;/h4>&lt;p>由于 ITS 能够接受的命令数量众多，通过普通的寄存器实现配置不太现实，因此 ITS 使用命令队列来接收和处理命令。ITS 命令队列是一个环形队列，放置在 64KiB 对齐的连续内存页中，每条命令占据 32 字节。操作系统申请了对应的内存后，需要将 ITS 命令队列的基址和大小写入到 ITS 的 &lt;code>GITS_BASER&lt;/code> 寄存器中。ITS 命令队列有两个指针：一个是写指针（&lt;code>GITS_CWRITER&lt;/code> 寄存器），操作系统向命令队列写入了命令之后，将写指针置于最后一条命令的后一个位置；另一个是读指针（&lt;code>GITS_CREADER&lt;/code> 寄存器），ITS 处理完一条命令之后，将读指针置于下一条命令的位置。操作系统通过检查读指针是否追上写指针即可判断 ITS 是否已经处理完了所有命令。&lt;/p>
&lt;p>&lt;img src="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/its-cmd-queue.png"
width="1536"
height="1094"
srcset="https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/its-cmd-queue_hu_77466cceac3d3628.png 480w, https://aarkegz.com/os/arm/gic-v3-and-its-partial-passthrough/images/its-cmd-queue_hu_f5ee7f179269ef94.png 1024w"
loading="lazy"
alt="ITS 命令队列（来自 ARM 官方文档）"
class="gallery-image"
data-flex-grow="140"
data-flex-basis="336px"
>&lt;/p>
&lt;p>关于 ITS 命令的格式和具体的命令内容，可以参考 ARM 官方文档。这里简单介绍一些常用的 ITS 命令：&lt;/p>
&lt;ul>
&lt;li>&lt;code>MAPD DeviceID, ITT_addr, Size&lt;/code>：将设备 ID 映射到中断翻译表地址。&lt;code>ITT_addr&lt;/code> 是中断翻译表的地址，&lt;code>Size&lt;/code> 代表了事件 ID 的范围。&lt;/li>
&lt;li>&lt;code>MAPTI DeviceID, EventID, pINTID, ICID&lt;/code>：将设备 ID 和事件 ID 映射到中断号和中断集合号。&lt;code>pINTID&lt;/code> 是中断号，&lt;code>ICID&lt;/code> 是中断集合号。&lt;/li>
&lt;li>&lt;code>MAPI DeviceID, EventID, ICID&lt;/code>：将设备 ID 和事件 ID 映射到中断集合号，使用 EventID 作为中断号。等价于 &lt;code>MAPTI DeviceID, EventID, EventID, ICID&lt;/code>。&lt;/li>
&lt;li>&lt;code>MAPC ICID, RDbase&lt;/code>：指定中断集合号和目标 GICR 的映射关系。&lt;code>RDbase&lt;/code> 是目标 GICR 的基址。&lt;/li>
&lt;li>&lt;code>INV DeviceID, EventID/INVALL ICID&lt;/code>：确保对应的缓存和 LPI 配置表中的内容一致。&lt;/li>
&lt;li>&lt;code>SYNC RDbase&lt;/code>：确保和目标 GICR 有关的 ITS 命令的效果已经是全局可见的。&lt;/li>
&lt;/ul>
&lt;h4 id="173-设置设备表和中断集合表">1.7.3. 设置设备表和中断集合表
&lt;/h4>&lt;p>设备表和中断集合表（以及这里不会介绍的 GICv4 引入的虚拟 PE 表）都属于 ITS 表（ITS Tables）的一种。ITS 提供了 8 个寄存器（&lt;code>GITS_BASER0&lt;/code> ~ &lt;code>GITS_BASER7&lt;/code>）来配置 ITS 表的基址和大小。每个 &lt;code>GITS_BASER&amp;lt;n&amp;gt;&lt;/code> 寄存器中有两个只读的字段，分别表示这个寄存器对应的表类型和每一项的大小。根据表项大小和中断数量，操作系统可以计算出表需要的空间，并分配对应大小的连续内存页，并且将分配的内存页的基址和大小写入到对应的 &lt;code>GITS_BASER&amp;lt;n&amp;gt;&lt;/code> 寄存器中。&lt;/p>
&lt;p>约定上，设备表使用 &lt;code>GITS_BASER0&lt;/code> 寄存器；中断集合表（如果需要）使用 &lt;code>GITS_BASER1&lt;/code> 寄存器。在配置之前，操作系统必须清零表中所有字节。完成配置之后，操作系统不需要（也不应该）手动修改设备表和中断集合表的内容，而是通过 ITS 命令来配置。&lt;/p>
&lt;p>设备表和中断集合表是每个 ITS 一份的，如果系统中有多个 ITS，那么每个 ITS 都需要分配自己的设备表和中断集合表。&lt;/p>
&lt;h4 id="174-设置中断翻译表">1.7.4. 设置中断翻译表
&lt;/h4>&lt;p>操作系统应该为每个连接到 ITS 上的设备申请一个中断翻译表，每个设备的中断翻译表的大小取决于设备的事件 ID 的数量以及 &lt;code>GITS_TYPER&lt;/code> 寄存器中记录的每个事件 ID 占用的字节数。中断翻译表并不是通过 GITS 的寄存器来配置的，而是由操作系统通过 ITS 的 &lt;code>MAPD&lt;/code> 命令将设备 ID 映射到中断翻译表的地址。&lt;/p>
&lt;p>和设备表以及中断集合表一样，在配置之前，操作系统必须清零中断翻译表中的所有字节；在配置之后，操作系统不需要（也不应该）手动修改中断翻译表的内容，而是通过 ITS 命令来配置。&lt;/p>
&lt;h4 id="175-设置-lpi-配置表">1.7.5. 设置 LPI 配置表
&lt;/h4>&lt;p>LPI 配置表是一个连续的 4KiB 对齐的内存区域。其具体大小取决于 LPI 的数量，每个 LPI 占用 1 字节，其中高 6 位代表其优先级的高 6 位（优先级的低 2 位固定为 0，也就是说 LPI 的优先级必然是 4 的倍数），最低位代表着 LPI 的使能状态（0 表示禁用，1 表示启用）。&lt;/p>
&lt;p>LPI 配置表的地址需要写入到 &lt;code>GICR_PROPBASER&lt;/code> 寄存器中。LPI 配置表是全局的，可能有硬件允许为不同的 GICR 设置不同的 LPI 配置表，但通常情况下，所有 GICR 都使用同一个 LPI 配置表。操作系统可以直接修改 LPI 配置表中的内容来配置 LPI 的优先级和使能状态。修改之后需要通过 ITS 的 INV/INVALL 命令来处理状态和缓存。&lt;/p>
&lt;p>在没有 ITS 的情况下，LPI 配置表依然存在。&lt;/p>
&lt;h4 id="176-设置-lpi-等待表">1.7.6. 设置 LPI 等待表
&lt;/h4>&lt;p>LPI 等待表是一个连续的 64KiB 对齐的内存区域。其具体大小取决于 LPI 的数量，每个 LPI 占用 1 个比特位，表示该 LPI 是否处于 Pending 状态。&lt;/p>
&lt;p>LPI 等待表的地址需要写入到 &lt;code>GICR_PENDBASER&lt;/code> 寄存器中。LPI 等待表是每个 GICR 独立的，操作系统需要为每个 GICR 分配独立的 LPI 等待表。操作系统不应该直接修改 LPI 等待表中的内容，而应该让 GICR 全权处理。&lt;/p>
&lt;p>和 LPI 配置表一样，LPI 等待表在没有 ITS 的情况下依然存在。&lt;/p>
&lt;h4 id="177-启用-its">1.7.7. 启用 ITS
&lt;/h4>&lt;p>在完成 ITS 的配置后，需要启用 ITS。可以通过设置 &lt;code>GITS_CTLR&lt;/code> 寄存器的 &lt;code>Enable&lt;/code> 位来启用 ITS。启用 ITS 后，ITS 将开始处理来自设备的 MSI，并将其转换为 LPI。&lt;/p>
&lt;h2 id="2-基于-gicv3-的部分直通实现虚拟化">2. 基于 GICv3 的部分直通实现虚拟化
&lt;/h2>&lt;h3 id="20-如何实现-gicv3-虚拟化">2.0. 如何实现 GICv3 虚拟化？
&lt;/h3>&lt;p>GIC 提供了对 Cpu Interface 的硬件辅助虚拟化支持（包括 GICv2 的 &lt;code>GICC_*&lt;/code> 寄存器和 GICv3 的 &lt;code>ICC_*&lt;/code> 寄存器；通过 &lt;code>GICH/GICV_*&lt;/code> 和 &lt;code>ICH/ICV_*&lt;/code> 寄存器实现）。然而，Distributor、Redistributor 和 ITS 是没有硬件辅助虚拟化的支持的；如果要实现虚拟化，必须要通过软件进行模拟。根据 ARM 的官方规范文档实现一个纯模拟的 GICv3 并非不可能，但实现起来非常复杂且性能可能有一定的问题。&lt;/p>
&lt;p>如果仅需要运行一个 Guest，Hypervisor 完全可以将 Distributor、Redistributor 和 ITS 的访问接口直通给它，让 Guest OS 直接控制硬件 GIC，这样是可以正常工作的。但是如果需要运行多个 Guest，数个 Guest OS 对 Distributor 和 ITS 的访问就会产生冲突。如果能够想办法让不同 Guest OS 对 GICv3 的访问互不干扰，就可以实现 GICv3 的分区直通，这就是 GIC 部分直通（并非此文作者提出）方案的思路。&lt;/p>
&lt;p>具体来说，当有如下保证时：&lt;/p>
&lt;ul>
&lt;li>每个物理 Cpu 只归属于一个 Guest。&lt;/li>
&lt;li>每个 Guest 的 GPA 空间和 HPA 空间都是无偏移的对等映射。&lt;/li>
&lt;/ul>
&lt;p>Hypervisor 就可以将 Guest 对 GICv3 的访问转发给物理 GICv3，仅做一些较小的拦截和修改：&lt;/p>
&lt;ul>
&lt;li>Cpu Interface：直接允许 Guest 访问 Cpu Interface 的 CSR（&lt;code>ICC_*&lt;/code>）。&lt;/li>
&lt;li>Redistributor：允许 Guest 访问 Redistributor 的大部分寄存器。只拦截对 &lt;code>GICR_PROPBASER&lt;/code> 的访问。&lt;/li>
&lt;li>Distributor：对 Guest 的访问进行筛查，允许 Guest 访问 Distributor 寄存器中它拥有的中断号所对应的部分。&lt;/li>
&lt;li>ITS：拦截 Guest 对 ITS 的访问，对 Guest 向 ITS 发送的命令进行转发。&lt;/li>
&lt;/ul>
&lt;h3 id="21-cpu-interface-的部分直通">2.1. Cpu Interface 的部分直通
&lt;/h3>&lt;p>由于 Cpu Interface 是每个 PE 独立的，因此在虚拟化时可以直接允许 Guest 访问 Cpu Interface 的 CSR（&lt;code>ICC_*&lt;/code>）。注意这里不是让 Guest 访问硬件辅助虚拟化所提供的 &lt;code>ICV_*&lt;/code> 寄存器，而是直接访问硬件 GICv3 的 &lt;code>ICC_*&lt;/code> 寄存器。为此需要在 &lt;code>HCR_EL2&lt;/code> 寄存器中关闭 IRQ 虚拟化（复位 &lt;code>IMO&lt;/code> 位），并且不要设置 &lt;code>ICH_HCR_EL2&lt;/code> 寄存器的 &lt;code>En&lt;/code> 位。&lt;/p>
&lt;h3 id="22-redistributor-的部分直通">2.2. Redistributor 的部分直通
&lt;/h3>&lt;p>Redistributor 同样是每个 PE 独立的，因此同样也可以直接允许 Guest 访问 Redistributor 的大部分寄存器。需要特殊处理的寄存器有 2 个：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;code>GICR_PROPBASER&lt;/code>：因为所有 GICR 的 &lt;code>GICR_PROPBASER&lt;/code> 寄存器应该都指向同一个 LPI 配置表，所以 Guest 不应该修改这个寄存器的值。Hypervisor 应该在初始化时自己分配 LPI 配置表并写入到每个 GICR 的 &lt;code>GICR_PROPBASER&lt;/code> 寄存器中。&lt;/p>
&lt;p>然而，这样的做法会导致 Guest 无法修改 LPI 的优先级和使能状态。为了解决这个问题，Hypervisor 可以有两种做法：&lt;/p>
&lt;ol>
&lt;li>正确但麻烦的做法：在 Guest 设置 &lt;code>GICR_PROPBASER&lt;/code> 寄存器时，Hypervisor 取消掉 LPI 配置表所在内存页的映射。这样，Guest 对它的 LPI 配置表的修改会引发一个错误，Hypervisor 也就可以将 Guest 的修改同步到自己的 LPI 配置表中。&lt;/li>
&lt;li>偷懒的做法：在 Guest 设置 &lt;code>GICR_PROPBASER&lt;/code> 寄存器时，Hypervisor 直接忽略这个设置。当 Guest 访问 &lt;code>GICR_INVLPIR&lt;/code> 寄存器（当 ITS 不存在时用来同步对 LPI 配置表的修改的寄存器）或者发送 &lt;code>MAPI&lt;/code>/&lt;code>MAPTI&lt;/code> 命令时，Hypervisor 认为 Guest 一定是要启用对应的 LPI，因此将这个中断号在自己的 LPI 配置表中设置为使能状态。这样的做法显而易见地不正确，但是实践证明可以正确地运行包括 Linux 在内的 Guest OS。&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>
&lt;p>&lt;code>GICR_TYPER&lt;/code>：具体地说，是 &lt;code>GICR_TYPER&lt;/code> 寄存器中的 &lt;code>Last&lt;/code> 位。&lt;code>Last&lt;/code> 位表示当前 GICR 是否是连续 GICR 地址范围的最后一个 GICR。如果一个 Guest 的 PE 对应的是物理上的前几个 GICR，那么它就无法正确地停止枚举 GICR。为了解决这个问题，Hypervisor 应该拦截 Guest 对 &lt;code>GICR_TYPER&lt;/code> 寄存器的访问，在 Guest 自己的最后一个 GICR 中将 &lt;code>Last&lt;/code> 位置为 1。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>对于其他的寄存器，Hypervisor 可以直接将 Guest 的 MMIO 访问转发给物理 GICR。&lt;/p>
&lt;h3 id="23-distributor-的部分直通">2.3. Distributor 的部分直通
&lt;/h3>&lt;p>Distributor 的情况比 Cpu Interface 和 Redistributor 要复杂一些。由于 Distributor 是全局的，所有 PE 都共享同一个 Distributor，因此 Hypervisor 需要对 Guest 对 Distributor 的访问进行筛查，具体来说：&lt;/p>
&lt;ul>
&lt;li>Hypervisor 应该保存一份 SPI 分配表，负责记录每一个 SPI 归属于哪一个 Guest。&lt;/li>
&lt;li>对于 &lt;code>IIDR&lt;/code>/&lt;code>TYPER&lt;/code>/&lt;code>TYPER2&lt;/code> 等等只读的信息寄存器，Hypervisor 可以直接将 Guest 的 MMIO 访问转发给物理 GICD。&lt;/li>
&lt;li>对于 &lt;code>CTLR&lt;/code> 控制寄存器，Hypervisor 应该阻止 Guest 的写操作，只允许读取。&lt;/li>
&lt;li>对于其他寄存器，Hypervisor 应该根据寄存器的偏移量来判断 Guest 试图访问的中断号，结合 SPI 分配表来判断 Guest 是否有权访问。特别是如果访问的是每个中断仅占用不到一个字节的寄存器，需要计算出 Guest 可以访问哪些位，然后通过掩码来过滤掉 Guest 不允许访问的位。&lt;/li>
&lt;/ul>
&lt;h3 id="24-its-的部分直通">2.4. ITS 的部分直通
&lt;/h3>&lt;p>在 GICv3 的所有组件中，ITS 的部分直通是最复杂的。原因在于，ITS 的命令队列、设备表和中断集合表都是操作系统分配的普通内存区域，而不是 GICv3 的寄存器。因此，多个 Guest OS 可能会多次尝试设置这些数据结构的地址，导致冲突。另外，多个 Guest OS 同时向 ITS 发送命令也会导致冲突。为了解决这些问题，Hypervisor 需要拦截 Guest 对 ITS 的全部操作，然后向物理 ITS 发出新的功能上等价的命令。&lt;/p>
&lt;h4 id="240-内存区域处理">2.4.0. 内存区域处理
&lt;/h4>&lt;p>正如上面所说，Hypervisor 不能让 Guest 为 ITS 分配命令队列，设备表和中断集合表。因此，Hypervisor 必须自行分配这些内存区域，并将它们的地址写入到 ITS 的寄存器中，然后阻止 Guest 对 &lt;code>GITS_BASER&amp;lt;n&amp;gt;&lt;/code> 和 &lt;code>GITS_CBASER&lt;/code> 寄存器的写操作。由于操作系统无需也不能直接访问设备表和中断集合表，忽略操作系统自己分配的内存区域是安全的；不过需要记录下 Guest 写入的 &lt;code>GITS_CBASER&lt;/code> 寄存器的地址，以便在 ITS 命令转发时使用。&lt;/p>
&lt;p>至于中断翻译表，Hypervisor 可以允许 Guest 为每个设备分配自己的中断翻译表，因为中断翻译表是和设备 ID 绑定的，而设备 ID 又是硬件指定而非操作系统可控制的，所以只要不让同一个物理设备同时出现在两个 Guest 的 DTB 中，就可以避免 Guest 之间的冲突。&lt;/p>
&lt;h4 id="241-its-命令转发">2.4.1. ITS 命令转发
&lt;/h4>&lt;p>Hypervisor 需要向 Guest 提供虚拟的 &lt;code>GITS_CBASER&lt;/code>、&lt;code>GITS_CWRITER&lt;/code> 和 &lt;code>GITS_CREADER&lt;/code> 寄存器。Guest 向虚拟 &lt;code>GITS_CWRITER&lt;/code> 写入值，说明 Guest 向 ITS 命令队列中写入了新的命令；此时 Hypervisor 可以通过虚拟 &lt;code>GITS_WRITER&lt;/code> 和虚拟 &lt;code>GITS_READER&lt;/code> 的差值来判断 Guest 写入了多少条命令，然后从 Guest 自行配置的命令队列（虚拟 &lt;code>GITS_CBASER&lt;/code> 指向的 Guest 物理地址）中读取这些命令，然后将命令转发给物理 ITS。Hypervisor 应该等待物理 ITS 处理完所有命令后，更新虚拟 &lt;code>GITS_CREADER&lt;/code> 的值为虚拟 &lt;code>GITS_CWRITER&lt;/code> 的值，表示虚拟 ITS 已经处理完了所有命令。&lt;/p>
&lt;p>首先需要注意的是，在以上过程中，应该用锁来避免多个 PE 同时访问物理 ITS 的命令队列，否则仍然会导致冲突。另一点需要注意的是，虽然设备 ID 和事件 ID 不会产生冲突，但是中断集合号是由 Guest OS 自行分配的，因此是可能产生冲突的（例如 Guest A 和 Guest B 都使用了中断集合号 0，分别指向自己的 0 号 GICR）。Hypervisor 应该在转发 ITS 命令时，检查中断集合号是否和其他 Guest 的中断集合号冲突，如果冲突，则需要将中断集合号转换为一个新的中断集合号，并更新 ITS 的中断集合表。&lt;/p>
&lt;h2 id="3-参考资料">3. 参考资料
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://developer.arm.com/documentation/ddi0487/latest/" target="_blank" rel="noopener"
>Arm Architecture Reference Manual for A-profile architecture&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://developer.arm.com/documentation/ihi0069/latest/" target="_blank" rel="noopener"
>Arm Generic Interrupt Controller Architecture Specification GIC architecture version 3 and version 4&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://developer.arm.com/documentation/ihi0048/latest/" target="_blank" rel="noopener"
>Arm® Generic Interrupt Controller Architecture version 2.0&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://developer.arm.com/documentation/198123/0302/" target="_blank" rel="noopener"
>Learn the architecture - Generic Interrupt Controller v3 and v4, Overview&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://developer.arm.com/documentation/102923/0100/" target="_blank" rel="noopener"
>Learn the architecture - Generic Interrupt Controller v3 and v4, LPIs&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://developer.arm.com/documentation/107627/0102/" target="_blank" rel="noopener"
>Learn the architecture - Generic Interrupt Controller v3 and v4, Virtualization&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://wiki.osdev.org/Generic_Interrupt_Controller_versions_3_and_4" target="_blank" rel="noopener"
>Generic Interrupt Controller versions 3 and 4 - OSDev Wiki&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/syswonder/hvisor/tree/dev" target="_blank" rel="noopener"
>syswonder/hVisor @ GitHub&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/arceos-hypervisor/arm_vgic" target="_blank" rel="noopener"
>arceos-hypervisor/arm_vgic @ GitHub&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Intel VMX 场景下的 EPT 与 TLB</title><link>https://aarkegz.com/os/x86/ept-and-tlb-in-intel-vmx/</link><pubDate>Tue, 15 Apr 2025 21:32:16 +0800</pubDate><guid>https://aarkegz.com/os/x86/ept-and-tlb-in-intel-vmx/</guid><description>&lt;img src="https://aarkegz.com/os/x86/ept-and-tlb-in-intel-vmx/20250421070216-8f7c0269-fnqg-1280x720.png" alt="Featured image of post Intel VMX 场景下的 EPT 与 TLB" />&lt;h2 id="问题ept-和-vmfunc会产生-tlb-错误吗">问题：EPT 和 VMFUNC，会产生 TLB 错误吗？
&lt;/h2>&lt;p>写这篇短文的原因是，最近我在尝试用 &lt;code>VMFUNC&lt;/code> 指令做一些工作。&lt;code>VMFUNC&lt;/code> 指令，简单来说，允许不经过 VM Exit，直接在 Guest 的 Ring 3 中切换 EPTP（Extended-Page-Table Pointer，EPT 指针）；当然，为了安全，Guest Ring 3 只能在 Hypervisor 设定好的一系列 EPTP 中选择一个；利用这条指令我们可以实现高效的上下文切换，从而使 VMX 能被用到更广阔的场景中，如进程隔离，安全容器等。&lt;/p>
&lt;p>然而这里有一个潜在的问题：在切换 EPTP 前后，同样的 GPA（Guest Physical Address，Guest 物理地址）可能会被映射到不同的 HPA（Host Physical Address，Host 物理地址），而且切换 EPTP 虽然不会修改 CR3 寄存器，但可能导致 CR3 指向不同的 Host 物理地址，这也就意味着 GVA（Guest Virtual Address，Guest 虚拟地址）到 GPA 之间的映射关系也可能会发生变化。这样一来，TLB 中的条目会如何呢？是会自动失效，还是需要手动失效？如果需要手动处理，是需要使用 &lt;code>INVEPT&lt;/code> 指令，还是可以使用 &lt;code>INVLPG&lt;/code> 指令？更关键的是，在 TLB 中，存储的到底是哪些映射，是 GVA 到 GPA，GPA 到 HPA，还是 GVA 到 HPA？&lt;code>VMFUNC&lt;/code> 前后，哪些会受到影响？&lt;/p>
&lt;p>能够回答这些问题的，只有一份文件：SDM（Software Developer&amp;rsquo;s Manual）。很幸运，SDM 中有这个问题的详细解答，也就是这篇文章的内容。&lt;/p>
&lt;p>（基于 2025 年 3 月版 SDM，其他版本章节序号可能有出入）&lt;/p>
&lt;h2 id="探索vmx-和-tlb">探索：VMX 和 TLB
&lt;/h2>&lt;h3 id="3-类-tlb">3 类 TLB
&lt;/h3>&lt;p>在 SDM Vol. 3C 30.4 Caching Translation Information 一节中，Intel 详细描述了 TLB 和 VMX 的交互方式。在和 VMX 有关的场景下，TLB 可以被分为 3 类：&lt;/p>
&lt;ul>
&lt;li>Linear mappings&lt;/li>
&lt;li>Guest-physical mappings&lt;/li>
&lt;li>Combined mappings&lt;/li>
&lt;/ul>
&lt;p>当 EPT 关闭时，不存在 GPA 和 HPA 的区别。此时，处理器只会创建和使用 Linear mappings，它存储的是 GVA 到物理地址的映射信息，关联的是 Guest 的页表。&lt;/p>
&lt;p>当 EPT 开启时，处理器只会创建和使用 Guest-physical mappings 和 Combined mappings。Guest-physical mappings 存储的是 GPA 到 HPA 的映射信息，关联的是 EPT 页表；Combined mappings 则存储 GVA 到 HPA 的映射信息，关联的是 Guest 页表和 EPT 页表。&lt;/p>
&lt;p>得到了这个信息，我们就可以回答上面倒数第二个问题了：TLB 中不会存储 GVA 到 GPA 的映射信息，会存储 GVA 和 GPA 到 HPA 的映射信息。由于使用 &lt;code>VMFUNC&lt;/code> 时 EPT 必然开启，因此 TLB 中必然包含 Guest-physical mappings 和 Combined mappings 两类。&lt;/p>
&lt;p>不过，剩下的问题还是没有得到解答，在通过 &lt;code>VMFUNC&lt;/code> 切换 EPTP 时，TLB 中的条目会如何呢？SDM Vol. 3C 30.1 Virtual Processor Identifiers (VPIDS) 中有着进一步的线索。&lt;/p>
&lt;h3 id="pcid-与-vpid">PCID 与 VPID
&lt;/h3>&lt;p>熟悉 TLB 的同学应当知道，TLB 中的项是可以和 PCID（Process Context ID，进程上下文 ID）关联的，这样在切换进程时就无需失效 TLB 中的所有项了，只需要修改 PCID 即可。&lt;/p>
&lt;p>同样地，VMX 也提供了 VPID（Virtual Process ID，虚拟进程 ID）。VPID 是 VMCS 中的一个字段，可以用以标识不同的虚拟 CPU。在上面说到的三类 TLB 项中：&lt;/p>
&lt;ul>
&lt;li>Linear mappings 和 PCID 以及 VPID 关联；&lt;/li>
&lt;li>Guest-physical mappings 和 EPTRTA（EPT Root Table Address，即 EPTP 中 EPT 基址部分）关联；&lt;/li>
&lt;li>Combined mappings 同时和 PCID、VPID 以及 EPTRTA 关联。&lt;/li>
&lt;/ul>
&lt;p>通过以上的设计可以推测，Intel 期望中的虚拟机内存结构是：&lt;/p>
&lt;ul>
&lt;li>单个虚拟机的多个的虚拟 CPU 共享同样的虚拟物理地址空间，即拥有同样的 EPTP；这样，不同的虚拟 CPU 之间可以共享 TLB 中的 Guest-physical mappings，因为它只与 EPTRTA 关联；&lt;/li>
&lt;li>单个虚拟机的多个的虚拟 CPU 之间通过 VPID 进行区分；虚拟机内部的进程通过 PCID 进行区分；这样，Linear mappings 和 Combined mappings 可以区分不同的虚拟 CPU 和进程，对于虚拟机中的进程和来说，TLB 的行为和物理机是一样的。&lt;/li>
&lt;/ul>
&lt;p>知道了 TLB 与 VPID 和 EPTRTA 的关系，上面的问题就有了答案：&lt;code>VMFUNC&lt;/code> 会切换 EPTP，所以 EPTRTA 必然会发生变化；而在开启了 EPT 的时候，TLB 中可能存在的 Guest-physical mappings 和 Combined mappings 都和 EPTRTA 关联；因此在 &lt;code>VMFUNC&lt;/code> 后，TLB 中的原有条目并不会被命中，也就不需要手动失效了。&lt;/p>
&lt;h3 id="剩余的细节vmfunc-的文档怎么说">剩余的细节：&lt;code>VMFUNC&lt;/code> 的文档怎么说
&lt;/h3>&lt;p>行至此处，我们似乎漏掉了一件重要的事情：&lt;code>VMFUNC&lt;/code> 指令的文档还没有出场呢。那中间会不会有着什么重要的细节呢？在 SDM Vol. 3C 27.5.6.3 EPTP Switching 中，果然有着一条和 TLB 相关的说明：&lt;/p>
&lt;p>如果 VMCS 中没有开启 VPID，&lt;code>VMFUNC&lt;/code> 指令会失效 TLB 中所有和 VPID 0（代表 VPID 未启用）相关的 Combined mappings，无论其 PCID 和 EPTRTA 是什么。&lt;/p>
&lt;p>我推测背后的原因是，如果 VPID 未开启的话，Combined mappings 相当于只与 PCID 和 EPTRTA 关联；如果虚拟机中的进程被迁移到另一个虚拟 CPU 上，但那个虚拟 CPU 又恰巧调度到了当前的物理 CPU 上，那么 Combined mappings 中的条目就会被命中；这个行为是不符合物理机的行为的，因此 Intel 选择了失效所有的 Combined mappings。至于 Guest-physical mappings 本身就是在不同的虚拟 CPU 之间共享的，因此不需要失效。至于这个推测是否正确，尚且不得而知。&lt;/p>
&lt;h2 id="答案无需忧心但需要小心">答案：无需忧心，但需要小心
&lt;/h2>&lt;p>经历了难忘的文档阅读后，我们终于得到了一个结论：&lt;/p>
&lt;ul>
&lt;li>使用 &lt;code>VMFUNC&lt;/code> 切换 EPTP 时，并不需要特意操心 TLB 条目的问题；
&lt;ul>
&lt;li>这是因为，&lt;code>VMFUNC&lt;/code> 一定会导致 EPTRTA 的变化，而 EPT 开启时，TLB 中的条目一定是和 EPTRTA 关联的；&lt;/li>
&lt;li>因此，&lt;code>VMFUNC&lt;/code> 后，TLB 中原有的条目不会被命中；&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>但是，如果要避免 &lt;code>VMFUNC&lt;/code> 指令失效 TLB 中的 Combined mappings，导致潜在的性能损失，则需要开启 VPID；&lt;/li>
&lt;/ul></description></item><item><title>Multiboot 极简示例</title><link>https://aarkegz.com/os/boot/multiboot-minimal-example/</link><pubDate>Thu, 03 Apr 2025 21:04:36 +0800</pubDate><guid>https://aarkegz.com/os/boot/multiboot-minimal-example/</guid><description>&lt;img src="https://aarkegz.com/os/boot/multiboot-minimal-example/20250421070146-b0ff6c6e-dfyr-1280x720.png" alt="Featured image of post Multiboot 极简示例" />&lt;h2 id="什么是-multiboot">什么是 Multiboot？
&lt;/h2>&lt;p>Multiboot 规范是一个开源的引导加载程序（bootloader）接口标准，它定义了操作系统内核和 bootloader 之间的交互方式。简单来说，如果你的内核符合 Multiboot 规范，那么它就可以：&lt;/p>
&lt;ul>
&lt;li>被任何 Multiboot 兼容的 bootloader（主要是 GRUB）引导，也能被 QEMU 加载；&lt;/li>
&lt;li>跳过实模式的初始化过程，直接进入保护模式执行；&lt;/li>
&lt;li>从 bootloader 处接收一些关于硬件环境的信息，例如内存布局等；&lt;/li>
&lt;/ul>
&lt;p>对于内核开发者来说，Multiboot 规范简化了内核的引导过程，将一部分无趣又繁琐的工作交给了 bootloader 来完成，这对于专注于内核开发本身，减少重复造轮子是有益的。同时，Multiboot 规范也使内核更容易与 bootloader 兼容。因此，在编写内核时，遵循 Multiboot 规范是一个不错的选择。&lt;/p>
&lt;p>这篇文章不仅详细介绍了 Multiboot 规范的基本内容，还给出了一个符合 Multiboot 规范的极简示例——启动后向串口打印「Hello, World!」，随后关机——整个示例不计注释仅包含约 40 行汇编代码，并可以在 QEMU 中运行。&lt;/p>
&lt;h2 id="如何使用-multiboot">如何使用 Multiboot？
&lt;/h2>&lt;h3 id="如何编写符合-multiboot-规范的内核">如何编写符合 Multiboot 规范的内核？
&lt;/h3>&lt;p>Multiboot 规范的内容可以在&lt;a class="link" href="https://www.gnu.org/software/grub/manual/multiboot/" target="_blank" rel="noopener"
>这里&lt;/a>找到。简单来说，要使内核符合 Multiboot 规范，需要在内核二进制文件的前 8192 字节中包含一个 Multiboot header，其格式如下：&lt;/p>
&lt;p>首先是 3 个必选的字段：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>偏移&lt;/th>
&lt;th>大小&lt;/th>
&lt;th>名称&lt;/th>
&lt;th>描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0x00&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>magic&lt;/code>&lt;/td>
&lt;td>魔数，必须是 &lt;code>0x1BADB002&lt;/code>，bootloader 通过这个值寻找 Multiboot header。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x04&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>flags&lt;/code>&lt;/td>
&lt;td>标志位，表示内核希望 bootloader 提供的功能。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x08&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>checksum&lt;/code>&lt;/td>
&lt;td>校验和，要求 &lt;code>magic + flags + checksum == 0&lt;/code>。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>其中 &lt;code>flags&lt;/code> 字段的定义如下：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>位&lt;/th>
&lt;th>描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>要求 bootloader 将所有 boot module 加载到 4KiB 对齐的地址上。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>要求 bootloader 提供内存布局信息。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>要求 bootloader 提供显示输出信息。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3 ~ 15&lt;/td>
&lt;td>保留。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>要求 bootloader 根据 Multiboot header 中提供的地址加载内核到内存。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>17 ~ 31&lt;/td>
&lt;td>保留。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>flags&lt;/code> 的第 0、1、2 位和此处的示例无关，这里不展开介绍；第 3 ~ 15 位和第 17 ~ 31 位保留，内核中这些位的值必须为 0；这里只讲述第 16 位：如果第 16 位被设置，bootloader 会将内核加载到 Multiboot header 中以下字段指定的地址上（如果第 16 位没有被设置，则内核二进制文件必须是自身包含了加载地址信息的 ELF 格式，bootloader 会根据 ELF 文件中的信息来加载内核），这些字段只有在第 16 位被设置时才会被使用：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>偏移&lt;/th>
&lt;th>大小&lt;/th>
&lt;th>名称&lt;/th>
&lt;th>描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0x0c&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>header_addr&lt;/code>&lt;/td>
&lt;td>内核被加载到内存后，Multiboot header 的物理地址。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x10&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>load_addr&lt;/code>&lt;/td>
&lt;td>内核被加载到内存后，内核的起始物理地址。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x14&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>load_end_addr&lt;/code>&lt;/td>
&lt;td>内核被加载到内存后，内核的结束物理地址。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x18&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>bss_end_addr&lt;/code>&lt;/td>
&lt;td>内核被加载到内存后，BSS 段的结束物理地址。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x1c&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>entry_addr&lt;/code>&lt;/td>
&lt;td>内核的入口物理地址。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>header_addr&lt;/code> 决定了内核的加载位置。加载完成后，位于 Multiboot header 开头的魔数会位于 &lt;code>header_addr&lt;/code> 指定的地址上。通过这个地址，就可以间接确定内核的加载位置。&lt;/p>
&lt;p>不难发现，&lt;code>header_addr&lt;/code> 这一个字段就足以确定内核的加载位置了，那为什么还需要 &lt;code>load_addr&lt;/code> 和 &lt;code>load_end_addr&lt;/code> 呢？这是因为 bootloader 并不一定要将内核二进制文件的所有内容都加载到内存中（例如对于一个 ELF 格式的内核二进制文件，加载代码和数据等几个段就足够了，其余的段和 ELF Header 等可以不加载）。&lt;code>load_addr&lt;/code> 和 &lt;code>load_end_addr&lt;/code> 正是圈定了内核二进制文件中需要加载的部分。详细地说：&lt;/p>
&lt;ul>
&lt;li>&lt;code>load_addr&lt;/code> 决定了内核加载的起始位置：bootloader 会从 Multiboot header 前 &lt;code>header_addr - load_addr&lt;/code> 字节处开始加载；&lt;/li>
&lt;li>&lt;code>load_end_addr&lt;/code> 决定了内核加载的结束位置：bootloader 会到 Multiboot header 后 &lt;code>load_end_addr - header_addr&lt;/code> 字节处结束加载；如果 &lt;code>load_end_addr&lt;/code> 为 0，则表示加载到二进制文件的末尾；&lt;/li>
&lt;li>也就是说，bootloader 一共会加载 &lt;code>load_end_addr - load_addr&lt;/code> 字节到物理内存的 &lt;code>[load_addr, load_end_addr)&lt;/code> 区间；并确保 Multiboot header 的魔数位于 &lt;code>header_addr&lt;/code> 处。&lt;/li>
&lt;/ul>
&lt;p>&lt;code>bss_end_addr&lt;/code> 则决定了 BSS 段的结束位置，bootloader 会将 &lt;code>[load_end_addr, bss_end_addr)&lt;/code> 区间的内存清零，内核可以使用这部分内存作为 BSS 段。如果不需要 BSS 段，可以将 &lt;code>bss_end_addr&lt;/code> 设置为 0。&lt;/p>
&lt;p>&lt;code>entry_addr&lt;/code> 决定了内核的入口地址，加载完成后，bootloader 会将跳转到这个地址，将控制权交给内核。&lt;/p>
&lt;p>Multiboot header 最后还有 4 个字段是和 &lt;code>flags&lt;/code> 中的第 2 位相关的，不置位第 2 位的情况下，可以忽略它们。这里只列出了它们的偏移和大小：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>偏移&lt;/th>
&lt;th>大小&lt;/th>
&lt;th>名称&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0x20&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>mode_type&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x24&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>width&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x28&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>height&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x2c&lt;/td>
&lt;td>4&lt;/td>
&lt;td>&lt;code>depth&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>有了符合要求的 Multiboot header 之后，内核就可以被兼容的 bootloader 引导了。&lt;/p>
&lt;h3 id="从-bootloader-到内核">从 bootloader 到内核
&lt;/h3>&lt;p>Multiboot 规范也规定了控制权交给内核后，整个系统的状态。简而言之，bootloader 会将系统初始化为一个最简的 32 位保护模式状态，内核可以运行，但需要自己完成进一步的初始化工作。具体来说，Multiboot 规范给出了如下保证：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>项目&lt;/th>
&lt;th>状态&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>EAX&lt;/code>&lt;/td>
&lt;td>包含魔数 &lt;code>0x2BADB002&lt;/code>，表示是由 Multiboot 规范引导的。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>EBX&lt;/code>&lt;/td>
&lt;td>包含 Multiboot 信息表的物理地址。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ESP&lt;/code>&lt;/td>
&lt;td>&lt;strong>无效值&lt;/strong>，内核需要自己设置栈指针。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>其他通用寄存器&lt;/td>
&lt;td>&lt;strong>无效值&lt;/strong>。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>CS&lt;/code>&lt;/td>
&lt;td>一个 32 位，可读可执行的段，基址为 0，大小为 4 GiB。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DS&lt;/code>~&lt;code>SS&lt;/code>&lt;/td>
&lt;td>一个 32 位，可读可写的段，基址为 0，大小为 4 GiB。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>GDTR&lt;/code>&lt;/td>
&lt;td>&lt;strong>无效值&lt;/strong>，即使以上的段是保证有效的，GDT 本身也可能是无效的，内核需要自己设置 GDT。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>CR0&lt;/code>&lt;/td>
&lt;td>&lt;code>PG&lt;/code> 复位，&lt;code>PE&lt;/code> 置位。即保护模式打开，但分页关闭。其他位不确定。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>EFLAGS&lt;/code>&lt;/td>
&lt;td>&lt;code>VM&lt;/code> 复位，&lt;code>IF&lt;/code> 复位。即虚拟 8086 模式关闭，中断关闭。其他位不确定。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>IDTR&lt;/code>&lt;/td>
&lt;td>&lt;strong>无效值&lt;/strong>，内核需要自己设置 IDT。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A20 门&lt;/td>
&lt;td>已打开。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="编写极简的-multiboot-示例">编写极简的 Multiboot 示例
&lt;/h2>&lt;h3 id="编码">编码
&lt;/h3>&lt;p>有了以上的知识储备，我们就可以开始编写一个符合 Multiboot 规范的极简内核了。为了简便起见，我们全程使用汇编语言，进入内核后不初始化栈，也不读取 Multiboot 信息表。仅仅向串口打印「Hello, World!」，然后通过 QEMU 的方式（向 0x604 端口写入 0x2000）关机。&lt;/p>
&lt;p>首先，我们需要一个 Multiboot header：我们将 &lt;code>flags&lt;/code> 的第 16 位置位，为了保留未来的扩展性，我们也置位第 1 位，即使我们并不读取内存信息；由于没有 BSS 段，我们将 &lt;code>bss_end_addr&lt;/code> 设置为 0；由于整个二进制文件都需要加载，我们将 &lt;code>load_end_addr&lt;/code> 设置为 0；&lt;code>header_addr&lt;/code> 设置为 Multiboot header 自身的地址，入口设置在 &lt;code>_start&lt;/code> 标签处：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-asm" data-lang="asm">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.att_syntax&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.equ&lt;/span> &lt;span style="color:#66d9ef">MULTIBOOT_MAGIC&lt;/span>, &lt;span style="color:#ae81ff">0x1BADB002&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.equ&lt;/span> &lt;span style="color:#66d9ef">MULTIBOOT_FLAGS&lt;/span>, &lt;span style="color:#ae81ff">0x00010002&lt;/span> &lt;span style="color:#75715e"># bit 1 (meminfo) and bit 16 (load address header fields)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#a6e22e">.equ&lt;/span> &lt;span style="color:#66d9ef">MULTIBOOT_CHECKSUM&lt;/span>, -(&lt;span style="color:#66d9ef">MULTIBOOT_MAGIC&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">+&lt;/span> &lt;span style="color:#66d9ef">MULTIBOOT_FLAGS&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># The entry point of the kernel
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#a6e22e">.section&lt;/span> &lt;span style="color:#66d9ef">.text.boot&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.code32&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.global&lt;/span> &lt;span style="color:#66d9ef">_start&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>_start:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">mov&lt;/span> %eax, %edi &lt;span style="color:#75715e"># The multiboot magic number
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">mov&lt;/span> %ebx, %esi &lt;span style="color:#75715e"># The multiboot information structure
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">jmp&lt;/span> &lt;span style="color:#66d9ef">entry32&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.balign&lt;/span> &lt;span style="color:#ae81ff">4&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.type&lt;/span> &lt;span style="color:#66d9ef">multiboot_header&lt;/span>, &lt;span style="color:#a6e22e">@object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>multiboot_header:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">.long&lt;/span> &lt;span style="color:#66d9ef">MULTIBOOT_MAGIC&lt;/span> &lt;span style="color:#75715e"># The magic number
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">.long&lt;/span> &lt;span style="color:#66d9ef">MULTIBOOT_FLAGS&lt;/span> &lt;span style="color:#75715e"># The flags
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">.long&lt;/span> &lt;span style="color:#66d9ef">MULTIBOOT_CHECKSUM&lt;/span> &lt;span style="color:#75715e"># The checksum
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">.long&lt;/span> &lt;span style="color:#66d9ef">multiboot_header&lt;/span> &lt;span style="color:#75715e"># The header address, the linear address where the magic number should be loaded
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">.long&lt;/span> &lt;span style="color:#66d9ef">_start&lt;/span> &lt;span style="color:#75715e"># The start address of the kernel image, same as _start in this demo
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">.long&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># The end address of the data segment, 0 means the end of the kernel image
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">.long&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e"># The end address of the bss segment, 0 means no bss segment
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">.long&lt;/span> &lt;span style="color:#66d9ef">_start&lt;/span> &lt;span style="color:#75715e"># The entry point of the kernel
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e"># There may be other fields here if we set the bit 2 of the flags, we can safely ignore them here
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.global&lt;/span> &lt;span style="color:#66d9ef">entry32&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>entry32:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">hlt&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们将 &lt;code>_start&lt;/code> 标签放在整个内核的起始位置，这样可以为不同的加载方式保留最好的兼容性。同时由于 Multiboot header 需要尽量靠近起始位置，所以我们最小化了 &lt;code>_start&lt;/code> 的大小，以 &lt;code>entry32&lt;/code> 标签作为真正的代码入口，让 &lt;code>_start&lt;/code> 仅仅负责跳转到 &lt;code>entry32&lt;/code>；&lt;code>multiboot_header&lt;/code> 则紧紧跟在 &lt;code>_start&lt;/code> 后面。&lt;/p>
&lt;blockquote>
&lt;p>这里将两个参数放入 &lt;code>%edi&lt;/code> 和 &lt;code>%esi&lt;/code> 则是遵循了 System V 规范，如果在此基础上扩展出一个 64 位内核，则可以让 C/C++/Rust 等语言的函数以参数的形式接收这两个参数，当然，那时需要修改以下串口输出的部分。&lt;/p>&lt;/blockquote>
&lt;p>接下来，我们需要编写 &lt;code>entry32&lt;/code> 的代码：输出「Hello, World!」，然后关机：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-asm" data-lang="asm">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.global&lt;/span> &lt;span style="color:#66d9ef">entry32&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>entry32:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Print &amp;#34;Hello, World!&amp;#34; to the serial port COM1 (0x3F8)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">mov&lt;/span> &lt;span style="color:#66d9ef">$0x3F8&lt;/span>, %dx &lt;span style="color:#75715e"># The serial port COM1
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">mov&lt;/span> &lt;span style="color:#66d9ef">$message&lt;/span>, %esi &lt;span style="color:#75715e"># The message to print
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">mov&lt;/span> &lt;span style="color:#66d9ef">$&lt;/span>(&lt;span style="color:#66d9ef">message_end&lt;/span> - &lt;span style="color:#66d9ef">message&lt;/span>), %ecx &lt;span style="color:#75715e"># The length of the message
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">rep&lt;/span> &lt;span style="color:#a6e22e">outsb&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Shutdown QEMU
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">mov&lt;/span> &lt;span style="color:#66d9ef">$0x604&lt;/span>, %dx
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">mov&lt;/span> &lt;span style="color:#66d9ef">$0x2000&lt;/span>, %ax
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">out&lt;/span> %ax, (%dx)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">hlt&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.section&lt;/span> &lt;span style="color:#66d9ef">.data.boot&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.type&lt;/span> &lt;span style="color:#66d9ef">message&lt;/span>, &lt;span style="color:#a6e22e">@object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>message:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">.asciz&lt;/span> &lt;span style="color:#e6db74">&amp;#34;\nHello, World!\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>message_end:
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里使用了很罕见的 &lt;code>rep&lt;/code> 前缀搭配 &lt;code>outsb&lt;/code> 指令来输出字符串。&lt;code>outsb&lt;/code> 指令会将 &lt;code>%ds:%esi&lt;/code> 指向的内存中的字节输出到 &lt;code>%dx&lt;/code> 指向的端口中。&lt;code>rep&lt;/code> 前缀则会重复执行 &lt;code>outsb&lt;/code> 指令，每次将 &lt;code>%ecx&lt;/code> 中的字节数减 1，&lt;code>%esi&lt;/code> 的值加 1，直到 &lt;code>%ecx&lt;/code> 中的字节数减为 0 为止。这样就可以一次性输出整个字符串了。最后通过向 0x604 端口写入 0x2000 来关机，这个方法是 QEMU 特有的。&lt;/p>
&lt;h3 id="汇编和链接">汇编和链接
&lt;/h3>&lt;p>我们直接使用 GCC 工具链完成汇编和链接的工作。首先，我们将以上的代码保存为 &lt;code>multiboot.asm&lt;/code>，并使用 &lt;code>as&lt;/code> 汇编成目标文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>$ as -o multiboot.o multiboot.asm
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>随后，我们使用 &lt;code>ld&lt;/code> 链接成 ELF 格式的可执行文件，这里需要准备一份链接脚本：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-ld" data-lang="ld">OUTPUT_ARCH(i386:x86-64)
BASE_ADDRESS = 0x100000;
ENTRY(_start)
SECTIONS {
. = BASE_ADDRESS;
.text : ALIGN(0x1000) {
*(.text.boot)
}
.data : {
*(.data.boot)
}
}
&lt;/code>&lt;/pre>&lt;p>即使全程使用 32 位环境，我们也使用了 64 位的 ELF 格式，这也是为了方便能在此基础上扩展成完整的内核。我们只有 &lt;code>.text&lt;/code> 和 &lt;code>.data&lt;/code> 两个段，起始地址设置在 0x100000（即 1 MiB）处。将链接脚本保存为 &lt;code>linker.lds&lt;/code>，然后使用 &lt;code>ld&lt;/code> 链接：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>$ ld -T linker.lds -o multiboot.elf multiboot.o
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后，我们使用 &lt;code>objcopy&lt;/code> 将 ELF 格式的可执行文件转换为裸的二进制文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>$ objcopy --strip-all -O binary multiboot.elf multiboot.bin
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="运行">运行
&lt;/h3>&lt;p>我们可以使用 QEMU 来运行这个内核，只需要将 &lt;code>multiboot.bin&lt;/code> 作为内核传入 QEMU 即可，如果一切顺利的话，QEMU 会在串口输出「Hello, World!」，然后关机：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>$ qemu-system-x86_64 -m 256M -smp &lt;span style="color:#ae81ff">1&lt;/span> -nographic -kernel multiboot.bin
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>SeaBIOS &lt;span style="color:#f92672">(&lt;/span>version rel-1.16.3-0-ga6ed6b701f0a-prebuilt.qemu.org&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>iPXE &lt;span style="color:#f92672">(&lt;/span>http://ipxe.org&lt;span style="color:#f92672">)&lt;/span> 00:03.0 CA00 PCI2.10 PnP PMM+0EFD0E60+0EF30E60 CA00
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Booting from ROM..
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Hello, World!
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>至此，我们的 Multiboot 极简示例就完成了。我们可以在此基础上扩展更多的功能，例如读取 Multiboot 信息表，初始化栈，设置 GDT 和 IDT 等等，直至成为一个完整的内核。&lt;/p>
&lt;p>以上代码已经放在&lt;a class="link" href="https://github.com/aarkegz/os-multiboot-minimal-example/" target="_blank" rel="noopener"
>这个仓库&lt;/a>中，并附上了功能更加强大的 Makefile，欢迎使用。&lt;/p></description></item><item><title>x86_64 下的段机制和 FS/GS</title><link>https://aarkegz.com/os/x86/segments-in-x86_64-and-fs-gs/</link><pubDate>Thu, 28 Mar 2024 13:46:26 +0800</pubDate><guid>https://aarkegz.com/os/x86/segments-in-x86_64-and-fs-gs/</guid><description>&lt;img src="https://aarkegz.com/os/x86/segments-in-x86_64-and-fs-gs/20250421071514-d1b858b4-clto-1280x720.png" alt="Featured image of post x86_64 下的段机制和 FS/GS" />&lt;h2 id="x86_64-下的段机制">x86_64 下的段机制
&lt;/h2>&lt;p>段机制曾经是 x86 架构内存管理机制的重要部分，然而在功能更强大、粒度更细、安全性也更好的分页机制出现之后，段就显得有些多余甚至碍手碍脚了。绝大多数操作系统也不再使用分段机制，而是只在启动时建立几个段将逻辑地址空间对等映射到线性地址空间。&lt;/p>
&lt;p>因此，在 x86_64 诞生之时，AMD 选择弃用 &lt;code>CS/DS/ES/SS&lt;/code> 四个段：在 64-bit 下，这四个段总是被认为基址是 0，范围是整个 64-bit 地址空间；读写这四个段寄存器的指令要么产生错误，要么无效。换而言之，这四个段不再起到任何作用，也几乎不再需要任何操作。&lt;/p>
&lt;p>然而，&lt;code>FS/GS&lt;/code> 这两个段被保留了下来，并被赋予了新的机制和功能：段基址不再由段寄存器指向的描述符决定，而是由 &lt;code>IA32_FS/GS_BASE&lt;/code> 这两个 MSR 决定；原有的范围检查和权限检查也不存在了。基本上可以说，在 64-bit 下，通过 &lt;code>FS/GS&lt;/code> 段访问一个内存地址，就是单纯在地址上加上一个偏移量，这是将 &lt;code>IA32_FS/GS_BASE&lt;/code> 当作了两个特殊的「基址寄存器」来使用了。&lt;/p>
&lt;p>有这样两个「寄存器」的好处是显而易见的（除了「多多益善」本身以外）：首先，使用 &lt;code>FS/GS&lt;/code> 段访问内存的指令都是现成的，这样修改属于「废物利用」，只需要添加几个 MSR 即可，相比于增加几个新的通用寄存器代价更低改动更小（想想加入 &lt;code>R8&lt;/code> ~ &lt;code>R15&lt;/code> 增添了多少麻烦事，而将 &lt;code>R16&lt;/code> ~ &lt;code>R31&lt;/code> 真正投入应用又不知要等多长时间）；其次，可以通过段实现更加复杂的寻址，例如 &lt;code>%gs:0x4(%rax,%rdx,8)&lt;/code> 等，虽然实践中出现这样的寻址的概率并不大；另外，&lt;code>IA32_FS/GS_BASE&lt;/code> 作为 MSR，更不容易被用户程序意外修改。&lt;/p>
&lt;p>以上几个特点使 &lt;code>IA32_FS/GS_BASE&lt;/code> 特别适合存储某些在线程运行过程中极少改变的指针，例如指向线程本地存储区域或 PerCPU 数据区域的指针。&lt;/p>
&lt;h2 id="fsgs-段和-percpu-数据及线程本地存储">&lt;code>FS/GS&lt;/code> 段和 PerCPU 数据及线程本地存储
&lt;/h2>&lt;p>习惯上，&lt;code>GS&lt;/code> 段主要用于内核存储 PerCPU 数据（每逻辑处理器一份的数据），为了方便内核使用，x86_64 还提供了额外的机制：&lt;code>IA32_KERNEL_GS_BASE&lt;/code> MSR 和 &lt;code>swapgs&lt;/code> 指令。&lt;/p>
&lt;p>CPU在用户态执行用户程序时，&lt;code>IA32_GS_BASE&lt;/code> 中存储的是用户态程序所使用的 &lt;code>GS&lt;/code> 段基址，内核所用的 &lt;code>GS&lt;/code> 段基址则被被暂存在 &lt;code>IA32_KERNEL_GS_BASE&lt;/code> MSR 中；当控制流通过系统调用等方式进入到内核态时，内核可以通过 &lt;code>swapgs&lt;/code> 指令将 &lt;code>IA32_KERNEL_GS_BASE&lt;/code> 和 &lt;code>IA32_GS_BASE&lt;/code> 两个 MSR 的内容互换，同时保存用户态的 &lt;code>GS&lt;/code> 段基址并加载内核态的 &lt;code>GS&lt;/code> 段基址；当控制流回到用户态时，内核可以再次使用 &lt;code>swapgs&lt;/code> 指令保存内核态的 &lt;code>GS&lt;/code> 段基址并恢复用户态的 &lt;code>GS&lt;/code> 段基址。&lt;/p>
&lt;p>&lt;code>GS&lt;/code> 段所指向的 PerCPU 数据具体如何排布取决于内核自身的设计，但一般都至少会包括系统调用时所需要切换到的内核栈地址，这是因为 &lt;code>syscall&lt;/code> 指令并不会切换栈，需要内核手动处理，而内核如果手动指定某个固定地址则会在多处理器系统下出现冲突，因此从 &lt;code>GS&lt;/code> 段读取一个 PerCPU 的地址是最为合理的。（顺带一提，在处理中断时就无需操心手动切换栈的问题，这种设计上的不统一也显示出整个 x86 架构的某种「历史厚重感」）&lt;/p>
&lt;p>另外，&lt;code>GS&lt;/code> 段有时也被栈保护机制所使用，放置 PerCPU 的 Stack Canary 原始值。在这种情况下，操作系统在分配 PerCPU 数据的存储空间时，必须考虑到 Stack Canary 的存在。&lt;/p>
&lt;p>&lt;code>FS&lt;/code> 则被用户态程序广泛使用于指向线程本地存储区域的地址。其具体排布可以参照 ABI 的定义及 glibc 或 musl 的实现。&lt;/p>
&lt;p>另外需要注意的是，在 64-bit 下访问 &lt;code>FS/GS&lt;/code> 段寄存器的效果是因厂家和型号而异的，因此在 64-bit 下不应该直接访问 &lt;code>FS/GS&lt;/code> 段寄存器，而是应该访问相关的 MSR（仅 CPL 0，Linux 下可以使用 &lt;code>arch_prctl&lt;/code> 系统调用间接访问）或者使用 FSGSBASE 扩展（CPL 3 可用）。如果想要自行实现的操作系统内核良好地支持面向 POSIX 标准的 C/C++用户态程序，一定要妥善实现 &lt;code>arch_prctl&lt;/code> 系统调用以及上下文切换（无论涉不涉及特权级切换）时 &lt;code>FS/GS&lt;/code> 段基址的保存和切换。&lt;/p>
&lt;h2 id="其他">其他
&lt;/h2>&lt;h3 id="拓展阅读">拓展阅读
&lt;/h3>&lt;ul>
&lt;li>&lt;a class="link" href="https://stackoverflow.com/questions/10810203/what-is-the-fs-gs-register-intended-for" target="_blank" rel="noopener"
>What is the &amp;ldquo;FS&amp;rdquo;/&amp;ldquo;GS&amp;rdquo; register intended for?&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.kernel.org/doc/html/next/x86/x86_64/fsgs.html" target="_blank" rel="noopener"
>Using FS and GS segments in user space applications&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://read.seas.harvard.edu/cs161/2021/lectures/3-multitasking-and-preemption.pptx" target="_blank" rel="noopener"
>x86-64: The %fs and %gs Segment Registers&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://stackoverflow.com/questions/62546189/where-i-should-use-swapgs-instruction" target="_blank" rel="noopener"
>Where I should use &amp;ldquo;swapgs&amp;rdquo; instruction&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://wiki.osdev.org/SWAPGS" target="_blank" rel="noopener"
>SWAPGS on OSDev Wiki&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://0xax.gitbook.io/linux-insides/summary/interrupts/linux-interrupts-1" target="_blank" rel="noopener"
>linux-insides - Interrupts - Introduction&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://0xax.gitbook.io/linux-insides/summary/concepts/linux-cpu-1" target="_blank" rel="noopener"
>linux-insides - Concepts - Per-CPU variables&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://stackoverflow.com/questions/6611346/how-are-the-fs-gs-registers-used-in-linux-amd64" target="_blank" rel="noopener"
>How are the fs/gs registers used in Linux AMD64?&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://akkadia.org/drepper/tls.pdf" target="_blank" rel="noopener"
>ELF Handling For Thread-Local Storage&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>