UEFI启动流程全解析:从SEC到BDS到底发生了什么

UEFI启动流程全解析:从SEC到BDS到底发生了什么

本文基于EDK2源码梳理UEFI五个阶段的主线逻辑,不是翻译spec。

一张图看懂UEFI启动全流程

按下电源键后,到操作系统接管之前,UEFI固件经历五个阶段,每个阶段职责清晰、逐层递进:
UEFI启动的五个Phase

每个阶段的核心任务可以一句话概括:

阶段一句话耗时
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()完成搬迁:

  1. 预留新Stack — 从永久内存底部分配
  2. 计算偏移量 — 新旧Stack顶的差值,正负方向取决于平台
  3. Heap搬迁 — CopyMem整个临时RAM Heap到永久内存,包括HOB列表
  4. Stack搬迁 — CopyMem旧Stack内容到新Stack,包括返回地址和局部变量
  5. 迁移Memory Pages — 早期AllocatePages分配的内存页也要搬
  6. 修正所有指针 — HOB指针、PPI数据库、FV Handle、Memory Allocation HOB逐一修正偏移
  7. Shadow PeiCore — 把PEI Core从Flash拷到永久内存执行,XIP变内存执行
  8. 切换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 对比

方面PEIDXE
调度对象PEIM(跑完可卸载)DXE Driver(永久驻留)
依赖表达PPI GUID布尔表达式Protocol GUID布尔表达式 + 优先级
通信机制PPI(临时指针)Protocol(永久接口)
内存临时RAM→永久RAM永久RAM

五、BDS阶段——"按F2进BIOS"的背后

核心矛盾:怎么决定从哪启动?

BDS不靠写死代码判断启动顺序,而是靠UEFI NVRAM变量:

  • BootOrderUINT16数组,存放启动选项编号的优先级
  • 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通信缓冲区等场景的设计原则一脉相承:在资源受限的早期阶段,所有操作都要可逆、可迁移。


实战踩坑总结

阶段后果经验
SECCAR大小被低估,FSP栈需求超出128KB30%概率冷启动死机,无日志实测+20%余量,别信文档估算
PEIStack偏移方向搞反所有指针修正反向,机器死透临时RAM和永久RAM地址高低因平台而异
DXE IPL页表漏映射HOB List所在的高地址切64位后立刻#PF遍历所有Resource Descriptor HOB建页表
DXEInstallProtocolInterface返回值没检查Dispatcher死循环重试ASSERT返回值,同名Protocol不能装两次
BDSS3 Resume时ACPI NVS内存被误标为不可用OS唤醒后Wake Vector为空,蓝屏保留System Memory + Memory Reserved + ACPI NVS三种类型

源码路径索引

阶段关键文件内容
SECUefiCpuPkg/SecCore/Ia32/SecEntry.nasmSEC入口汇编,CAR初始化
SECUefiCpuPkg/SecCore/SecMain.cSEC主逻辑,微码加载,交棒PEI
PEIMdeModulePkg/Core/Pei/PeiMain/PeiMain.cPeiCore主入口,搬迁总控
PEIMdeModulePkg/Core/Pei/Memory/MemoryServices.cInstallPeiMemory,内存迁移
PEIMdeModulePkg/Core/Pei/Dispatcher/Dispatcher.cPEI Dispatcher
DXE IPLMdeModulePkg/Core/DxeIplPeim/DxeIpl.cDxeIpl入口
DXE IPLMdeModulePkg/Core/DxeIplPeim/DxeLoad.cDXE Core加载,页表构建
DXEMdeModulePkg/Core/Dxe/Dispatcher/Dispatcher.cDXE Dispatcher主循环
DXEMdeModulePkg/Core/Dxe/Hand/Handle.cProtocol安装/重安装
DXEMdeModulePkg/Core/Dxe/Hand/Locate.cProtocol查找
BDSMdeModulePkg/Universal/BdsDxe/BdsEntry.cBDS入口,启动设备扫描
规范PI Specification Volume 1SEC(Ch.4) PEI(Ch.5) DXE(Ch.7-10)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

媳妇爱吃酸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值