BIOS/UEFI 初始化阶段 CPU 状态设置

各位同事,今天我们来聊聊 BIOS/UEFI 初始化阶段最核心的部分——CPU 状态设置。说白了,这一步决定了你的处理器从复位向量开始,到交出控制权给操作系统之前,到底处在什么配置下。我见过不少项目,因为这里几个寄存器没写对,导致系统启动到一半就莫名其妙死机,或者性能跑不满。嗯,咱们今天就把它彻底理清楚。

复位后的 CPU 是什么状态?

CPU 上电复位那一刻,所有核心都处于一个默认但比较“原始”的状态。你想想看,这时候 Cache 是关闭的、页表还没建、GDT 也不存在。我最初做这个的时候,以为复位后 CPU 已经能正常访问 4GB 内存了,结果发现第一条指令都跑在实模式下,地址线只有 20 位有效。这件事让我印象深刻。

以下是复位后 CPU 主要寄存器的默认值:

寄存器/状态 复位默认值 说明
CS.Base 0xFFFF0000 实模式下基址为 0xFFFF0000,这样 EIP 从 0xFFF0 开始
EIP 0x0000FFF0 第一条指令地址 = 0xFFFFFFF0
CR0 0x60000010 PE=0 (实模式), EM=0, WP=0, NE=1, ET=1, PG=0
EFLAGS 0x00000002 IF 被清零,中断关闭
GDTR.Limit 0xFFFF 基址和 limit 未定义,需要手动加载
L1/L2 Cache 全部无效(Invalid) 数据可能残留,需要 Invalidate
APIC 通常处于 BSP 模式 其他核心被 HALT 住,等待 SIPI

注意看,CR0 的 PE 位是 0,所以 CPU 默认在实模式。我建议你把这个表打印出来贴在工位上,刚上电调试时对照着查很实用。

必须做好的几件事

BIOS/UEFI 在初始化的第一步,需要依次完成以下 CPU 状态设置。顺序错了可能直接挂掉——别问我怎么知道的(苦笑)。

  1. Invalidate 所有 Cache
    写一条 WBINVD 指令 + INVD 指令组合,或者在各个缓存控制器中写清除寄存器。我曾经在 QEMU 上忘了做这一步,结果从实模式切到保护模式后,缓存里还留着旧的段描述符,导致访问数据全乱套。
  2. 建立临时的 GDT 和 IDT
    至少需要一个代码段描述符和一个数据段描述符。IDT 可以留空(设置掩码忽略中断),但基址必须合法。我个人习惯直接在主内存底部开一个 64KB 的临时区域,把 GDT 扔进去。
  3. 设置控制寄存器 CR0(PE=1)
    这是从实模式切换到保护模式的一步。一定要先刷新指令流水线(用 far jmp)。嗯,这里要注意——如果在切换之前没有正确设置 CR3,而且 PG 位仍然为 0,其实可以只开保护不开分页。很多早期 UEFI 就是这么干的。
  4. 切换到分页模式(如需)
    现代 UEFI 通常直接进入长模式(IA-32e),所以需要建立页表。至少要为前 4MB 建立 2MB 大页映射(PDPE+ PDE 各一)。
  5. 配置 MSR 寄存器
    比如 IA32_MISC_ENABLE、IA32_FEATURE_CONTROL、MTRR 等。尤其是 MTRR,如果不设好,访问内存时可能会产生 UC(不可缓存)的默认行为,性能灾难。

关键代码片段:切换保护模式的核心步骤

; 假设我们已经设置好了 GDT 在内存地址 GDT_BASE
    lgdt [GDT_DESC]          ; 加载全局描述符表寄存器
    mov eax, cr0
    or eax, 1                ; 设置 PE 位
    mov cr0, eax             ; 进入保护模式
    jmp 0x08:PM_ENTRY        ; far jump 刷新 CS 并加载代码段选择子

PM_ENTRY:
    ; 此时 CPU 已经在保护模式下运行
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov ss, ax
    ; ... 然后可以继续设置分页或切换到长模式

UEFI 特有的 CPU 状态初始化

UEFI 比传统 BIOS 多做了几件事。我记得在 Intel EDK2 的 SecCore 里,启动时 BSP 会先做一段非常短的汇编(16 位实模式),然后快速跳转到 32 位保护模式,再设置页表直接跳到 64 位长模式。这个过程几乎全是在 UEFI 的 SEC 和 PEI 阶段完成的。

