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 状态设置。顺序错了可能直接挂掉——别问我怎么知道的(苦笑)。
- Invalidate 所有 Cache
写一条 WBINVD 指令 + INVD 指令组合,或者在各个缓存控制器中写清除寄存器。我曾经在 QEMU 上忘了做这一步,结果从实模式切到保护模式后,缓存里还留着旧的段描述符,导致访问数据全乱套。 - 建立临时的 GDT 和 IDT
至少需要一个代码段描述符和一个数据段描述符。IDT 可以留空(设置掩码忽略中断),但基址必须合法。我个人习惯直接在主内存底部开一个 64KB 的临时区域,把 GDT 扔进去。 - 设置控制寄存器 CR0(PE=1)
这是从实模式切换到保护模式的一步。一定要先刷新指令流水线(用 far jmp)。嗯,这里要注意——如果在切换之前没有正确设置 CR3,而且 PG 位仍然为 0,其实可以只开保护不开分页。很多早期 UEFI 就是这么干的。 - 切换到分页模式(如需)
现代 UEFI 通常直接进入长模式(IA-32e),所以需要建立页表。至少要为前 4MB 建立 2MB 大页映射(PDPE+ PDE 各一)。 - 配置 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 状态设置的关键模块画成一张流程图。它概括了从上电复位到交出控制权的完整路径。
这张图从左到右是典型流程。注意,实际项目中你可能需要根据硬件手册微调顺序(比如有些芯片要求先开启分页才能访问 APIC 页面)。我个人习惯在每个节点后都插一段调试打印,输出 CR0/CR3/MSR 的状态,这样出问题了至少能知道是哪个步骤挂的。
最后两个小建议
- **多用
cpuid做序列化**:每次设置完控制寄存器后,加一条cpuid能保证前面的写操作完全提交。虽然慢几个 cycle,但调试期值得。 - **保留一段安全模式的回退路径**:如果你的板子切长模式失败,可以跳回保护模式继续输出错误信息。这个是我在项目中吃过亏才加上的——当时新板子调了三天才发现是内存条插错了,但 CPU 已经进长模式,连串口信息都看不到了。
总结:CPU 状态设置是 BIOS/UEFI 初始化中最脆弱也最关键的一环。从复位后清缓存、建 GDT、切保护模式,到配置 MSR、唤醒其他核心,每一步都离不开对硬件的深入理解。下次当你 debug 启动阶段的问题时,优先检查当前 CPU 处于什么模式、Cache 策略对不对、页表映射是否覆盖了正确区域。抓住这三个要素,大部分故障都能快速搞定。