UEFI启动流程全解析:从SEC到BDS到底发生了什么
本文基于EDK2源码梳理UEFI五个阶段的主线逻辑,不是翻译spec。
一张图看懂UEFI启动全流程
按下电源键后,到操作系统接管之前,UEFI固件经历五个阶段,每个阶段职责清晰、逐层递进:

每个阶段的核心任务可以一句话概括:
| 阶段 | 一句话 | 耗时 |
|---|---|---|
| SEC | 没有内存的情况下让CPU能跑C代码 | 毫秒级 |
| PEI | 从临时内存搬到永久内存,初始化硬件 | 秒级 |
| DXE IPL | 交棒:PEI→DXE,CPU切64位 | 毫秒级 |
| DXE | 建驱动框架,加载所有驱动 | 秒级 |
| BDS | 选启动设备,加载OS | 秒级 |
下面逐个拆。
一、SEC阶段——没有内存怎么办?
核心矛盾:DRAM没初始化,代码在哪跑?
CPU上电后,内存控制器还没配置,DDR还没训练,一根内存条都不能用。但CPU总要有个地方放Stack、放局部变量——C语言的第一行代码往哪写?
CPU开了个后门:Cache-as-RAM(CAR)。把L1/L2 Cache的某一片区域设为"不回写模式",CPU就以为这片Cache是一块SRAM,直接在上面跑C代码。
; UefiCpuPkg/SecCore/Ia32/SecEntry.nasm 简化示意
; 1. 禁用MTRR
mov ecx, 0x2FF
xor edx, edx
xor eax, eax
wrmsr
; 2. 设置CAR基地址和大小
mov ecx, CAR_BASE_MSR
mov edx, 0
mov eax, CAR_BASE_ADDR
wrmsr
; 3. 设置MTRR为Write-Back但不eviction
mov ecx, CAR_MASK_MSR
mov edx, 0
mov eax, CAR_MASK_VALUE
wrmsr
; 4. 重新启用MTRR
mov ecx, 0x2FF
mov edx, 0
mov eax, 0x800
wrmsr
; 5. 设置Stack指针指向CAR
mov esp, CAR_STACK_TOP
; 6. 安全调用C函数
call SecCoreStartupWithStack
CAR大小平台差异巨大:老平台64KB,新平台可以开到1MB甚至更多。这决定了SEC阶段能干多少事。
SEC干的四件事
| 步骤 | 内容 | 产出 |
|---|---|---|
| ① CAR初始化 | 把Cache当RAM用 | 临时代码/数据区域 |
| ② 微码加载 | CPU出厂补丁,通过WRMSR触发 | CPU行为正确 |
| ③ BFV定位 | 从SPI Flash找到固件根 | FV Header地址 |
| ④ 交棒PEI | 打包EFI_SEC_PEI_HAND_OFF | 临时RAM信息、Stack信息、BFV地址 |
SEC最后调用SwitchStack,把控制权交给PEI Core,自己永不返回。
类比:SEC就像酒店前台的"临时寄存柜"——房间(DRAM)还没准备好,你先在寄存柜(CAR)里凑合,洗个脸换身衣服。等房间好了,前台把钥匙和寄存柜信息一起交给你。
二、PEI阶段——从临时内存搬到永久内存
核心矛盾:临时RAM只有64KB,迟早要搬
PEI Core在临时RAM上建好HOB列表(Hand-Off Block——“账本”),然后开始逐个dispatch PEIM。某个早期PEIM(通常是Memory Init PEIM)完成DRAM训练后,调用PeiInstallPeiMemory()注册永久内存。
但这个函数只是记下地址、设一个flag——不立刻搬。因为调用者此刻还在临时RAM的Stack上跑,你不能在自己脚底下拆地板。
// MdeModulePkg/Core/Pei/Memory/MemoryServices.c
EFI_STATUS EFIAPI PeiInstallPeiMemory (
IN CONST EFI_PEI_SERVICES **PeiServices,
IN EFI_PHYSICAL_ADDRESS MemoryBegin,
IN UINT64 MemoryLength
)
{
// 只能调一次!
if (PrivateData->PeiMemoryInstalled) {
ASSERT (FALSE);
return EFI_SUCCESS;
}
PrivateData->PhysicalMemoryBegin = MemoryBegin;
PrivateData->PhysicalMemoryLength = MemoryLength;
PrivateData->FreePhysicalMemoryTop = MemoryBegin + MemoryLength;
// 设flag,告诉Dispatcher"该搬家了"
PrivateData->SwitchStackSignal = TRUE;
return EFI_SUCCESS;
}
搬家8步
Dispatcher在下一个PEIM执行完毕后检查flag,调用PeiCheckAndSwitchStack()完成搬迁:
- 预留新Stack — 从永久内存底部分配
- 计算偏移量 — 新旧Stack顶的差值,正负方向取决于平台
- Heap搬迁 — CopyMem整个临时RAM Heap到永久内存,包括HOB列表
- Stack搬迁 — CopyMem旧Stack内容到新Stack,包括返回地址和局部变量
- 迁移Memory Pages — 早期AllocatePages分配的内存页也要搬
- 修正所有指针 — HOB指针、PPI数据库、FV Handle、Memory Allocation HOB逐一修正偏移
- Shadow PeiCore — 把PEI Core从Flash拷到永久内存执行,XIP变内存执行
- 切换Stack重入PeiCore — 旧Stack永不返回,新PeiCore在永久内存上重新开始
类比:搬家后纸笔记的所有"东西在第3号寄存柜"都得改成"东西在301房第2个柜子"。漏改一条就找不到东西了。在固件里,漏改一个指针,机器就死了。
第二次进PeiCore后,安装gEfiPeiMemoryDiscoveredPpiGuid,通知所有等内存的PEIM可以干活了。最终PEI Core找到gEfiDxeIplPpiGuid,把HOB列表交给DXE IPL。
三、DXE IPL——PEI到DXE的"交棒人"
核心矛盾:PEI在32位跑,DXE要64位
DXE IPL本身是一个PEIM,由PEI Dispatcher在最后阶段调度。它的Depex依赖gEfiPeiMemoryDiscoveredPpiGuid——永久内存可用后才跑。
// MdeModulePkg/Core/DxeIplPeim/DxeIpl.c
EFI_STATUS EFIAPI PeimInitializeDxeIpl (
IN EFI_PEI_FILE_HANDLE FileHandle,
IN CONST EFI_PEI_SERVICES **PeiServices
)
{
// 获取启动模式
PeiServicesGetBootMode (&BootMode);
// 切换到64位长模式 + 加载DXE Core
Status = HandOffToDxeCore (PeiServices);
// 不应该返回。返回了说明交棒失败。
ASSERT_EFI_ERROR (Status);
CpuDeadLoop ();
return Status;
}
三件事
① 获取HOB List — 不拷贝,只传指针
SEC → EFI_SEC_PEI_HAND_OFF.HobList → PEI Core
PEI Core → PeiServices->HobList → DxeIpl
DxeIpl → HANDOFF_TO_DXE.HobList → DXE Core
全程只有指针在传递,没有数据拷贝。HOB List是PEI在永久内存里建好的,DXE直接用。
② 加载DXE Core镜像
从FV HOB找到DXE Firmware Volume,在里面搜索EFI_FV_FILETYPE_DXE_CORE,用PE/COFF加载器加载到内存。DXE Core本质上是一个PE32+可执行文件——和Windows加载.dll是同一套逻辑。
③ 32位→64位长模式切换
; UefiCpuPkg/CpuDxe/Ia32/Paging.nasm 简化示意
; 1. 设置CR3 = 页表基址(PML4)
mov eax, [esp+16]
mov cr3, eax
; 2. 启用PAE
mov eax, cr4
bts eax, 5
mov cr4, eax
; 3. 设置EFER.LME = 1(Long Mode Enable)
mov ecx, 0xC0000080
rdmsr
bts eax, 8
wrmsr
; 4. 启用分页(CR0.PG = 1)
mov eax, cr0
bts eax, 31
mov cr0, eax
; ——此刻CPU在64位兼容模式——
; 5. 远跳转到64位代码段
jmp 0x28:LongModeEntry
页表构建是最大的坑:必须覆盖HOB List所在的所有物理内存段。如果HOB List建在4GB以上(比如channel B插了内存而channel A没插),而BuildPageTablesIa32Pae只映射了低4GB——切模式后立刻触发#PF,CPU原地去世。
四、DXE阶段——驱动框架的三把火
核心矛盾:怎么让几十个驱动有序加载、互相找到?
DXE Core烧三把火:Dispatcher(调度驱动)、Protocol Database(驱动间通信)、Handle Database(设备身份管理)。
Dispatcher:五级优先级 + Depex
DXE Dispatcher不是"谁的Depex先满足谁先跑"。它有五级优先级:
#define DXE_DRIVER_PRIORITY_PLATFORM_PROCESSOR 0x03000000 // 最早:CPU/芯片组初始化
#define DXE_DRIVER_PRIORITY_PLATFORM_CHIPSET 0x03010000
#define DXE_DRIVER_PRIORITY_BUS 0x03020000 // 总线驱动
#define DXE_DRIVER_PRIORITY_DEVICE 0x03030000 // 设备驱动
#define DXE_DRIVER_PRIORITY_PLATFORM_APPLICATION 0x03040000 // 最晚:应用层
每个驱动的Depex是Protocol GUID的布尔表达式:
// USB键盘驱动的Depex
AND
gEfiUsbIoProtocolGuid // 必须有USB IO
gEfiSimpleTextInputProtocolGuid // 且已有输入设备
END
Dispatcher多轮扫描:第一轮跑优先级最高且Depex满足的,装了新Protocol后,第二轮可能有更多驱动Depex满足了,继续跑……直到没有新的驱动可以调度。
Protocol Database:双向索引
全局 Handle List
└─ Handle #1 [PCI Root Bridge]
│ └─ Protocol → gEfiPciHostBridgeResourceAllocationProtocolGuid
│ └─ Protocol → gEfiDevicePathProtocolGuid
└─ Handle #2 [SATA Controller]
└─ Protocol → gEfiPciIoProtocolGuid
└─ Protocol → gEfiDevicePathProtocolGuid
全局 Protocol Entry List(按GUID索引)
└─ gEfiDevicePathProtocolGuid → Handle #1, Handle #2
└─ gEfiPciIoProtocolGuid → Handle #2
双向索引:从Handle能找到它装了什么Protocol;从Protocol GUID能找到所有装了它的Handle。
Notify机制:异步依赖的解法
驱动A需要Protocol X,但Protocol X由驱动B提供,而B还没跑。A调用RegisterProtocolNotify注册一个Event。当B调用InstallProtocolInterface安装Protocol X时,A的Event被Signal,A被唤醒。
// 驱动A:等待Protocol X
gBS->RegisterProtocolNotify (&gProtocolXGuid, Event, &Registration);
gBS->WaitForEvent (1, &Event, &Index); // 阻塞等待
gBS->LocateProtocol (&gProtocolXGuid, Registration, &Interface); // 获取
类比:Protocol Database是公司的"内部黄页"。安装Protocol是"挂牌营业",LocateProtocol是"翻黄页找服务",RegisterProtocolNotify是"订阅通知——XX部门成立了好告诉我"。
PEI vs DXE 对比
| 方面 | PEI | DXE |
|---|---|---|
| 调度对象 | PEIM(跑完可卸载) | DXE Driver(永久驻留) |
| 依赖表达 | PPI GUID布尔表达式 | Protocol GUID布尔表达式 + 优先级 |
| 通信机制 | PPI(临时指针) | Protocol(永久接口) |
| 内存 | 临时RAM→永久RAM | 永久RAM |
五、BDS阶段——"按F2进BIOS"的背后
核心矛盾:怎么决定从哪启动?
BDS不靠写死代码判断启动顺序,而是靠UEFI NVRAM变量:
- BootOrder:
UINT16数组,存放启动选项编号的优先级 - Boot####:每个启动选项的详细信息(设备路径、描述、属性)
BootOrder: [0x0000, 0x0001, 0x0002]
Boot0000: {
Description: "Windows Boot Manager",
DevicePath: PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0)/HD(3,GPT,...)/\\EFI\\Microsoft\\Boot\\bootmgfw.efi
}
Boot0001: {
Description: "USB HDD: SanDisk Ultra",
DevicePath: PciRoot(0x0)/Pci(0x1D,0x0)/USB(0x0)/HD(1,MBR,...)/\\EFI\\BOOT\\BOOTX64.EFI
}
BDS干的三件事
① 扫描启动设备
// 扫描所有SimpleFileSystem设备(硬盘、U盘、CD-ROM)
gBS->LocateHandleBuffer (ByProtocol, &gEfiSimpleFileSystemProtocolGuid, ...);
// 扫描PXE网络启动设备
gBS->LocateHandleBuffer (ByProtocol, &gEfiPxeBaseCodeProtocolGuid, ...);
// 扫描HTTP启动设备
BdsHttpBootSupport ();
② 按BootOrder尝试启动
for (Index = 0; Index < BootOrderCount; Index++) {
OptionNumber = BootOrder[Index];
// 读Boot####变量
// 解析DevicePath
// 加载EFI程序
Status = BdsBootByOption (BootOption);
if (Status == EFI_SUCCESS) break; // 启动成功就不再试下一个
}
③ 处理超时和热键
BootCurrent变量记录当前正在尝试的选项PlatformBootTimeOut控制菜单显示超时- 用户按F2/F12/Del进入BIOS Setup或Boot Menu
类比:BDS就像酒店前台问你"从哪个门出去"——正门(硬盘)、侧门(U盘)、后门(网络)。你提前在前台登记了优先级(NVRAM变量),前台按顺序帮你开门。
六、五个阶段的"交棒"关系
UEFI启动的核心设计思想是严格分层、单向交棒:
SEC ──交棒──▶ PEI ──交棒──▶ DXE IPL ──交棒──▶ DXE ──交棒──▶ BDS ──交棒──▶ OS
│ │ │ │ │
│ EFI_SEC_ │ HOB List │ HOB List │ Protocol │ BootOption
│ PEI_HAND_OFF │ PPI Database │ DXE Core入口 │ Handle DB │ DevicePath
│ │ │ 64位模式 │ │
└──────────────┴──────────────┴───────────────┴──────────────┘
每层交棒的数据结构不同,但原则一致:只传指针和数据结构,不传执行状态。上一层跑完就"死掉"(不再返回),下一层拿到的是一个干净的环境 + 一份"家底清单"。
这个设计和PCIe枚举、ACPI表构建、SMM通信缓冲区等场景的设计原则一脉相承:在资源受限的早期阶段,所有操作都要可逆、可迁移。
实战踩坑总结
| 阶段 | 坑 | 后果 | 经验 |
|---|---|---|---|
| SEC | CAR大小被低估,FSP栈需求超出128KB | 30%概率冷启动死机,无日志 | 实测+20%余量,别信文档估算 |
| PEI | Stack偏移方向搞反 | 所有指针修正反向,机器死透 | 临时RAM和永久RAM地址高低因平台而异 |
| DXE IPL | 页表漏映射HOB List所在的高地址 | 切64位后立刻#PF | 遍历所有Resource Descriptor HOB建页表 |
| DXE | InstallProtocolInterface返回值没检查 | Dispatcher死循环重试 | ASSERT返回值,同名Protocol不能装两次 |
| BDS | S3 Resume时ACPI NVS内存被误标为不可用 | OS唤醒后Wake Vector为空,蓝屏 | 保留System Memory + Memory Reserved + ACPI NVS三种类型 |
源码路径索引
| 阶段 | 关键文件 | 内容 |
|---|---|---|
| SEC | UefiCpuPkg/SecCore/Ia32/SecEntry.nasm | SEC入口汇编,CAR初始化 |
| SEC | UefiCpuPkg/SecCore/SecMain.c | SEC主逻辑,微码加载,交棒PEI |
| PEI | MdeModulePkg/Core/Pei/PeiMain/PeiMain.c | PeiCore主入口,搬迁总控 |
| PEI | MdeModulePkg/Core/Pei/Memory/MemoryServices.c | InstallPeiMemory,内存迁移 |
| PEI | MdeModulePkg/Core/Pei/Dispatcher/Dispatcher.c | PEI Dispatcher |
| DXE IPL | MdeModulePkg/Core/DxeIplPeim/DxeIpl.c | DxeIpl入口 |
| DXE IPL | MdeModulePkg/Core/DxeIplPeim/DxeLoad.c | DXE Core加载,页表构建 |
| DXE | MdeModulePkg/Core/Dxe/Dispatcher/Dispatcher.c | DXE Dispatcher主循环 |
| DXE | MdeModulePkg/Core/Dxe/Hand/Handle.c | Protocol安装/重安装 |
| DXE | MdeModulePkg/Core/Dxe/Hand/Locate.c | Protocol查找 |
| BDS | MdeModulePkg/Universal/BdsDxe/BdsEntry.c | BDS入口,启动设备扫描 |
| 规范 | PI Specification Volume 1 | SEC(Ch.4) PEI(Ch.5) DXE(Ch.7-10) |

4594

被折叠的 条评论
为什么被折叠?