下面我用一个表格对比 BIOS 和 UEFI 在初始化阶段的差异:

项目 传统 BIOS UEFI (EDK2)
内存访问模式 先实模式,后保护模式,有可能最后才开分页 直接从实模式跳到保护→长模式(IA-32e)
Cache 控制 通常不开 MTRR,直到进入操作系统 在 PEI 阶段就设置 MTRR 为 WB 模式
AP 初始化 使用 BIOS 中断直接唤醒 通过 SIPI 和 Startup IPI 在 DXE 阶段唤醒
MSR 保存/恢复 较少动态保存 UEFI 的 Runtime Services 会保存一些 MSR
安全性 无保护 设置 IA32_FEATURE_CONTROL 锁定位(Lock bit)

你看到了,UEFI 的做法更“现代”,但也更复杂。我曾在一个定制板卡上发现,UEFI 的 Cache 默认配置把部分外设 MMIO 区域也设置成了 WB,导致 DMA 数据不一致。这个问题查了一整天才定位到——就是因为缺少对 MTRR 中不可缓存区域的覆盖。

容易踩的坑(避坑指南)

  • 中断向量未清空就开中断 —— 我曾经在切换保护模式时忘记把 IDT 装满,结果一个 IRQ 触发直接 triple fault。后来我学乖了,在 sti 之前一定确认 IDT 有效。
  • 页表属性中的 NX 位与老旧 CPU 不兼容 —— 如果你的代码还需要支持 Intel Atom 之前的 CPU,那个 NX bit 是保留位。设置不对就出错。
  • 忘记刷新 TLB —— 修改 CR3 或 PTE 后一定要 invlpg 或完全刷新。否则旧映射还在,访问随机地址。
  • MSR 的 Lock 位提前锁死 —— 我见过一个项目,在 PEI 阶段就把 IA32_FEATURE_CONTROL 锁了,结果后来想调整 VMX 设置已来不及。建议等到 DXE 阶段确认不需要再改时再锁。

警告:不要把初始化顺序写死到 .asm 里的时候忘了加内存屏障

现代 CPU 有乱序执行,设置控制寄存器与后续指令之间必须有 mfence 或 cpuid 序列化。否则可能设置还没生效,后面代码已经跑在旧状态下。

小技巧:如果你在调试阶段拿不到逻辑分析仪,可以在 CR0 设置前后读 CPUID.EAX=1 检查 Feature flags 是否正确解锁。另外,把 MTRR 的回读值与预期值比对,能快速定位缓存一致性错误。

核心知识体系图

下面我把 BIOS/UEFI 初始化阶段 CPU 状态设置的关键模块画成一张流程图。它概括了从上电复位到交出控制权的完整路径。

BIOS/UEFI 初始化阶段 CPU 状态设置流程 CPU 上电复位 Invalidate 所有 Cache 建立临时 GDT & IDT CR0 置 PE=1 + far jmp 建立分页表 (2MB 映射) 配置 MSR (MTRR, Feature) SIPI 唤醒 AP / 返沪 DXE ⚠️ 易忘点 • Cache Invalidate 后 要加 mfence • GDT 描述符必须 8 字节对齐 💡 建议 CR0.PG 设为 1 之前 先设置 CR3 并刷新 TLB

这张图从左到右是典型流程。注意,实际项目中你可能需要根据硬件手册微调顺序(比如有些芯片要求先开启分页才能访问 APIC 页面)。我个人习惯在每个节点后都插一段调试打印,输出 CR0/CR3/MSR 的状态,这样出问题了至少能知道是哪个步骤挂的。

最后两个小建议

  • **多用 cpuid 做序列化**:每次设置完控制寄存器后,加一条 cpuid 能保证前面的写操作完全提交。虽然慢几个 cycle,但调试期值得。
  • **保留一段安全模式的回退路径**:如果你的板子切长模式失败,可以跳回保护模式继续输出错误信息。这个是我在项目中吃过亏才加上的——当时新板子调了三天才发现是内存条插错了,但 CPU 已经进长模式,连串口信息都看不到了。

总结:CPU 状态设置是 BIOS/UEFI 初始化中最脆弱也最关键的一环。从复位后清缓存、建 GDT、切保护模式,到配置 MSR、唤醒其他核心,每一步都离不开对硬件的深入理解。下次当你 debug 启动阶段的问题时,优先检查当前 CPU 处于什么模式、Cache 策略对不对、页表映射是否覆盖了正确区域。抓住这三个要素,大部分故障都能快速搞定。