UEFI/Windows启动流程

大多数人第一次认真关注 Windows 的启动过程,往往是被一块蓝屏逼的。屏幕上写着 0xc000000f0xc0000225,或是那句经典的"你的电脑需要修复",附带的"按 Enter 重试、按 F8 进入启动设置、按 ESC 进入 UEFI 固件设置"三个选项全都不管用。此时你会发现一个尴尬的事实:我们对这台每天开机使用的机器,了解得几乎为零。

本文要做的事,就是把下面这条被无数技术文章反复引用、却极少被真正讲透的链路,拆成可以逐环节审视的工程细节:

  1. 控制权交接了三次,而不是一次。 固件 → 引导管理器、引导管理器 → 系统加载器、系统加载器 → 内核,每一次交接都伴随着执行环境(分页是否开启、物理地址还是虚拟地址、谁拥有内存、谁拥有中断)的根本性变化。
  2. 中间夹着两个配置数据库。 一个是固件侧的 NVRAM 变量(BootOrder / Boot####),一个是微软侧的 BCD 注册表 hive。二者都叫"启动配置",格式、位置、读写者完全不同,但它们必须保持一致,任何一边漂移都会导致启动失败。
  3. 有一条与安全启动并行的"测量链"。 Secure Boot 决定"能不能跑",Measured Boot 记录"跑了什么",BitLocker 则基于后者决定"要不要解开磁盘密钥"。三者共用同一套 PCR 与事件日志。
  4. 用户给出的链路里有一个历史遗留的表述需要修正。 在 Windows 10 2004 及以后,hal.dll 已经不再是独立于内核的硬件抽象层实现,它被静态链接进了 ntoskrnl.exe。这不是咬文嚼字,而是理解现代 Windows 内核布局的关键。
  5. 每一次"下一步"都有失败分支。 启动链路真正的复杂度不在正常路径,而在它为每个环节准备的诊断、回滚与自动修复机制:Boot Status Policy、Last Known Good、WinRE、自动修复、启动计数。

本文按这条链路的自然顺序展开,但每一章都会同时讲三件事:它做了什么它依赖什么前提它在什么条件下失败,以及失败时你看到的是什么。写作目标是让读者读完以后,面对任何一条启动错误信息,都能立刻定位到它发生在链路的哪一段、该查什么证据、用什么工具修。

阅读约定:

  • 文中出现的十六进制地址、结构体字段名、函数名,凡属未公开接口(如 OslFwpKernelSetupPhase1OslArchTransferToKernelImgArchStartBootApplication)均来自公开的逆向工程与安全研究文献,版本号不同会存在差异,阅读时请以"机制描述"为主,不要把它当成稳定 ABI。
  • 涉及注册表键、bcdedit 命令、错误码的部分属于稳定事实,可直接用于生产环境排障。
  • 文中所有"实践建议"均标注了风险等级,涉及分区与 NVRAM 的操作请务必先备份。

第一章 固件初始化:从复位向量到 ExitBootServices

1.1 一次加电,CPU 做的第一件事

按下电源键后,电源管理电路把 PWR_OK 信号拉高,主板解除对 CPU 的复位。x86-64 处理器在复位后的状态是一套刻意保留的、近乎"原始"的状态:寄存器被清零或置为固定值,分页未开启,大部分特性未启用,CPU 处于实地址模式的长模式前状态。唯一重要的是指令指针------它固定指向线性地址 0xFFFFFFF0,这个地址被称为复位向量(Reset Vector)

这个地址位于 4GB 物理地址空间尽头往下 16 字节处,硬件(芯片组)保证这段地址被映射到主板 SPI Flash 上的固件映像末尾。那里通常只有一条远跳转指令,把执行流转向固件真正的入口。

从这一刻起,运行的代码来自主板上的那颗闪存芯片,而不是硬盘。这一点是理解整个安全启动模型的前提:操作系统还没参与任何事,第一道信任判定已经在固件里完成了。

1.2 PI 规范的七个阶段

现代 UEFI 固件不是一块单体的 16 位 ROM,而是一套模块化、分阶段执行的"微型操作系统"。UEFI 平台初始化(PI)规范把它划分为七段:

各阶段的要点如下。

SEC 阶段。 这是信任根所在。它处理的输入是"任何形式的处理器重启",输出是"一个可用于执行 C 语言代码的临时栈"。技术上最关键的手段是 Cache-as-RAM(CAR):在没有 DRAM 可用的情况下,把 CPU 的 L1/L2 缓存配置为不回写的临时 SRAM 使用,从而让后续的 C 代码能正常跑起来。SEC 还负责加载微码补丁,并把控制权交给 PEI 核心。

在开启了 Intel Boot Guard(或 AMD 平台的对等技术)的机器上,SEC 阶段之前还有一层更底层的硬件信任根:CPU 内部的微码会验证固件中的认证代码模块(ACM),ACM 再测量并验证固件的初始引导块(IBB)。IBB 验证失败,机器根本不会走到你能看到屏幕的地方------这是一个"固件级根套件防护",同时也是为什么某些刷过第三方 BIOS 或绕过 Boot Guard 的机器会直接变砖。

PEI 阶段。 它完成整个启动过程中最耗时、也最容易出硬件故障的一步:内存训练(Memory Training)。DDR 内存的初始化需要对信号时序做大量校准,这段时间内屏幕通常是黑的或卡在厂商 LOGO 上,耗时从几百毫秒到数秒不等,取决于内存容量、插槽数量与固件策略。这也是"开机黑屏十几秒才亮"最常见的非故障解释。

PEI 的输出是一组 HOB(Hand-Off Block) 列表,用一种自描述的数据结构把内存布局、固件卷位置、平台信息传给 DXE。这是固件内部的"交接文档"。

DXE 阶段。 固件的主体。DXE 派发器会遍历固件卷(FV)中的 FFS 文件,按依赖表达式(DEPEX)顺序执行 DXE 驱动。在这里,UEFI 的"服务导向"架构才真正成型:

  • 句柄数据库(Handle Database)Protocol 机制:设备被抽象为句柄,能力被抽象为 Protocol(如 EFI_BLOCK_IO_PROTOCOLEFI_SIMPLE_FILE_SYSTEM_PROTOCOLEFI_GRAPHICS_OUTPUT_PROTOCOL)。
  • Boot Services :内存分配、事件/定时器、映像加载(LoadImage/StartImage)、协议安装等。这些服务在 ExitBootServices() 之后全部失效。
  • Runtime Services :变量读写(GetVariable/SetVariable)、实时时钟、系统复位、以及后文会重点讲的 SetVirtualAddressMap。这些服务在操作系统运行期间依然可用。

这一阶段会初始化 USB、存储控制器、PCIe 总线、网卡(PXE)、显卡(GOP 驱动)。显卡 GOP(Graphics Output Protocol)在这里被建立起来,它是后续 Windows 引导管理器能显示图形化启动菜单、能显示那个转圈小圆点的根本原因------在纯 Legacy BIOS 时代,引导阶段只有 80x25 的文本模式和 VGA 调色板。

BDS 阶段。 固件开始执行启动策略。它的行为围绕 NVRAM 中的一组变量展开:

  • BootOrder:一个 16 位整数数组,定义启动项尝试顺序。
  • Boot######## 是十六进制编号,每个变量是一条"启动选项",其载荷是 EFI_LOAD_OPTION 结构,包含描述文字、设备路径(Device Path)、可选的参数数据。
  • BootNext:一次性的"下一次启动用这一项",优先级高于 BootOrder 的第一项。Windows 的"高级启动 → 立即重启"、"从设备启动"就是通过写入 BootNext 实现的。
  • BootCurrent:当前正在使用的启动项编号,只读。
  • Timeout:固件启动菜单等待时间。

BDS 按 BootOrder 依次尝试:解析 Boot#### 中的设备路径,找到对应分区上的 EFI 应用文件,调用 LoadImage 加载、StartImage 执行。若某一条失败(文件不存在、签名不通过),则尝试下一条;若全部失败,则进入"可移动介质回退路径"------查找默认的 \\EFI\\BOOT\\BOOTX64.EFI(x64 平台的默认回退文件名)。这个回退机制正是 Windows 安装 U 盘、以及绝大多数修复工具盘能"插上就能启动"的原因:固件不需要任何 NVRAM 条目,只要在某个 FAT 分区上放一个正确命名的文件即可。

关于设备路径。 启动项里最关键的数据结构是 EFI_DEVICE_PATH_PROTOCOL,它是一条自描述的、二进制的"寻址链"。一条典型的 Windows 启动项设备路径在概念上是:

复制代码
PciRoot(0x0)/Pci(0x1,0x2)/Pci(0x0,0x0)/NVMe(0x1,00-25-38-B9-31-A4-FE-FD)
  /HD(1,GPT,d022bdc9-8147-4835-812a-8f0fa6365b22,0x800,0x32000)
  /\\EFI\\Microsoft\\Boot\\bootmgfw.efi

即:从 PCI 根桥出发,走两段 PCI 桥,定位到 NVMe 控制器与命名空间,再定位到 GPT 分区表第 1 个分区(用分区 GUID 而不是盘符或序号标识),最后是分区内的文件路径。注意这里两个细节:

  • 分区用 GUID 标识。这意味着你可以把磁盘接到主板的另一个接口、甚至改变分区顺序,只要分区 GUID 不变,固件依然能找到它。这是 UEFI 相对 Legacy MBR"第几块盘第几个分区"这种脆弱寻址的重大改进,也是为什么"换了个 SATA 口就起不来"在 UEFI 机器上罕见得多。
  • 文件系统路径用反斜杠、大小写不敏感 。UEFI 规范定义的 FAT 文件系统驱动本身大小写不敏感,因此 \\EFI\\Microsoft\\Boot\\bootmgfw.efi\\efi\\microsoft\\boot\\BOOTMGFW.EFI 等价。

1.3 ESP:固件唯一认识的那个分区

上文的路径落在一个特殊分区上:EFI 系统分区(ESP)

为什么必须是 FAT32? 因为 UEFI 规范只强制要求固件实现 FAT 文件系统驱动。NTFS、exFAT 的支持依赖厂商额外实现,不能假定存在。这带来一个重要的排障推论:如果你的 ESP 被格式化成 NTFS,Windows 将永远无法启动 ,而修复方法只能是重建 ESP(diskpart 重建 + bcdboot 重新写入)。这在"用第三方分区工具整理磁盘"之后是相当常见的故障。

另一个常见坑是 ESP 与"系统保留分区"的混淆

  • UEFI/GPT 机器上:ESP(FAT32,约 100MB)承担启动文件的角色;可能还有一个 MSR(Microsoft 保留分区,16MB,无文件系统,供磁盘管理使用)。
  • Legacy BIOS/MBR 机器上 :承担启动文件角色的是"系统保留"(System Reserved)分区(NTFS,约 100--500MB,带活动标记),启动文件位于 \\Boot\\BCD,引导代码位于 MBR 与分区引导扇区。

这两套体系的文件名、命令参数、修复方式全都不同。在动手修复之前,第一件事永远是确认当前是 UEFI 还是 Legacy ------CMD 里执行 bcdedit /enum{bootmgr}path\\EFI\\Microsoft\\Boot\\bootmgfw.efi(UEFI)还是 \\Windows\\system32\\winload.exe(Legacy 场景下的不同结构),或者用 msinfo32 查看"BIOS 模式"。

1.4 Secure Boot:固件侧的"能不能跑"

启用了 Secure Boot 的固件,在执行任何一个 EFI 映像之前都要做一次签名验证。其信任结构是一个四层密钥体系:

验证流程本身不复杂:固件读出映像中 PE/COFF 的安全目录(WIN_CERTIFICATE),取出 Authenticode 签名链,然后依次判断------

  1. 签名的哈希是否出现在 dbx 中?是 → 拒绝执行。这一条优先级最高,是微软封杀已知恶意引导器(如某些被泄露签名的 bootkit)的手段。
  2. 签名链能否回溯到 db 中的某个证书或被信任的哈希?能 → 允许执行。
  3. 否则 → 拒绝,通常在屏幕上提示 "Selected boot image did not authenticate" 或类似信息。

Windows 引导管理器 bootmgfw.efiMicrosoft Windows Production PCA 2011 (及其继任体系)签署,几乎所有出厂机器的 db 中都含有这张证书,所以 Windows 能顺利启动。第三方引导器(Linux 的 shim、GRUB、rEFInd)则由 Microsoft Corporation UEFI CA 2011 体系签署,走的是微软提供的第三方签名服务------这也是为什么"装 Ubuntu 双系统"在 Secure Boot 开启的机器上依然可用。

一个正在发生、且必须知道的现实问题:2011 系列证书正在到期。 微软的替换方案是 2023 系列证书:

这件事的实践含义是:db 中只有 2011 证书的老机器,将来无法信任用 2023 证书签署的新版 bootmgfw.efi 与新的 dbx 更新。 它不会立刻导致启动失败(现有已签名文件仍然有效),但会形成"安全冻结"------引导器无法再被安全更新,dbx 无法再扩展。这是当前企业环境中需要提前规划证书更新的重要议题。

1.5 Measured Boot:记录"跑了什么"

Secure Boot 是"准入控制",它只回答能或不能。另一条并行的链条是测量启动(Measured Boot):每加载一个组件,就把它的摘要"扩展(Extend)"进 TPM 的一个 PCR,并把对应事件写入事件日志。

PCR 的核心性质是只能扩展、不能直接写

复制代码
PCR[N]_new = Hash( PCR[N]_old || 待测量数据 )

这意味着 PCR 的最终值隐含了整个测量序列,任何一步被替换或插入,最终值都会变化,且无法伪造一个"看似正确"的历史。

TCG PC 客户端规范定义的 PCR 分配如下(这是排障与取证时最常查的一张表):

有几点值得单独强调:

  • PCR 4 记录的是 bootmgfw.efi 本体。 从实测的事件日志中可以直接读到类似 PciRoot(0x0)/Pci(...)/NVMe(...)/HD(1,GPT,...)/\\EFI\\Microsoft\\Boot\\bootmgfw.efi 的设备路径,以及该映像在内存中的加载地址、长度、链接时基址。也就是说,如果 Windows 是被另一个引导器(如 GRUB)链式加载进来的,PCR 4 会出现两次 EV_EFI_BOOT_SERVICES_APPLICATION 事件,且第一条的设备路径指向非微软的 EFI 文件。这是远程证明(Attestation)检测"是否经过第三方引导器"的常用判据,也是部分反作弊引擎检测虚拟机的手段。
  • PCR 7 记录的是 Secure Boot 策略,而不是引导器本体。 它包含 PK、KEK、db、dbx 的内容与 Secure Boot 开关状态。这个区别后文讲 BitLocker 时至关重要。
  • separator 事件。 固件在 PCR 0--6 各写入一个 EV_SEPARATOR,用于标记"测量阶段的边界",防止后续组件伪造出"我早就测量过了"的假象。
  • 事件日志在哪? Windows 把合并后的日志(固件部分 + OS 加载器扩展部分)保存在 %WINDIR%\\Logs\\MeasuredBoot\\,可用 tpmtool.exe 或跨平台的 tpm2_eventlog 解析;运行期通过 Tbsi_Get_TCG_Log 提供给用户态。

1.6 ExitBootServices:整条链路最不可逆的一步

当引导加载器(对 Windows 而言是 winload.efi)认为自己已经准备好了,它会调用 ExitBootServices()。这一步的意义远超"卸载驱动":

  1. 它宣告固件引导服务终结。 从此刻起,所有 Boot Services(内存分配、协议数据库、定时器、映像加载)全部失效。
  2. 它把内存的所有权交给操作系统。 调用时需要传入一个由 GetMemoryMap() 获得的 MapKey,固件用它校验"你看到的内存映射是最新的"。如果期间发生了任何改变映射的事(比如有人分配了内存),MapKey 失效,调用会返回错误,加载器必须重新取映射再试。这是恶意 UEFI 驱动常挂钩的点:通过 hook ExitBootServices,可以在这一刻拿到 winload 的返回地址,进而定位 LOADER_PARAMETER_BLOCK、找到 ntoskrnl 的基址------这正是若干已知 bootkit(如 BlackLotus 一类技术路线)的通用套路。
  3. Runtime Services 仍需保留,但地址要换。 引导期 Runtime Services 用的是物理地址,而操作系统马上要开启自己的分页与虚拟地址空间。因此固件要求:OS 在调用 SetVirtualAddressMap() 时,把每一个运行时内存描述符的物理地址替换为虚拟地址。固件驱动借此"搬迁"自己的指针。此后操作系统通过 gRT(Runtime Services 表)继续调用变量读写、重启、关机、实时时钟。

这就是为什么 Windows 能在一个已经跑起来的系统里继续读写 UEFI 变量(bcdedit 改 NVRAM 启动项、Get-SecureBootUEFI 读 Secure Boot 状态、Set-SecureBootUEFI 更新证书)------这些操作最终都经由 Runtime Services 落到固件。

实践提示: 某些机器上 bcdedit /set {fwbootmgr} displayorder ... 报"数据无效"或直接失败,根因往往不在 Windows,而在固件对变量写操作的实现质量(变量空间不足、认证变量需要特定属性、或固件要求进入 Setup 模式)。这类问题只能靠升级固件解决。

1.7 时间都去哪了:固件阶段的耗时构成

如果一台机器"开机慢",先分清慢在哪一段。固件阶段(从上电到 bootmgfw.efi 获得控制权)的典型耗时分布:

最有效的固件级优化手段:

  • 在固件设置中启用 Fast Boot(注意与 Windows 的"快速启动"是两个不同的东西):跳过不必要的 USB 设备枚举、省略部分自检,通常能省下 1--5 秒。代价是可能识别不到慢速外设,需要用"Ultra Fast"以外的档位或临时关闭来进 UEFI 设置。
  • 减少不必要的 Option ROM:未使用的 RAID 控制器、网卡 PXE ROM 都会增加 DXE 时间。
  • 关闭不需要的启动项(尤其是网络上不存在的 PXE 项),避免 BDS 逐个超时。
  • 注意:在开启 BitLocker 且 PCR 绑定包含 PCR 0/2/4 的机器上,更新固件会触发 BitLocker 恢复密钥界面 。所以"升级 BIOS"之前应先 manage-bde -protectors -disable C: 挂起保护,升级完成后再 -enable。这也解释了为什么微软默认把 BitLocker 绑到 PCR 7 + 11 而不是 0+2+4(详见第五章相关讨论)。

第二章 bootmgfw.efi:Windows 引导管理器

2.1 它是谁,它从哪来

bootmgfw.efiWindows Boot Manager 在 UEFI 平台上的实现。"mgfw"即 M anager G raphics/EFI F irmware W rapper 一类的历史缩写延续,其对应的 Legacy BIOS 版本叫 bootmgr(无扩展名)。

它在磁盘上有几处副本,理解它们的区别很重要:

当你在 WinRE 命令提示符里执行:

复制代码
bcdboot C:\\Windows /s S: /f UEFI

这条命令做的事情,本质上就是:在 \\Windows\\Boot\\EFI\\ 下取一组文件,复制到 ESP 的 \\EFI\\Microsoft\\Boot\\,生成/更新其中的 BCD,并向 NVRAM 写入(或刷新)一条指向 \\EFI\\Microsoft\\Boot\\bootmgfw.efiWindows Boot Manager 启动项。

注意一个常见误解bcdboot 并不只复制 bootmgfw.efi。它复制的是整套引导环境:引导管理器本体、语言资源文件(\\EFI\\Microsoft\\Boot\\Fonts\\ 下的字体、bg-BGzh-CN 等语言目录)、内存诊断程序、以及用当前系统配置生成的 BCD。这也解释了为什么在一个"BCD 彻底损坏"的机器上,bcdboot 往往比 bootrec /rebuildbcd 更彻底------后者试图在既有 BCD 上打补丁,前者直接从源头重建整套。

2.2 作为 EFI 应用的 PE/COFF 结构

bootmgfw.efi 是一个标准的 PE32+ 映像 (64 位 PE/COFF),只是它的子系统类型不是常见的 3(IMAGE_SUBSYSTEM_WINDOWS_CUI)或 2(GUI),而是 10( IMAGE_SUBSYSTEM_EFI_APPLICATION。固件内置的 PE/COFF 加载器正是靠这个字段判断"这是一个可以直接执行的 EFI 应用"。

一个典型的 bootmgfw.efi 大致结构如下(用 dumpbin /headers 或任何 PE 查看工具可见):

复制代码
DOS Header (e_magic "MZ", e_lfanew → PE 签名偏移)
  └─ DOS Stub ("This program cannot be run in DOS mode.")
PE Signature "PE\\0\\0"
COFF File Header
  Machine:          IMAGE_FILE_MACHINE_AMD64 (0x8664)
  NumberOfSections: 典型 5--8
  Characteristics:  IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE ...
Optional Header (PE32+)
  Magic:                    0x20b (PE32+)
  Subsystem:                10 (EFI Application)
  SizeOfImage:              全部节按 SectionAlignment 展开后的大小
  SizeOfHeaders:            头部按 FileAlignment 对齐后的大小
  ImageBase:                链接时基址(典型 0x10000000)
  SectionAlignment:         通常为 0x1000(4KB,与页对齐)
  FileAlignment:            通常为 0x200
  CheckSum:                 对 EFI 应用有要求
  DataDirectory[SECURITY]:  指向 Authenticode 签名(WIN_CERTIFICATE)
  DataDirectory[BASERELOC]: 基址重定位表
Sections:  .text .rdata .data .pdata .reloc ...
Security Directory: WIN_CERTIFICATE → Authenticode 签名

几个与启动密切相关的细节:

SectionAlignment 与内存布局。 UEFI 规范要求加载器按节头(Section Header)把映像"铺"到内存中:每个节的 VirtualAddress/VirtualSize 决定它在映像内的偏移,节与节之间按 SectionAlignment 对齐。加载器先用 AllocatePages 申请 SizeOfImage 大小的内存,拷贝头部与各个节,处理 .reloc,然后才能跳转。若 ImageBase 处内存被占用,就要重定位------UEFI 环境中固件通常能按链接基址加载(因为此时内存几乎全空),所以重定位表往往用不上,但它是安全启动验证的一部分:签名覆盖的是校验和计算范围内的内容,重定位表参与完整性计算。

.pdata与异常处理。 x64 采用基于表的异常处理(不像 x86 用链式 SEH),.pdata 节存放函数表的 RUNTIME_FUNCTION 条目。引导管理器自身也使用结构化异常处理,错误分支(例如读 BCD 失败)最终会走到一个统一的失败处理路径,在屏幕上渲染那个我们熟悉的错误界面。

签名与校验和。 CheckSumDataDirectory[SECURITY] 是 Secure Boot 能否通过的关键。任何对 bootmgfw.efi 的字节级修改(打补丁、注入代码)都会让签名验证失败。这正是 bootkit 难以"修改微软引导器"而倾向于"挂钩其运行时行为"的原因。

2.3 引导管理器运行时:一个极简操作系统

bootmgfw.efi 被 StartImage 执行后,就成为一个运行在 EFI 环境下的独立程序,它拥有:

  • EFI Boot Services(内存、协议、事件)
  • EFI Boot Services 中的文件访问EFI_SIMPLE_FILE_SYSTEM_PROTOCOL + EFI_FILE_PROTOCOL)------注意,此时还没有 NTFS 驱动,只有固件提供的 FAT 驱动,这就是为什么 BCD 与引导器必须放在 FAT32 的 ESP 上
  • GOP 图形输出(用于渲染图形化启动菜单与错误界面)
  • 一套内部库 ,微软称之为 Boot Library,导出符号多为 Bl* 前缀(如 BlImgAllocateImageBufferBlBdStartBlFwGetBootOption 等)

它启动后的主要工作,可以概括为"找 BCD,读 BCD,按 BCD 决策"。

2.4 BCD 是怎么被找到的

引导管理器定位 BCD 的优先级顺序如下:

  1. 固件传入的启动选项参数(可选数据) 。UEFI 启动项 Boot#### 的载荷中可以附带参数数据,某些部署场景(如网络启动、OEM 定制)会用这里传递替代的 BCD 路径。这是最高优先级,但普通 Windows 安装通常不带这个数据。
  2. 默认路径 。在没有显式指定时,引导管理器会尝试从自己所在分区的 \\EFI\\Microsoft\\Boot\\BCD 读取。这里"自己所在分区"由加载它的设备路径推导------即引导管理器会去打开与自身映像同分区的那个简单文件系统句柄,再按路径打开文件。这就是为什么"BCD 和 bootmgfw.efi 必须在同一个 ESP 上"是硬性要求。
  3. 跨分区的显式 device 元素 。BCD 内部 {bootmgr} 对象的 device 元素会再次指明引导管理器所在卷,path 指明自身路径,这两个值主要用于 bcdedit 展示与修复工具校验,而不是定位 BCD 自身。

说得再直白一点:BCD 的路径是"算"出来的,不是"配"出来的。 引导管理器从"我自己被放在哪"反推出 BCD 的位置。这个设计带来一个重要推论:

如果你把 ESP 里的 bootmgfw.efi 单独复制到另一个 FAT 分区并让固件从那里启动,引导管理器会去那个分区的 \\EFI\\Microsoft\\Boot\\BCD 找配置,而不是原来的 ESP。这正是许多"多 ESP 混乱"故障的根源------机器上有两个 ESP(比如装了两块盘,各带一个),固件选中了没有正确 BCD 的那个,于是报 0xc000000f

2.5 引导菜单与选择逻辑

读到 BCD 之后,引导管理器要决定"加载谁"。BCD 中与此相关的关键元素(位于 {bootmgr} 对象,GUID {9DEA862C-5CDD-4E70-ACC1-F32B344D4795}):

选择过程:

  1. displayorder 只列出一个条目,且 timeout 为 0(默认值),则不显示菜单,直接加载。这就是为什么绝大多数 Windows 机器开机看不到启动菜单------不是没有菜单,是它没机会显示。
  2. 若有多个条目(多系统、WinRE 项、VHD 项),则显示图形化菜单,等待 timeout 秒。
  3. 倒计时结束或用户选择后,得到目标对象的 GUID,读取该对象的 device/path,加载对应 EFI 应用。

Windows 8 之后 F8 失效的原因 :传统上,Windows 在引导管理器阶段接受 F8 键,进入"高级启动选项"(安全模式、最后一次正确配置等)。但从 Windows 8 起,为了提高启动速度,引导管理器默认不再等待按键扫描------按键检测被移到了系统加载器与内核阶段。取而代之的是从 Windows 内部主动重启到高级启动Shift + 重启shutdown /r /o /t 0、或设置中的"高级启动"。这些操作最终是通过写入 BCD 的 {globalsettings} 中一个一次性引导标志(以及设置 BootNext 指向 WinRE)实现的------所以它是一次性的:下一次正常启动不会进入恢复环境。

如果你想恢复"启动时按 F8 出菜单"的旧行为,命令是:

复制代码
bcdedit /set {globalsettings} bootmenupolicy legacy

恢复默认:

复制代码
bcdedit /set {globalsettings} bootmenupolicy standard

这个开关的本质是:legacy 模式下引导管理器会建立一个按键轮询循环并等待按键,代价是每次启动都增加一点延迟;standard 模式下跳过这个循环。在排查"安全模式都进不去"的场景时,把它设为 legacy 常常很有用。

2.6 引导管理器的三条分支

引导管理器最终会加载三个可能的 EFI 应用之一:

(1) \\Windows\\system32\\winload.efi------ 正常启动路径。 这是本文第四章的主角,对应 BCD 中的 "Windows Boot Loader" 对象。

(2) \\Windows\\system32\\winresume.efi------ 从休眠/快速启动恢复。 当 BCD 的 {bootmgr}{default} 指向的对象带有 resumeobject 元素,且上一次关机写入了有效的休眠映像(hiberfil.sys 中含有有效的恢复数据),引导管理器会转去加载 winresume.efi。

这里有一个容易被忽略的重要事实:Windows 10 默认开启"快速启动"(Fast Startup,内部名 Hiberboot),而快速启动走的正是休眠恢复路径。 也就是说,你每次"关机再开机",走的都不是纯粹的冷启动路径,而是:

复制代码
关机:注销所有用户 → 把内核态(内核 + 已加载驱动 + 部分系统状态)写入 hiberfil.sys → 断电(S5)
开机:固件初始化 → bootmgfw.efi → 检测到有效恢复数据 → winresume.efi → 直接从休眠映像恢复内核态

它跳过了"重新加载并初始化内核与全部驱动"这一大段工作,所以快。代价是:

  • 双系统场景下,Windows 分区的文件系统处于"已挂载"状态,Linux 挂载 NTFS 可能造出不一致(这就是"双系统必须关掉快速启动"这条建议的技术根源)。
  • 某些驱动/固件状态不会被真正重置,遇到"重启能好、关机再开就出问题"的怪现象,第一件事就是关掉快速启动验证。
  • 快速启动只需要"精简版"休眠文件(powercfg /h /type reduced,约物理内存 20%),而完整休眠支持需要 "full"(约 40%)。

(3) \\ EFI\\Microsoft\\Boot\\memtest.efi------ 内存诊断。 对应 {memdiag} 对象(GUID {B2721D73-1DB4-4C62-BF78-C548A880142D}),它会在引导管理器菜单的"工具"分支中出现(或由 mdsched.exe 触发一次性引导)。

另外,BCD 中还存在一类"启动扇区应用"(bootsector,{ntldr} 用于引导 Windows Vista 之前的系统)和"自定义应用"(如引导 Linux 的 GRUB 也可以作为一个条目被 Windows 引导管理器加载,虽然实践中通常是反过来:GRUB 链式加载 Windows)。

2.7 引导管理器阶段的故障特征

判断"故障发生在引导管理器阶段"的典型证据:

  • 错误信息中出现了 File:指向 \\EFI\\Microsoft\\Boot\\... \\Windows\\system32\\winload.efi ------ 后者虽指向 winload,但报错者常常是 bootmgfw(因为它负责加载 winload)。
  • 屏幕上的错误界面是深色背景、白字、带错误码的那种(WinRE 风格的引导错误界面),而不是蓝屏。
  • 错误码集中在 0xc000000f0xc000000e0xc000000d0xc0000225

最常见的三类根因:

根因一:BCD 文件本身丢失或不可读。 典型触发场景:ESP 被格式化、磁盘克隆后 ESP 未正确复制、第三方分区工具挪动了分区导致设备路径失效、磁盘出现坏扇区。

根因二:BCD 存在,但内容指向了错误的位置。 BCD 内部用"磁盘签名 + 分区偏移"(MBR)或"磁盘 GUID + 分区 GUID"(GPT)来定位分区。克隆磁盘、更换主板、改变磁盘控制器模式、把系统盘从一台机器搬到另一台,都可能让这些标识与物理现实不匹配。此时 BCD 文件是完好的,但它指向了一个不存在的分区。

根因三:固件与 Windows 的启动项不一致。 比如 NVRAM 里的 Boot#### 设备路径指向的分区 GUID,与当前磁盘上的分区 GUID 不匹配(常见于换了硬盘、重新分区、清除了 NVRAM)。这种情况下固件根本找不到 bootmgfw.efi,屏幕上通常是固件自己的提示("No bootable device" 或厂商 LOGO 后进入固件设置),还没到 Windows 的错误界面。

快速判别三者的方法:

复制代码
# 在 WinRE 命令提示符中
diskpart
  list disk
  list volume        # 确认是否存在 FAT32 的 ESP,盘符是多少
  exit
dir S:\\EFI\\Microsoft\\Boot\\    # 若提示路径不存在 → 根因一/三
bcdedit /store S:\\EFI\\Microsoft\\Boot\\BCD /enum all
                      # 若提示无法打开存储 → 根因一(文件损坏)
                   # 若能打开但 device/osdevice 指向 unknown → 根因二
bcdedit /enum firmware        # 查看固件层启动项与实际磁盘是否匹配

第三章 BCD:固件之外的启动数据库

3.1 BCD 本质上是一个注册表 hive

这是理解 BCD 最重要的一句话:BCD 文件就是 Windows 注册表 hive 文件,用的是同一个 regf格式,可以用 reg load挂载、可以用注册表编辑器查看、可以用任何 hive 解析库读写。

它的内部结构:

复制代码
BCD (regf 文件)
└─ Objects
   ├─ {9dea862c-5cdd-4e70-acc1-f32b344d4795}      ← {bootmgr}
   │   ├─ Description
   │   │   └─ Type = 0x10100002                ← 对象类型码
   │   └─ Elements
   │       ├─ 11000001   device       (REG_BINARY, BCD 设备描述符)
   │       ├─ 12000002   path          (REG_SZ, UTF-16LE)
   │       ├─ 12000004   description   (REG_SZ)
   │       ├─ 12000005   locale        (REG_SZ)
   │       └─ 14000006   inherit       (REG_MULTI_SZ / 对象列表)
   ├─ {a5a30fa2-3d06-4e9f-b5f4-a01df9d1fcba}      ← {fwbootmgr}
   ├─ {b2721d73-1db4-4c62-bf78-c548a880142d}      ← {memdiag}
   ├─ {随机 GUID}                       ← Windows Boot Loader
   │   ├─ Description → Type = 0x10200003
   │   └─ Elements
   │       ├─ 11000001  device
   │       ├─ 21000001  osdevice
   │       ├─ 12000002  path          (\\Windows\\system32\\winload.efi)
   │       ├─ 22000002  systemroot    (\\Windows)
   │       ├─ 23000003  resumeobject
   │       └─ 25000020  nx
   └─ ...

挂载方式(在 WinRE 或正常系统中,以管理员权限):

复制代码
reg load HKLM\\BCD00000000 S:\\EFI\\Microsoft\\Boot\\BCD
regedit      # 浏览 HKEY_LOCAL_MACHINE\\BCD00000000
reg unload HKLM\\BCD00000000

离线挂载的价值在于:当 bcdedit 因为某些元素损坏而拒绝打开存储时,直接操作 hive 层可以绕过校验做修复;同时也便于做取证(例如查看某台机器历史上配置过哪些启动项)。

3.2 对象类型码

每个对象在 Description\\Type 中记录一个 32 位类型码:

对应的常见 GUID:

3.3 元素类型码的编码规则

BCD 的元素类型是一个 32 位整数,按位域划分:

Class(消费者类别):

Format(数据格式):

于是,元素类型码的高 8 位就决定了它的"命名空间归属"。举例:

  • 0x11000001 = Class 1 (Library) + Format 1 (Device) + SubType 1 → Library 类的 device 元素
  • 0x21000001 = Class 2 (Application) + Format 1 (Device) + SubType 1 → 应用类的 osdevice 元素
  • 0x25000020 = Class 2 + Format 5 (Integer) + SubType 0x20 → 应用类的 nx 元素
  • 0x12000004 = Class 1 + Format 2 (String) + SubType 4 → Library 类的 description 元素

这个编码规则非常实用 :看到任何一个陌生的元素码,你都能立即判断它的数据类型和适用范围,而不必查表。也就能理解为什么 bcdedit /set 时类型必须匹配------对 Integer 类型的元素赋字符串值会被拒绝(部分类型支持字面量,如 nx OptIn)。

3.4 常用元素速查表

Library 类(所有对象通用,前缀 0x1x):

Application 类(前缀 0x2x,仅对特定应用类型有意义):

说明: 上表类型码取自微软公开文档与 bcdedit /? TYPES 的实测输出,不同 Windows 版本间可能有增删。在生产环境使用前,建议先在本机运行 bcdedit /? TYPES <应用类型> 确认。

几个在排障中特别有用的元素:

  • safeboot(0x25000080) :设为 Minimal 后重启,等同于"强制安全模式"。当系统因驱动问题无法启动、而你又进不去高级启动菜单时,这是救命的命令:

    bcdedit /set {default} safeboot minimal

    修复完成后务必清除:

    bcdedit /deletevalue {default} safeboot

  • lastknowngood(0x26000025):使用"最后一次正确配置"。注意,LKG 的语义在现代 Windows 上已经弱化------它恢复的是上一次成功启动所用的 ControlSet,而不是完整的系统还原点。

  • nointegritychecks/ testsigning:这两个会显著降低系统安全性(关闭驱动签名强制),且它们本身是被 BitLocker 的 BCD 验证配置文件所"监视"的(见下文)。排障时可以临时开,用完必须关。

  • hypervisorlaunchtype:设为 Off 会禁用 Hyper-V/虚拟化安全特性(VBS、Credential Guard、内存完整性)。在"装了 Hyper-V 或某些沙箱/安卓子系统后无法启动"的排障中,这是常用手段。

3.5 继承机制:inherit 与全局设置对象

BCD 采用了类似注册表/组策略的"继承"设计,避免在每个条目里重复配置。默认继承链:

复制代码
{globalsettings}      所有对象都应继承的全局设置(调试、EMS、全局引导标志)
{bootloadersettings}    所有 OS Loader 条目应继承的设置
{resumeloadersettings}  所有 Resume Loader 条目应继承的设置
{emssettings}           EMS(紧急管理服务,串口控制台)设置
{dbgsettings}           调试器全局设置(串口/USB/网络 KD 参数)
{hypervisorsettings}    虚拟机监控程序设置
{badmemory}             全局内存坏块列表(继承给所有条目)
{ramdiskoptions}        RAM 磁盘选项(WinPE/WDS 场景)

一个 OS Loader 条目默认的 inherit 值通常是:

复制代码
inherit = {bootloadersettings} {globalsettings} {resumeloadersettings} {emssettings} {dbgsettings} {hypervisorsettings} {badmemory}

实践意义 :当你在某个具体条目上 bcdedit /deletevalue 删不掉某个设置时,很可能它来自继承对象。用 bcdedit /enum all 可以看到 {globalsettings} 等对象自身的内容。这也是为什么"改了设置没生效"的常见原因------被继承值覆盖了,或反过来,你在具体条目上设的值被继承链中的同名元素掩盖。

{badmemory}与内存坏块 :Windows 会在检测到内存错误后,把出错的物理页记录到 {badmemory} 对象的 0x1700000a(BadMemoryList)中,并在以后启动时避开这些页。这个机制有时会造成"越修越少的可用内存",用 bcdedit /deletevalue {badmemory} badmemorylist 可以清空(前提是故障内存已更换)。

3.6 设备描述符:BCD 如何定位一个分区

BCD 中 deviceosdevice 等元素的 Format 是 Device,其值不是简单的盘符,而是一个结构化的 设备描述符。它包含一个"设备类型 GUID",后跟类型特定的数据。

常见形态:

其中最值得关注的是 locate=。它把"BCD 指向固定的磁盘签名 + 分区偏移"变成了"启动时动态搜索"。

  • MBR 磁盘:默认使用磁盘签名(MBR 偏移 0x1B8 处的 4 字节)+ 分区起始 LBA。克隆磁盘时签名可能重复或改变,导致 BCD 指向错误。
  • GPT 磁盘:使用磁盘 GUID + 分区 GUID。更健壮,但如果用工具重新生成了分区表,同样会失效。
  • 改为 locate=的效果:启动时 bootmgfw/winload 会扫描所有可访问的分区,找到第一个含有指定路径的卷。这在"磁盘标识变了但内容还在"的场景下极为有效,代价是启动略慢,且多系统环境下可能匹配到错误的分区。

bcdedit输出中 device 显示为 unknown的含义:说明 BCD 中记录的磁盘标识,在当前系统中找不到对应分区。这是诊断"BCD 指向错误分区"最直接的信号,常见于把系统盘换到另一台机器、或克隆之后。

3.7 bcdedit 之外的操作方式

导出/导入整个存储:

复制代码
bcdedit /export C:\\BCD.bak          # 导出系统 BCD
bcdedit /import C:\\BCD.bak          # 导入

注意 /export 导出的仍是 hive 格式的二进制文件,不是可读文本;bcdedit /enum all > C:\\bcd.txt 才能得到可读快照。动手改 BCD 之前先做这两件事,是唯一值得养成的习惯。

针对离线存储操作:

复制代码
bcdedit /store S:\\EFI\\Microsoft\\Boot\\BCD /enum all
bcdedit /store S:\\EFI\\Microsoft\\Boot\\BCD /set {default} safeboot minimal

在 WinRE 中,系统 BCD 未必是默认存储(WinRE 有自己的 BCD),所以离线操作时务必显式带 /store

创建新条目(典型场景:手工添加第二个 Windows):

复制代码
bcdedit /create /d "Windows 10 - 第二个安装" /application osloader
# 会返回一个新 GUID {xxxxxxxx-...}
bcdedit /set {新GUID} device partition=D:
bcdedit /set {新GUID} osdevice partition=D:
bcdedit /set {新GUID} path \\Windows\\system32\\winload.efi
bcdedit /set {新GUID} systemroot \\Windows
bcdedit /displayorder {新GUID} /addlast

固件层启动项(NVRAM):

复制代码
bcdedit /enum firmware  # 列出固件启动项(含 Windows Boot Manager 与其他 OS)
bcdedit /set {fwbootmgr} displayorder {bootmgr} /addfirst

{fwbootmgr} 对象是 NVRAM 中 Boot#### 变量在 BCD 里的镜像。操作它等价于操作固件启动顺序。这比进 UEFI 设置界面点鼠标方便,但同样受限于固件的变量写实现质量。

3.8 多系统场景下的 BCD

理解多系统,关键是分清"谁在管菜单":

模式 A:Windows 引导管理器管菜单(Windows 优先)。

NVRAM 第一启动项是 Windows Boot Manager\\EFI\\Microsoft\\Boot\\bootmgfw.efi。要加 Linux,需要在 BCD 中增加一个指向 GRUB 的条目:

复制代码
bcdedit /create /d "Ubuntu" /application bootsector
bcdedit /set {新GUID} device partition=S:
bcdedit /set {新GUID} path \\EFI\\ubuntu\\grubx64.efi
bcdedit /displayorder {新GUID} /addlast

模式 B:GRUB/rEFInd 管菜单(Linux 优先)。

NVRAM 第一启动项指向 \\EFI\\ubuntu\\grubx64.efi\\EFI\\refind\\refind_x64.efi,由它检测 Windows 并生成 chainloader /EFI/Microsoft/Boot/bootmgfw.efi 条目。这种模式更常见,因为 Linux 安装程序默认会这样做(把 GRUB 设为第一启动项)。

模式 C:固件管菜单。 直接在 UEFI 设置里选择启动项,或用开机时的启动菜单键(F8/F11/F12 因厂商而异)。这个模式下两套 BCD/GRUB 配置互不干扰,最不容易出问题。

多系统的铁律:

  1. 不要让 Linux 的 GRUB 配置文件去"猜" Windows 的 ESP。 GRUB 的 os-prober 在某些配置下会找错 ESP,表现为 GRUB 菜单里的 Windows 项启动后报 0xc000000f
  2. Windows 大版本更新会重写 NVRAM 启动项 ,把 Windows Boot Manager 重新设为第一。这是"装完 Windows 更新后 GRUB 不见了"的原因,重新进 UEFI 设置调整顺序即可,数据不会丢。
  3. 两个 ESP 是灾难的开始。 双盘双系统时,如果两块盘各有一个 ESP,而固件选中了没有对应 BCD 的那个,就会启动失败。建议全机只保留一个 ESP。
  4. 快速启动与双系统不兼容:务必在 Windows 中关闭快速启动(控制面板 → 电源选项 → 选择电源按钮的功能 → 取消"启用快速启动"),否则 Linux 挂载 NTFS 时可能损坏数据。

3.9 BCD 与 BitLocker 的联动

BCD 内容会被纳入 BitLocker 的完整性保护。BitLocker 定义了一个"BCD 验证配置文件(BCD Validation Profile)"------一张允许存在/忽略的元素白/黑名单。默认的验证配置文件包含如下元素(前缀含义:全部 = 所有应用类型都校验,winload = 仅对 OS Loader 校验):

这张表的实践含义极其重要 :修改上述任何一个元素,都会改变 BitLocker 的度量值,从而在下一次启动时触发 BitLocker 恢复密钥界面

这解释了一个经典困扰:

"我只是开了一下测试签名(bcdedit /set testsigning on),重启就要我输 BitLocker 恢复密钥!"

原因就在这里。testsigning(0x16000049)在验证配置文件中被监视。同理,bcdedit /set {default} nx AlwaysOff/set debug on 也会触发。

正确做法

复制代码
# 修改 BCD 之前,先挂起 BitLocker
manage-bde -protectors -disable C:
  <进行 BCD 修改 / 固件升级 / 硬件变更>
manage-bde -protectors -enable C:

挂起保护的实质是:在磁盘上放置一个明文的保护器("挂起"密钥),允许本次启动绕过 PCR 校验;重新启用后该明文保护器被删除,PCR 度量值被重新记录。这比"输入恢复密钥"优雅,因为它不需要人手、且能自动重新密封。

风险提示-disable 期间机器上存在明文密钥,应在受控环境下尽快完成操作并 -enable


第四章 winload.efi:操作系统加载器

如果说 bootmgfw.efi 是"选谁来跑",那么 winload.efi 就是"把它跑起来所需的全部准备工作"。这是整条链路中工作量最大、也最容易出错的一环。

4.1 定位与加载

引导管理器确定目标条目后,读取其 device(0x11000001)与 path(0x12000002)元素。对标准 Windows 10 UEFI 安装,这两个值是:

复制代码
device  partition=C:      (即 \\Device\\HarddiskVolumeN,Windows 所在卷)
path    \\Windows\\system32\\winload.efi

引导管理器用固件的文件服务把这个文件读进内存,但它不是简单地跳转执行 ------它通过 ImgArchStartBootApplication 一类的内部例程,完成一个"受保护的启动应用移交":验证签名、准备执行环境、传递参数,然后进入 winload 的入口。

这里有一个安全上的重要设计:从 bootmgfw 到 winload 的这次加载,同样要过签名验证 。即便 Secure Boot 在固件层已经关闭,Windows 的引导管理器依然会用微软的密钥体系校验 winload.efi 的完整性。这就是所谓 Trusted Boot(可信启动):微软把自己的信任链从固件层延续到了操作系统加载器层,每一环都验证下一环。

如果这一步失败,你会看到:

复制代码
Recovery
Your PC needs to be repaired
The application or operating system couldn't be loaded because a required file is missing or contains errors.
File: \\Windows\\system32\\winload.efi
Error code: 0xc0000428

0xc0000428 的含义是 STATUS_INVALID_IMAGE_HASH ------ 镜像哈希无效,即签名校验失败。这通常意味着:

  • winload.efi 被篡改(恶意软件或错误的修补);
  • 磁盘错误导致文件读取损坏;
  • 系统文件版本与引导管理器期望的不一致(例如跨版本回滚、组件存储损坏)。

注意区分 0xc000000f 0xc0000428:前者是"找不到/打不开"(路径或 BCD 问题),后者是"找到了但不可信"(内容问题)。两者修复方向完全不同------前者查 BCD 与分区,后者跑 sfc / DISM

4.2 winload 的工作清单

进入 winload 之后,它要在没有操作系统、没有文件系统驱动、没有内存管理、没有进程概念的环境下,把整个内核运行环境搭起来。可以把它做的事分为九组:

(1)建立基础执行环境。 初始化自己的堆、异常处理、调试输出通道(串口/USB/网络 KD,由 BCD 的 {dbgsettings} 决定),初始化显示(沿用固件 GOP 提供的 framebuffer,绘制 Windows 启动 LOGO 或转圈动画)。

(2)加载并显示启动相关的图形与本地化资源。 字体(.ttf)、多语言字符串、启动动画资源(bootres.dll 中的位图/动画)。这也是为什么"启动界面变成乱码/方块"往往是资源文件损坏而非内核问题。

(3)加载并校验 SYSTEM 注册表 hive。 读取 \\Windows\\System32\\config\\SYSTEM(几十 MB 级的 hive),在内存中构建 \\Registry\\Machine\\System。这是后续几乎所有决策(加载哪些驱动、用什么启动类型、有哪些服务)的依据。

(4)解析启动配置。 读取 BCD 中该条目的全部元素、SYSTEM\\CurrentControlSet\\Control 下的内核调参数(即"控制向量",包含上百个内核调优项)、安全启动/代码完整性策略(SI Policy,来自 \\Windows\\System32\\CodeIntegrity\\SiPolicy.p7bCiPolicies\\Active\\)。

(5)加载代码完整性模块与策略。 ci.dll(Code Integrity)随 winload 一同加载,用于后续验证每一个被加载的映像。

(6)加载 HAL 与内核映像。 按 BCD 的 kernel(0x22000011,默认 ntoskrnl.exe)与 hal(0x22000012,默认 hal.dll)元素,把两个 PE 映像装入内存、完成重定位与导入解析。

(7)加载引导必需的驱动。 包括:

  • 微码更新 mcupdate.dll(CPU 微码补丁,由加载器在早期应用)
  • 调试传输 kdcom.dllkdnet.dll + kd_02_*.dll(内核调试的通信后端)
  • HAL 依赖层 与早期总线/中断支持(acpi.syspci.sysintelppm.sys 等)
  • 启动文件系统与磁盘栈 相关驱动(disk.syspartmgr.sysstorport/storahci/stornvmentfs.sysfvevol.sys 若启用 BitLocker、volsnap.sys
  • 图形 bootvid.dll(启动时的图形/蓝屏渲染)、basicdisplay.sys 或厂商 GOP 兼容驱动
  • 安全相关 sgrmagent.sys(System Guard 运行时监视代理)、mssecflt.sys(安全事件组件微筛选器)
  • ELAM 驱动WdBoot.sys 等,见 4.9)
  • 其他 Start=0(boot start)驱动,按服务组顺序加载

(8)构建内存与页表结构。 这是 winload 最"内核味"的工作:建立物理内存描述符列表、构建初始页表(PML4/PDPT/PD/PT 层级)、把内核映像映射到高半核(典型 x64 Windows 内核基址在 0xFFFFF800'00000000 之后)、建立恒等映射的过渡区域、设置 LoaderBlock。

(9)移交控制权。 调用 ExitBootServices()、调用 SetVirtualAddressMap() 搬迁运行时服务、然后跳转到内核入口。

4.3 PE 映像是怎么被"加载"的

理解第(6)步的"加载一个 PE 映像",对理解驱动签名、导入依赖、以及某些蓝屏成因很关键。流程如下:

  1. 读头部 ,确认 MachineIMAGE_FILE_MACHINE_AMD64、子系统是 IMAGE_SUBSYSTEM_NATIVE(值为 1,驱动程序与内核用),Magic0x20b(PE32+)。
  2. 申请内存 :按 SizeOfImage 申请连续页。注意此时还没有虚拟内存管理器,用的是加载器自己的页分配器(在 UEFI 环境下基于 AllocatePages)。
  3. 铺开各节 :按节头的 PointerToRawData/SizeOfRawData 从文件拷贝,按 VirtualAddress/VirtualSize 放到内存中;VirtualSize 大于 SizeOfRawData 的部分(.bss 类未初始化数据)清零。
  4. 处理重定位 :如果实际加载基址 ≠ ImageBase,遍历 .reloc 表按类型(x64 上主要是 IMAGE_REL_BASED_DIR64)修正。内核与 HAL 通常能被加载到首选基址,但驱动几乎都要重定位。
  5. 解析导入表 :对每个依赖模块,确认它已经在加载列表中;把导入地址表(IAT)中的每一项填成目标函数的实际地址。这里有个重要约束:驱动的导入依赖必须能被满足。 如果驱动 A 依赖驱动 B 的导出,而 B 还没加载或加载失败,A 的加载就失败------这是"驱动加载顺序组"(ServiceGroupOrder + 每个服务的 Group/Tag)存在的根本原因。
  6. 校验映像 :调用 ci.dll 验证签名(Authenticode + 目录签名 .cat),并检查是否被安全策略(如 HVCI、WDAC 策略)允许。
  7. 登记进加载器列表 :把它加入到加载器维护的"加载顺序链表"(LoadOrderListHead)与"启动驱动链表"中。这个链表最终会通过 LoaderBlock 交给内核,内核据此构建 PsLoadedModuleList

一个常见的蓝屏成因就来自第 6 步 :驱动文件本身完好,但它的目录签名 .cat 文件丢失或组件存储中的 catalog 损坏,导致校验失败,报 0xc0000428 或在内核阶段报 0xc0000221(STATUS_IMAGE_CHECKSUM_MISMATCH)。

4.4 SYSTEM hive 与 ControlSet 的选择

\\Windows\\System32\\config\\SYSTEM 是启动期最关键的一个文件。winload 把它整体读入内存,并在其中做几件决定性的事:

(1)选择 ControlSet。 SYSTEM hive 下通常有多个 ControlSet001ControlSet002,以及一个易失的 CurrentControlSet 符号链接。选择逻辑由 SYSTEM\\Select 键决定:

复制代码
Select
  Current  = 1     → 本次启动用 ControlSet001
  Default  = 1     → 下次默认用 ControlSet001
  Failed   = 2     → 上一次启动失败的是 ControlSet002
  LastKnownGood = 2
  • 正常启动时,Current = Default
  • 若上一次失败(或用户选择了"最后一次正确配置"),Current 会被设为 LastKnownGood
  • 启动成功后,内核会把当前使用的 ControlSet 复制一份作为新的 LastKnownGood。

(2)读取内核调参(控制向量)。 ControlSet00N\\Control 下的上百个值决定了:SystemStartOptions(BCD 传入的启动选项字符串)、Session ManagerBootExecute、内存管理参数、分页文件设置)、ServiceGroupOrderGroupOrderListVideoCrashControlProductOptionsMiniNT(WinPE 标记)等。

(3)枚举启动驱动。 遍历 ControlSet00N\\Services 下所有 Start = 0(SERVICE_BOOT_START)的服务,读出其 ImagePathGroupTagTypeErrorControl,构建加载队列。

如果 SYSTEM hive 损坏 ,你会看到 0xc0000001 或更典型的 0xc0000218(STATUS_REGISTRY_HIVE_RECOVERED / hive 损坏相关)。修复手段是从 \\Windows\\System32\\config\\RegBack\\ 恢复旧副本(注意:Windows 10 1803 之后,系统默认不再自动备份注册表到 RegBack,所以这个目录可能是空的),或使用系统还原点。

4.5 页表与内存布局:winload 最"硬核"的部分

在 x64 上,Windows 使用四级页表(PML4 → PDPT → PD → PT),每级 512 项 × 8 字节 = 4KB 一页,每级覆盖 9 位虚拟地址,共 48 位虚拟地址空间(Windows 实际只用其中的一部分),页大小 4KB(大页 2MB/1GB 用于特定映射)。

winload 在移交前建立的映射,大致要满足:

  • 内核映像映射 :把 ntoskrnl.exe 按其 ImageBaseSystem 区域,典型 0xFFFFF800'00000000 起,且启用 ASLR 时有随机偏移)映射到内核虚拟地址空间的高半部
  • 恒等映射 / 过渡映射:由于从 winload 跳转到内核时,CPU 必须先开启内核自己的 CR3,而跳转指令本身还在当前地址空间执行,需要有一段在两个地址空间中都有效的代码过渡。加载器会建立必要的临时映射,保证最后那条跳转指令不会"跳进虚空"。
  • HAL 与所有已加载驱动的映射:同样映射到系统空间。
  • 物理内存的全映射 :内核需要能访问任意物理页(用于 PFN 数据库、分页池等),加载器建立起物理内存描述符列表(Memory Descriptor List),描述每一段物理内存的类型(LoaderFreeLoaderLoadedProgramLoaderFirmwareTemporaryLoaderOsloaderStackLoaderSystemBlockLoaderBad 等)。
  • ACPI/固件表保留区:ACPI 表、SMBIOS、UEFI 运行时服务代码/数据区被标记为保留,内核不得覆盖。
  • 帧缓冲区:GOP 的 framebuffer 被保留并映射,以便内核与 bootvid 继续往同一个地址画东西(这就是为什么从启动 LOGO 到登录界面的过渡可以不闪黑)。

一个关键细节:内核的 ASLR 是在这里做的。 每次启动,winload 会随机选择内核映像的加载偏移(KASLR 的一部分)。这使得 rootkit 无法硬编码内核地址,也是前文提到的 bootkit 必须通过挂钩 ExitBootServices / OslArchTransferToKernel 去"现找"ntoskrnl 基址的原因------因为地址每次都不一样。

4.6 加载器参数块(LoaderBlock):交接的"信封"

winload 与内核之间传递的是一个巨大的结构体,通常称为 LoaderBlock (内核侧符号 KeLoaderBlock,类型 LOADER_PARAMETER_BLOCK 的加载器版本)。它承载了内核在 Phase 0 之前无法自己获取的一切。主要字段族包括:

为什么 LoaderBlock 如此重要? 因为在它交接完成之前,内核是没有"自我认知"的:它不知道自己被加载到哪里、不知道内存多大、不知道有哪些驱动、不知道磁盘在哪。LoaderBlock 就是内核的"出生证明"。

同时它也成了攻防焦点:任何能在 OslArchTransferToKernel 之前篡改 LoaderBlock 的代码,都能改变内核看到的现实(插入伪造的驱动、修改内存描述符、伪造安全启动状态)。这解释了为什么现代防护(Secure Launch / DRTM、HVCI、System Guard)会在这条路径上层层设防。

4.7 最后的移交:三行代码的距离

移交过程的实质,可以压缩到三个动作:

复制代码
// 概念化伪代码,非真实实现
Status = OslFwpKernelSetupPhase1(...);   // 内含 gBS->ExitBootServices(ImageHandle, MapKey)
                                          // 以及 gRT->SetVirtualAddressMap(...) 的准备工作
if (NT_SUCCESS(Status)) {
    OslArchTransferToKernel(LoaderBlock, KernelEntry);   // 不再返回
}

第一步: ExitBootServices 前文已述。此处补充两点:

  • 调用前必须先用 GetMemoryMap() 拿到最新的 MapKey。如果在两者之间发生了任何内存分配,MapKey 过期,调用返回 EFI_INVALID_PARAMETER,加载器需要重试(重新取映射、再调用)。这也是某些恶意驱动插入分配后容易造成启动失败的原因。
  • 调用成功后,所有 Boot Services 函数指针立即失效。唯一还能用的只有 Runtime Services。

第二步: SetVirtualAddressMap 遍历内存映射中标记为 Runtime 的描述符,把它们的 VirtualStart 填上内核分配的虚拟地址,通知固件"以后请按这些虚拟地址访问"。固件驱动借此修正自己的内部指针。此后 Windows 通过 gRT 调用 GetVariable/SetVariable/ResetSystem 等。

第三步:跳转。 加载 CR3(切换到内核页表)、设置栈、把 LoaderBlock 的地址按约定放进寄存器(x64 调用约定下通常是 RCX 或由加载器与内核约定的寄存器/全局),然后跳到 ntoskrnl 的入口点------也就是 KiSystemStartup

从此,winload 的代码不再存在。 它所在的内存会被内核在 Phase 1 的 Phase1InitializationDiscard 中连同 INIT 段一起丢弃(释放回可用内存池)。

4.8 winload 阶段的度量与安全

(1)PCR 11 与 BitLocker 的解锁。

如果系统盘被 BitLocker 加密,winload 必须在加载内核之前把磁盘密钥解出来。流程:

  1. bootmgfw 从磁盘的 BitLocker 元数据区(位于分区头部的未加密区域)读出加密的 VMK(Volume Master Key)。注意:元数据区与引导文件都在未加密的"引导块"里,否则固件根本没有文件系统驱动能读加密区)。
  2. 向 TPM 请求解封(Unseal)VMK。TPM 只在当前 PCR 值与密封时记录的一致时才释放密钥。
  3. 用 VMK 解开 FVEK(Full Volume Encryption Key) ,此后磁盘栈(fvevol.sys)可以透明解密读写。

PCR 校验配置文件(Platform Validation Profile) 决定了"哪些 PCR 必须一致":

可用 manage-bde -protectors -get C: 查看当前生效的配置:

复制代码
TPM:
  ID: {85825FF8-3733-48D0-B0EE-4D32D8AAFD7A}
  PCR Validation Profile: 7, 11
    (Uses Secure Boot for integrity validation)

这里有一个值得深入理解的安全权衡:

  • PCR 7 只度量"Secure Boot 策略",不度量引导器二进制本身。 也就是说,只要 Secure Boot 处于开启状态、密钥库未被改动,任何合法签名的引导器都能让 PCR 7 保持不变。这正是"为什么用 PCR 7+11 比 0+2+4 更不容易在固件更新后进恢复模式"的原因,同时也是它的弱点:若某个被合法签名但存在漏洞的引导器(例如带有 CVE 的老版 shim)被换上,PCR 7 不会察觉。
  • PCR 11 由 Windows 引导管理器扩展,用于"锁定"VMK 的派生。 关键点在于:引导管理器在把控制权交给 winload 之后,会往 PCR 11 扩展一个值,使得此后任何代码都无法再从 TPM 解出正确的 VMK。这保证了即使操作系统层被完全攻破,攻击者也拿不到那把密钥。
  • PCR 0/2/4 度量具体固件与引导器哈希,更严格,但任何固件更新、引导管理器更新(微软的安全更新会替换 bootmgfw.efi)都会改变这些值,从而触发 BitLocker 恢复。这是"可用性与严格性"的典型取舍。

(2)PCR 12/13/14 与 ELAM。

  • winload 把 ELAM(Early Launch Anti-Malware)策略的哈希写入 PCR 13,并在加载每个 boot-start 驱动时把驱动镜像的摘要也扩展进去。这构成了"启动模块详情"的完整记录。
  • 若 ELAM 驱动判定某个启动驱动为恶意,除了阻止其加载外,还可以调用 Tbsi_Revoke_Attestation(),往 PCR 12 扩展一个未指定的值并递增事件计数器,从而主动破坏本机的可信状态,使远程证明服务器拒绝为这台机器签发健康证明。这是"即使攻击成功也无法伪装成健康"的设计。
  • PCR 14 记录"启动权威(Boot Authorities)",即在验证内核驱动时被考虑的公钥集合。远程证明可以借此判断机器上是否存在"自定义内核签名者(Custom Kernel Signers)"策略------这是反作弊与高安全场景关注的点。

(3)ELAM 驱动本身。

ELAM(早启动反恶意软件)自 Windows 8 引入。其要点:

  • 它是一个 boot-start 驱动,属于特殊的 Early-Launch 加载顺序组 (注意:这个组不在 ServiceGroupOrder 的列表里,是加载器特判的)。
  • 签名要求:必须由带有 Early Launch EKU( 1.3.6.1.4.1.311.61.4.1 的代码签名证书签署(另加常规 Code Signing EKU 1.3.6.1.5.5.7.3.3)。
  • 它为每个后续的 boot-start 驱动返回一个分类:Good / Bad / Unknown
  • 策略存储在 HKLM\\SYSTEM\\CurrentControlSet\\Policies\\EarlyLaunch\\DriverLoadPolicy;默认策略下,Good、Unknown、以及"Bad 但被标记为 Critical(阻止会导致系统无法启动)"的驱动会被加载,普通 Bad 驱动被跳过。
  • 恶意特征数据存储在 HKLM\\ELAM hive(文件为 \\Windows\\System32\\config\\ELAM),由 winload 与 ELAM 驱动一同加载,且必须带签名供 ELAM 驱动校验。
  • Windows 默认的 ELAM 驱动是 Windows Defender 的 WdBoot.sys

4.9 两条变体路径:休眠恢复与 WinPE

(1) winresume.efi------ 休眠与快速启动。

与 winload 并列,winresume.efi 负责从 hiberfil.sys 恢复系统。它的工作要简单得多:

  1. 读取 BCD 中 resume 对象的 filedevice(0x21000001)/ filepath(0x22000002),定位 hiberfil.sys
  2. 校验休眠映像的完整性(签名与校验和),若开启 BitLocker,同样要走 TPM 解封流程。
  3. 恢复内存映像:把休眠文件中保存的内核态页面写回物理内存,恢复处理器状态(CR3、CR4、GDT/IDT 寄存器、浮点/SIMD 状态等)。
  4. 跳转到内核的休眠恢复入口,由内核完成设备状态恢复。

快速启动时,休眠映像里保存的是什么? 只有内核态:内核映像自身的状态、已加载驱动的状态、以及部分系统缓存。用户会话被完全关闭(所有用户进程终止、用户配置注销),所以快速启动后你仍需重新登录、重新打开程序,但内核与驱动已经"预热"好了。

这也带来一个排障要点:如果问题出在驱动初始化(例如某个驱动偶发初始化失败),快速启动会把它"冻"进休眠文件里反复复现;而"重启"(restart)走的是完整冷启动,往往能解决问题。 这就是为什么技术支持的第一句话总是"你试过重启吗"------restart 与 shutdown+power on 在 Windows 10 上不是一回事。

(2)WinPE / 恢复环境。

WinRE 与安装环境走的是另一套结构:

  • 引导管理器加载的仍是 winload.efi,但 BCD 条目带有 winpe(0x26000022)= Yes 标记。
  • systemroot 指向 \\Windowsdevice/osdevice 是一个 RAM 磁盘ramdisk=[C:]\\sources\\boot.wim,{ramdiskoptions})。
  • winload 把 WIM 映像整体读入内存的一个 RAM 磁盘,然后从中启动一个精简的 Windows。

WinRE 的配置独立于主系统,其 BCD 通常位于 \\EFI\\Microsoft\\Recovery\\BCD(ESP 上),由 reagentc 管理:

复制代码
reagentc /info              # 查看 WinRE 状态与位置
reagentc /enable            # 启用(会注册到主 BCD 的 recoverysequence)
reagentc /disable

BCD 中的 recoverysequence(0x14000008)与 recoveryenabled(0x16000009) :前者是"引导失败后自动尝试的恢复条目 GUID",后者是总开关。二者配合 {bootmgr}displayorder/toolsdisplayorder,构成了"连续启动失败自动进 WinRE"的机制。

4.10 winload 阶段的错误码速查

排障的一般路径(按"从外到内、从便宜到昂贵"的顺序):

复制代码
1. 确认硬件可见:diskpart → list disk / list volume
     看不到盘 → 硬件/固件/控制器问题,先解决这个
2. 检查文件系统:chkdsk C: /f /r
3. 检查系统文件:sfc /scannow /offbootdir=C:\\ /offwindir=C:\\Windows
4. 修复组件存储:DISM /Image:C:\\ /Cleanup-Image /RestoreHealth
5. 重建引导文件:bcdboot C:\\Windows /s S: /f UEFI
6. 重建 BCD:ren BCD BCD.old 后再 bcdboot(bcdboot 会生成新的)
7. 最后手段:系统还原 / 重置此电脑(保留个人文件)/ 重装

第五章 内核启动:ntoskrnl.exe 与 hal.dll

5.1 先修正一个流传很广的说法

用户的原始描述是:内核启动:ntoskrnl.exe(系统内核主体)、hal.dll(硬件抽象层)

这个说法在 Windows 10 之前是准确的,但在 Windows 10 2004(版本 2004 / 20H1)之后已经过时了

事实是:从 Windows 10 2004 开始,HAL 的实现被静态链接进了 ntoskrnl.exe。磁盘上那个 hal.dll仍然存在,但它只是一个向后兼容的存根(stub)。

证据非常直观------比较文件大小:

为什么这么做?回顾历史:

  • Windows NT 时代 :HAL 是真正独立的模块。安装介质上有多个 HAL 文件(hal.dllhalacpi.dllhalapic.dllhalmacpi.dllhalmps.dll 等),安装程序根据"是否有 ACPI""是否有 APIC""是否多处理器"选择其中一个,重命名为 hal.dll
  • Windows 8:x86 平台也统一为单一 HAL(x64 平台一直只有一个)。多个 HAL 文件被合并为一个运行时自适应的实现。
  • Windows 10 2004:HAL 与内核彻底合并,静态链接。

合并的动机是性能与安全

  1. 减少跨模块调用开销。 HAL 函数(如 HalRequestSoftwareInterruptKeGetCurrentIrql 相关路径、I/O 端口访问、时钟/中断控制)是内核最热的调用路径之一。静态链接后可以被内联、被优化,不必再通过导入表间接跳转。
  2. 消除一个攻击面。 HAL 作为一个独立映像,理论上可以被单独替换或挂钩。合并后不再有这个独立入口。
  3. 简化映像加载。 加载器少加载、少重定位、少校验一个映像。

那么 BCD 里的 hal元素(0x22000012)还有用吗? 有。加载器依然会按这个元素加载文件(保持行为兼容、也方便极端情况下的替换调试),但被加载的那个文件不再包含 HAL 主体代码。

这一修正在实践中的意义:

  • 当你看到某些资料说"ntoskrnl.exe 和 hal.dll 是两个独立的内核组件"时,要知道那是 2019 年以前的模型。
  • 当你用 lm 命令在 WinDbg 里看已加载模块时,会看到 hal 依然作为一个模块存在------那是内核为兼容旧驱动(驱动可能导入 HAL 导出函数)而保留的导出转发层。
  • 恶意软件分析时,如果你在内存里发现一个"看起来像 HAL 但不在预期位置"的模块,那才是可疑的;正常的 HAL 代码已经在 ntoskrnl 的 .text 段里了。
  • 蓝屏分析时,hal.dll 出现在调用栈中不代表 HAL 有问题,它可能只是被转发的内联函数符号。真正要看的是调用栈上更靠近栈顶的第三方驱动。

综上,对现代 Windows 10/11 更准确的描述是:

复制代码
内核启动:ntoskrnl.exe
  ├─ 内核层(Kernel):线程调度、中断/异常分发、DPC、自旋锁、多处理器同步
  ├─ 硬件抽象层(HAL):已静态链接进内核,负责中断控制器、时钟、I/O 端口、多处理器启动等平台差异
  └─ 执行体(Executive):对象管理、内存管理、I/O 管理、进程管理、安全引用监视器、PnP/电源、配置管理、缓存管理
     (hal.dll 作为兼容存根被加载,提供旧驱动的导入转发)

5.2 入口点:KiSystemStartup

winload 跳转的目标是 ntoskrnl.exe 的入口函数 KiSystemStartup(不同版本中入口名与包装层有差异,这是引导处理器路径的统称)。

此刻的机器状态:

  • CPU 处于 64 位长模式,分页已开启(内核自己的 CR3)
  • 中断被禁用
  • 只有一个处理器在跑(引导处理器 BSP)
  • 没有线程调度(调度器数据结构还没建)
  • 没有进程对象(连 Idle 都没有)
  • 但 LoaderBlock 已经就绪,内存描述符、已加载模块列表、SYSTEM hive 都在

KiSystemStartup 及其后续调用链的骨架:

复制代码
KiSystemStartup(Prcb / LoaderBlock)
  ├─ 初始化处理器状态:IDT、GDT、TSS、PCR(Processor Control Region)
  ├─ HalInitializeProcessor()  ← 为当前 CPU 初始化 HAL 侧的 PCR 与处理器间中断向量
  ├─ KiInitializeKernel()      ← 内核初始化主函数(每个 CPU 都会走一遍)
  │    ├─ 引导处理器上:初始化共享的内核数据结构、链表头、空闲线程/进程对象
  │    ├─ 读取并应用 KASLR 相关的映像基址信息
  │    ├─ 检查是否启用虚拟化(BCD 的 hypervisorlaunchtype)与 CPU 是否支持硬件虚拟化
  │    ├─ 首次调用:InitBootProcessor()   ← 阶段 0 的总调度
  │    └─ 后续处理器(AP):只调用 HalInitSystem()
  └─ 当前执行流"蜕化"为 Idle 线程(KiIdleLoop)

"蜕化为 Idle 线程" 是一个精妙的设计:内核不需要专门创建一个 Idle 线程再调度过去------它直接让当前这段执行流,在完成初始化后,变成 0 号 CPU 的空闲线程。此后,当没有其他可运行线程时,CPU 就回到这条执行流上,执行 KiIdleLoop(不断执行 halt 指令等待中断)。

5.3 阶段 0(Phase 0):关中断下的骨架搭建

阶段 0 的核心特征:中断关闭、单处理器、不提供任何可被中断/重入的服务。 它的目标只有一个------建立"足以运行阶段 1"的最小数据结构集。

主调度函数是 InitBootProcessor()(早期版本为 ExpInitializeExecutive())。它按大致如下顺序推进:

(1) HalInitSystem(0) 让 HAL 在任何进一步初始化之前拿到控制权。它的职责包括:为每个 CPU 准备中断控制器、配置用于 CPU 时间记账的间隔时钟定时器中断。注意 HAL 初始化分两次调用,HalInitSystem(0) 是阶段 0 版本,HalInitSystem(1) 在阶段 1 中调用以开启中断。

(2)处理 burnmemory启动选项。 若 BCD 中设置了 truncatememory(0x15000007),丢弃指定数量的物理内存。这是内核开发者用来模拟低内存环境的手段。

(3)NLS(国家语言支持)表的早期初始化。 仅初始化到足以做 Unicode ↔ ANSI/OEM 转换的程度------因为后续要用它来打印调试字符串、处理路径。

(4)建立系统根路径与崩溃字符串缓存。 在内核映像中搜索蓝屏所需的消息字符串位置并缓存下来。原因很实际:崩溃时系统状态不可靠,不能指望再去查表。

(5)初始化配额机制、读取"控制向量"。 控制向量是 HKLM\\SYSTEM\\CurrentControlSet\\Control 下的 150+ 个内核调优项,包括授权数据、版本信息、以及各种系统行为开关。

(6)调用各子系统的阶段 0 初始化:

(7)关键一步:创建 System 进程与阶段 1 线程。 进程管理器创建了名为 Idle 的初始进程对象,然后创建 System 进程(PID 4)和一个系统线程,线程入口是 Phase1Initialization这个线程此时还不会运行------因为中断仍然关闭,调度器还不能抢占当前执行流。

(8)回到 KiInitializeKernel,分配 DPC 栈,然后进入 Idle 循环。 一旦进入 Idle 循环,调度器开始工作,那个等待中的 Phase1Initialization 线程就获得了 CPU,阶段 1 开始。

理解阶段 0 → 阶段 1 的切换,是理解"为什么某些东西不能在驱动入口里做"的关键。 阶段 0 时:没有线程调度、没有中断、除了非分页池几乎没有内存服务。所以任何驱动代码(它们最早在阶段 1 才被初始化)都不可能运行在阶段 0。这也是为什么 DriverEntry 里不能假设"系统已经完全就绪"------尤其是不能假设其他驱动已经加载、不能假设分页可用(若驱动被标记为在分页可用前加载)。

5.4 阶段 1(Phase 1):从骨架到可用系统

阶段 1 由 Phase1Initialization 线程驱动,此时中断已可开启、调度器已工作、可以创建线程、可以使用更多内存服务。

其推进顺序(不同版本差异较大,以下为逻辑主线):

  1. Phase1InitializationDiscard:释放内核映像的 INIT 段。内核为了省内存,把只运行一次的初始化代码放在单独的 INIT 段,用完即弃。这也是为什么用 WinDbg 做早期断点时,某些初始化函数"过一会儿就找不到了"。
  2. 提升线程优先级到 31(最高),防止被抢占------初始化不应被打断。
  3. 建立 NUMA / 处理器组拓扑 :在逻辑处理器与处理器组之间建立优化的映射关系(考虑 NUMA 局部性与距离),除非被 BCD 的 numproc/groupawareness 等设置覆盖。
  4. HalInitSystem(1):让 HAL 准备接收设备中断并开启中断。这是系统从"关中断的初始化态"进入"正常可中断运行态"的分界点。
  5. 启动图形界面 :调用启动视频驱动(bootvid.dll),显示 Windows 启动画面(默认黑底 + 转圈动画)。若使用了 quietboot 选项则跳过;若启用 sos 选项,会在屏幕上打印加载的每个驱动与版本信息、处理器数量、内存大小。
  6. 电源管理初始化PoInitSystem)。
  7. 初始化系统时间 :调用 HalQueryRealTimeClock() 读取 CMOS/RTC,记录为"系统启动时间"。
  8. 启动其余处理器KeStartAllProcessors() + HalAllProcessorsStarted()。引导处理器通过发送 Startup IPI(SIPI) 逐个唤醒应用处理器(AP),每个 AP 从实模式的一段 trampoline 代码开始,切换到长模式,走各自的 KiSystemStartupHalInitializeProcessorKiInitializeKernel(非引导路径,只调 HalInitSystem)→ 进入 Idle 循环。实际启动的处理器数量受物理数量、SKU 授权、BCD 的 numproc/onecpu 选项共同限制。
  9. 对象管理器创建命名空间 :根 \\\\ObjectTypes\\Global??,创建 \\DosDevices 符号链接。
  10. 执行体创建对象类型:信号量、互斥体、事件、定时器、以及 I/O 管理器的设备、驱动、控制器、适配器、文件对象。
  11. 内核调试库完成初始化(若之前未触发)。
  12. 安全引用监视器 创建 \\Security 目录,初始化审计结构。
  13. 创建 \\SystemRoot符号链接
  14. 内存管理阶段 1MmInitSystem(1)):创建 \\Device\\PhysicalMemory section 对象,创建系统工作线程,映射 NLS 表到系统空间,映射 ntdll.dll 到系统地址空间。
  15. I/O 管理器初始化与启动驱动加载IoInitSystemIopInitializeBootDrivers):这是阶段 1 中对外表现最明显的一步------按 ServiceGroupOrder 与每个服务的 Group/Tag 顺序,逐个初始化 winload 已经加载到内存中的 boot-start 驱动。每个驱动被初始化前,PnP 管理器会先咨询 ELAM 驱动,按其返回的分类决定是否放行(见 4.8 节)。
  16. 最后:创建会话管理器进程 smss.exe 内核等待它最多约 5 秒,若 smss 在此期间崩溃,系统直接 bugcheck(这就是 0xc000021a 中"关键系统进程终止"的一类典型来源)。

5.5 从内核态到用户态:smss 之后

内核自身并不创建"Windows 桌面"。它只创建第一个用户态进程,之后的所有用户态都由用户态进程自己拉起来。链条如下:

复制代码
内核 Phase 1
  └─ 创建 smss.exe(Session Manager Subsystem,第一个用户态进程,不受会话管理)
       ├─ 执行 BootExecute:autochk(磁盘检查)、chkdsk 计划任务
       ├─ 处理 PendingFileRenameOperations(延迟的文件重命名/删除,Windows 更新的核心机制)
       ├─ 加载剩余注册表 hive(SAM、SECURITY、SOFTWARE、DEFAULT)
       ├─ 创建 \\KnownDlls、\\DosDevices 等对象目录
       ├─ 启动子系统:csrss.exe(Client/Server Runtime Subsystem,Win32 子系统用户态部分)
       ├─ 启动 wininit.exe(前一个会话/系统的初始化进程)
       └─ smss 随后自行退出(它是一次性的引导进程)
            wininit.exe
              ├─ 创建 services.exe(SCM,服务控制管理器)
              │    └─ 按 Start=2 (AUTO_START) 加载自动启动服务与驱动
              ├─ 创建 lsass.exe(本地安全认证子系统:认证、LSA 策略、SAM/AD 访问)
              ├─ 创建 lsm.exe / 会话相关(版本相关)
              └─ 创建 winlogon.exe
                   ├─ 加载 LogonUI(凭据提供程序 UI)
                   ├─ 认证成功后,由 userinit.exe 启动用户 shell
                   └─ explorer.exe(图形 shell)→ 桌面出现

理解这条链的实践价值:

  • "explorer 没起来但能看到鼠标" → 问题在 userinit/shell 层,内核是好的。可以用任务管理器 Ctrl+Shift+Esc → 运行新任务 → explorer.exe
  • "卡在欢迎界面很久" → 常见原因是 SCM 加载某个服务超时(服务启动超时默认 30 秒/个),或组策略脚本、网络位置感知等待网络。
  • "登录后又注销" → 典型是 userinit 被恶意软件篡改,或 Shell 注册表值被改(检查 HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\Shell 应为 explorer.exe)。
  • smss.exe有多个实例是正常的:一个主实例 + 每个会话一个子实例(子实例启动后会短暂存在)。

5.6 内核阶段的蓝屏定位

一旦进入内核,错误就以**停止码(Stop Code)**的形式呈现。按发生的子阶段归类:

0x7B值得单独说一句,因为它是"装完系统换 SATA 模式"或"新机器首次开机"最常见的蓝屏:

Windows 只会加载"启动所必需的"存储驱动。若安装时是 RAID/RST 模式,注册表里 Start=0 的驱动就是 RAID 驱动;此时把固件改成 AHCI,系统起来后找不到原来的控制器,磁盘栈建不起来,于是 0x7B

修复办法(离线注入驱动):

复制代码
# 在 WinRE 中,假设 Windows 在 D:
dism /image:D:\\ /add-driver /driver:E:\\Drivers\\iaStorVD.inf
# 或者启用通用 AHCI 驱动(多数情况已内置):
reg load HKLM\\OFFSYS D:\\Windows\\System32\\config\\SYSTEM
reg add HKLM\\OFFSYS\\ControlSet001\\Services\\msahci /v Start /t REG_DWORD /d 0 /f
reg add HKLM\\OFFSYS\\ControlSet001\\Services\\pciide /v Start /t REG_DWORD /d 0 /f
reg add HKLM\\OFFSYS\\ControlSet001\\Services\\iaStorV /v Start /t REG_DWORD /d 0 /f
reg unload HKLM\\OFFSYS

现代 Intel 平台上更常见的则是 VMD(Volume Management Device)控制器 开关导致的 0x7B------第 11/12 代酷睿及之后的部分笔记本,BIOS 中"Enable VMD controller" 开关一旦改变,就必须重装或注入对应驱动。这是 OEM 预装机器上非常典型的一类"重置 BIOS 后就蓝屏"。


第六章 端到端时间线与状态快照

6.1 完整时间线

把前五章串成一条时间线(典型 UEFI + Windows 10,NVMe SSD,快速启动开启,耗时为常见范围):

6.2 各阶段的"环境状态"对照表

这是理解"为什么某些事只能在某个阶段做"的核心表格:

这张表解释了很多"为什么":

  • 为什么 BCD 必须在 FAT 分区上?→ 因为 bootmgfw 阶段只能用固件的 FAT 驱动。
  • 为什么不能把 winload.efi 放在 NTFS 分区上?→ 可以,因为 winload 自带 NTFS 读取能力;但 bootmgfw 不行,所以 bootmgfw 与 BCD 必须在 ESP(FAT32)。
  • 为什么驱动不能在 DriverEntry 里做复杂初始化?→ 因为它们运行在 Phase 1,此时很多子系统才刚初始化。
  • 为什么 ExitBootServices 之后不能再分配引导服务内存?→ 因为 Boot Services 已经不存在了。
  • 为什么内核还能读 UEFI 变量?→ 因为 Runtime Services 通过 SetVirtualAddressMap 保留了。

第七章 故障定位手册:从现象到根因

7.1 第一原则:先定位"死在哪一段"

面对一台起不来的机器,最有价值的一步不是急着敲命令,而是判断控制流停在了链路的哪一段。判别依据只有三个:屏幕内容、错误码前缀、以及按键响应。

7.2 引导错误码(0xc0000xxx)决策表

这些是 bootmgfw / winload 阶段抛出的错误,出现在深色背景的 WinRE 风格界面上。它们不是蓝屏,也不是内核错误,处理手段与蓝屏完全不同。

7.3 标准修复流程(UEFI/GPT)

以下流程按"可逆性从高到低、破坏性从低到高"排列。每一步之间都应尝试重启验证,不要一次性全跑完。

准备:进入 WinRE

  • 方法一:连续三次强制断电(在 Windows LOGO 出现时长按电源键 5--10 秒,重复三次),第四次开机自动进恢复环境。
  • 方法二:从 Windows 安装 U 盘启动 → 语言选择后点"下一页" → 左下角"修复计算机" → 疑难解答 → 高级选项。
  • 方法三:正常系统中 设置 → 恢复 → 高级启动 → 立即重新启动,或命令行 shutdown /r /o /t 0

第 1 步:确认磁盘与分区可见

复制代码
diskpart
  list disk
  select disk 0
  list volume
  exit

要识别出来三样东西:

  • ESP:FAT32,约 100 MB,通常无盘符。记下卷号。
  • Windows 分区:NTFS,容量最大,通常有 Windows 目录。
  • (可选)恢复分区:NTFS,约 500 MB--1 GB。

list disk 看不到系统盘 → 这是硬件/固件问题,停止软件修复,先解决磁盘可见性(控制器模式、线缆、驱动器本身)。

第 2 步:给 ESP 分配盘符

复制代码
diskpart
  list volume
  select volume N        ← ESP 的卷号
  assign letter=S
  exit

第 3 步:检查 BCD 内容

复制代码
bcdedit /store S:\\EFI\\Microsoft\\Boot\\BCD /enum all /v

重点看:

  • {bootmgr}device / path 是否正确
  • Windows Boot Loader 条目的 deviceosdevice 是否指向真实分区,是否显示为 unknown
  • path 是否为 \\Windows\\system32\\winload.efi

第 4 步:检查文件系统与系统文件

复制代码
chkdsk C: /f /r
sfc /scannow /offbootdir=C:\\ /offwindir=C:\\Windows
DISM /Image:C:\\ /Cleanup-Image /RestoreHealth

注意 WinRE 中的盘符可能与正常系统不同(常见 C: 变成 D:)。用 diskpart list volumedir 确认,不要凭感觉。

第 5 步:重建引导文件(最有效的一步)

复制代码
bcdboot C:\\Windows /s S: /f UEFI

这条命令会:复制整套 EFI 引导文件到 ESP、生成新的 BCD、并刷新 NVRAM 启动项。它通常能一次性解决 0xc000000f / 0xc0000225 / 0xc000014c 这一类问题。

第 6 步:顽固 BCD 的处理(保留备份)

复制代码
cd /d S:\\EFI\\Microsoft\\Boot
attrib BCD -s -h -r
ren BCD BCD.old
bcdboot C:\\Windows /s S: /f UEFI

永远用 ren而不是 del BCD.old 保留了原配置,可以在事后离线挂载出来比对,确认"原来的配置到底哪里错了"------这在反复启动失败的场景里是重要的诊断线索。

第 7 步:bootrec 的适用边界

复制代码
bootrec /fixmbr        ← 仅 Legacy/MBR 有意义;UEFI/GPT 上不适用
bootrec /fixboot       ← UEFI 环境常报 "Access is denied"(因为 ESP 上没有可写的引导扇区概念)
bootrec /scanos        ← 扫描已安装的 Windows,UEFI 上有用
bootrec /rebuildbcd    ← 尝试重建 BCD 条目,UEFI 上有用但不如 bcdboot 彻底

很多人不知道的一点bootrec /fixmbr/fixboot 是为 Legacy BIOS 设计的。在 UEFI/GPT 机器上,/fixboot 报 "Access is denied" 是正常的、预期的 ,不代表失败。UEFI 场景下的正确工具是 bcdboot

第 8 步:最后手段(按破坏性递增)

  1. 系统还原(rstrui.exe,从 WinRE 运行,不影响个人文件)
  2. 卸载最近的质量更新 / 功能更新
  3. 重置此电脑 → 保留个人文件
  4. 全新安装

7.4 几类典型故障的专项处置

案例一:换固态/克隆后起不来(0xc000000e / 0xc000000f)

根因:BCD 中记录的磁盘签名(MBR)或分区 GUID(GPT)与新磁盘不符;或克隆时 ESP 未被正确复制;或新盘 ESP 与旧盘 ESP 冲突。

处置:

复制代码
# 1) 确认 Windows 分区与 ESP 都在
diskpart → list volume
# 2) 修复分区标识:若 device 显示 unknown,改为 locate
bcdedit /store S:\\EFI\\Microsoft\\Boot\\BCD /set {default} device locate=\\Windows
bcdedit /store S:\\EFI\\Microsoft\\Boot\\BCD /set {default} osdevice locate=\\Windows
# 3) 或直接重建(更干净)
bcdboot C:\\Windows /s S: /f UEFI
# 4) 若机器上有两个 ESP,把多余的那个删掉或确保固件选中正确的那个

案例二:BitLocker 恢复密钥反复出现

根因排序:修改了被 BCD 验证配置文件监视的元素(testsigningnointegritychecksnxdebughypervisordebug 等);更新了固件而 PCR 配置包含 0/2/4;更换了硬件;TPM 被清除。

处置原则:

复制代码
# 事前预防:任何会改变度量的操作之前
manage-bde -protectors -disable C:
   <做固件更新 / 改 BCD / 换硬件>
manage-bde -protectors -enable C:

# 事后:进入恢复界面输入恢复密钥,系统会自动重新密封 PCR 值
# 查看当前状态
manage-bde -status
manage-bde -protectors -get C:

案例三:双系统后 Windows 起不来(GRUB 菜单里的 Windows 项报 0xc000000f)

根因:os-prober 生成的 chainloader 路径指向了错误的 ESP,或 Windows 更新后 bootmgfw.efi 路径/分区变化。

处置:在 Linux 下确认 /boot/efi/EFI/Microsoft/Boot/bootmgfw.efi 是否真实存在;用 efibootmgr -v 查看 NVRAM 启动项;最稳妥的做法是在固件启动菜单(F8/F12)里直接选 "Windows Boot Manager",绕过 GRUB。

案例四:Intel VMD 导致的 INACCESSIBLE_BOOT_DEVICE(0x7B)

现象:OEM 笔记本(11 代酷睿及之后)在 BIOS 重置、清除 CMOS、或固件更新后,进系统就蓝屏 0x7B

根因:BIOS 中 "VMD Controller" 开关被改变,而 Windows 只安装了原开关状态下对应的存储驱动。

处置:进 BIOS(开机按 F2)→ Advanced → VMD setup menu → 把 "Enable VMD controller" 改回原来的值 → F10 保存。若不知道原值,两种都试一次。这是纯配置问题,不需要重装。

案例五:更新固件后要求 BitLocker 恢复密钥(且 PCR 含 0/2/4)

根因:PCR 0(固件代码)随固件更新而改变,密封的 VMK 无法解封。

处置:输入恢复密钥进入系统后,用 manage-bde -protectors -get C: 确认 PCR 配置。如果长期困扰,可考虑(在有管理策略支持的环境中)把验证配置改为 7+11;但不要为了省事而关闭 PCR 校验------那等于放弃 TPM 对引导链的保护。

7.5 该看哪些日志

关于 bootstat.dat与"自动修复"的触发机制: Windows 维护一个启动成功/失败计数器。当连续启动失败达到阈值(且 bootstatuspolicy 未被设为 IgnoreAllFailures),引导管理器会转而加载 WinRE。你也可以在正常系统中手动查看与设置:

复制代码
bcdedit /enum {current} | findstr /i bootstatuspolicy
bcdedit /set {current} bootstatuspolicy IgnoreAllFailures   ← 禁止自动进 WinRE(不推荐长期使用)
bcdedit /set {current} bootstatuspolicy DisplayAllFailures  ← 每次失败都提示

第八章 安全机制全景:七道防线

把散落在各章的安全机制串起来,Windows 10 的启动链实际上部署了七道防线:

第一道:硬件信任根(Boot Guard / PSP 硬件验证)

CPU 微码与 ACM 在固件执行前验证固件签名。失败 → 变砖或拒绝执行。防的是"刷恶意 BIOS"。

第二道:固件 Secure Boot

PK/KEK/db/dbx 四层密钥体系,验证每一个 EFI 映像。防的是"替换 bootmgfw.efi"。

第三道:Trusted Boot(微软自己的信任链)

bootmgfw 验证 winload.efi,winload 验证所有它加载的驱动(通过 ci.dll)。即便 Secure Boot 关闭,这一层依然工作。防的是"替换 winload 或注入未签名驱动"。

第四道:Measured Boot(TPM 度量)

PCR 0--7(固件)+ 11/12/13/14(Windows)记录所有被加载组件的摘要,事件日志可被远程验证。防的不是攻击本身,而是"攻击后伪装成健康"。

第五道:ELAM(早启动反恶意软件)

在 boot-start 驱动被初始化之前逐个分类(Good/Bad/Unknown),并可按策略阻止。防的是 rootkit 型驱动。

第六道:代码完整性 / HVCI / VBS

运行期持续强制签名策略(WDAC 可进一步限定允许的签发者);基于虚拟化的安全(VBS)把安全敏感的内核代码放到受 hypervisor 保护的隔离区域;HVCI(内存完整性)确保内核代码页不可被写入、不可执行未签名代码。

第七道:BitLocker(数据静态保护 + 引导链密封)

把磁盘密钥与 PCR 值绑定。任何改变引导链的行为都会导致密钥无法解封。防的是"离线攻击"(把硬盘拆下来读数据、或用 U 盘启动改文件)。

这七道防线的关键性质 :它们不是冗余的,而是分层的------每一道防的是不同的攻击路径:

一个常被问的问题:关掉 Secure Boot 会怎样?

  • 第二道防线消失:任何 EFI 应用都能被固件加载,包括 bootkit。
  • 后面五道还在:bootmgfw 仍会验证 winload,winload 仍会验证驱动,ELAM 仍在工作,HVCI 仍在工作。
  • BitLocker 会立刻要求恢复密钥------因为 PCR 7(Secure Boot 状态)变了。这就是为什么"关 Secure Boot 试试"之后,很多人反而遇到更多麻烦。

第九章 工具箱

9.1 引导修复类

9.2 诊断与取证类

9.3 值得记住的几条命令组合

"我什么都不会,先给我一套能跑的":

复制代码
diskpart
  list volume
  select volume 
  assign letter=S
  exit
bcdboot C:\\Windows /s S: /f UEFI

"启动太慢,我想知道慢在哪":

复制代码
# 查看最近几次启动的总耗时与分阶段耗时
eventvwr.msc
  → 应用程序和服务日志 → Microsoft → Windows → Diagnostics-Performance → 操作
  → 事件 ID 100(启动性能),其中 BootTime / MainPathBootTime 等字段是毫秒数

"我要改 BCD,但不想触发 BitLocker 恢复":

复制代码
manage-bde -protectors -disable C:
bcdedit /export C:\\BCD.bak
bcdedit /enum all > C:\\bcd-before.txt
  <修改>
manage-bde -protectors -enable C:

"机器上有好几个 ESP/启动项,我晕了":

复制代码
bcdedit /enum firmware            # 固件层有哪些启动项
bcdedit /enum all /v              # BCD 层有哪些对象与元素
diskpart → list volume            # 有几个 FAT32 分区

第十章 常见误解澄清(Q&A)

Q1:UEFI 就是 BIOS 的新名字吗?

不是。BIOS 是一种固件实现;UEFI 是一套接口规范(外加 PI 规范定义内部分阶段流程)。现代"BIOS"通常指的是"符合 UEFI 规范的固件"。日常说"进 BIOS"其实是进固件的设置界面。

Q2:GPT 和 UEFI 必须一起用吗?

规范上不强制:UEFI 也能从 MBR 磁盘启动,Legacy BIOS 也能读 GPT(仅限某些实现)。但Windows 的硬性规则是:UEFI 启动必须配 GPT 磁盘,Legacy 启动必须配 MBR 磁盘。这条规则没有例外,也是"磁盘 0 使用了不支持的分区表类型"这类安装错误的根源。

Q3:ESP 上的 \\EFI\\BOOT\\bootx64.efi是干什么的?

它是固件的默认回退路径:当 NVRAM 中没有任何可用启动项时,固件会尝试在可移动介质上找这个文件。安装介质、PE 工具盘都依赖它。正常安装 Windows 的机器上,这个文件可能存在也可能不存在,不影响正常启动。

Q4: bootmgfw.efi bootmgr.efi是同一个东西吗?

在微软的发布体系里,bootmgfw.efi 是"面向固件启动管理器"的引导管理器(放在 ESP),而 bootmgr.efi 是其 EFI 变体的另一份称呼/副本(常见于 \\Windows\\Boot\\EFI\\,用于某些部署与修复路径)。功能上是同一个程序的不同安放位置/命名。真正要区分的是它们与 winload.efi(操作系统加载器)的差别。

Q5:Secure Boot 会阻止我装 Linux 吗?

不会。主流发行版(Ubuntu、Fedora、Debian、openSUSE 等)的 shim 都由微软第三方签名服务签署,Secure Boot 开启时也能正常启动。会被阻止的是未签名的自制引导器,此时需要自己注册密钥或关闭 Secure Boot。

Q6:关 Secure Boot 能让系统变快吗?

几乎不会。签名验证只发生几次,每次几十毫秒。它带来的风险(失去引导链保护、BitLocker 进恢复模式)远大于收益。

Q7:为什么我按 F8 进不了安全模式?

Windows 8 之后默认不再在引导管理器阶段等待 F8。替代方案:① Shift + 重启;② shutdown /r /o /t 0;③ 连续三次强制断电触发 WinRE;④ 把 bootmenupolicy 设为 legacy 恢复旧行为。

Q8:BCD 能用记事本编辑吗?

不能直接编辑------它是二进制的注册表 hive。但可以 reg load 挂载后用注册表编辑器修改。日常使用应当用 bcdedit

Q9: bootrec /fixboot报 Access is denied 是不是完了?

不是。在 UEFI/GPT 机器上这是预期行为------ESP 上没有 /fixboot 想写的那种引导扇区。UEFI 环境的正确工具是 bcdboot

Q10:快速启动和休眠是一回事吗?

共用同一个机制(hiberfil.sys + winresume.efi),但保存的内容不同:完整休眠保存内核态 + 全部用户会话;快速启动只保存内核态与驱动状态,用户会话在关机时被完全关闭。所以"快速启动后再开机"你仍需重新登录。

Q11:为什么"重启"能解决而"关机再开"解决不了?

重启走完整冷启动,所有驱动重新初始化;关机再开走快速启动路径,把上一次的内核状态恢复回来,问题被一起恢复。遇到偶发的驱动/设备异常,优先用"重启"而不是"关机再开"。

Q12:hal.dll 丢了系统会怎样?

Windows 10 2004 之后,hal.dll 只是兼容存根,但它仍被加载器按 BCD 的 hal 元素加载。文件缺失会导致加载失败(通常表现为 0xc0000001 或类似)。修复方式:从安装介质/健康机器复制同名文件,或 sfc / DISM 修复。但真正决定系统能否启动的是 ntoskrnl.exe 里已合并的 HAL 代码。

Q13:PCR 7+11 和 PCR 0+2+4+11 哪个更安全?

严格性上 0+2+4+11 更高(度量固件与引导器本体);可用性上 7+11 更好(固件/引导器安全更新不触发恢复)。微软默认选 7+11 是可用性优先的取舍。高安全场景可考虑更严格的配置,但要配套管理固件更新的流程(更新前挂起 BitLocker)。

Q14:我的机器开机要 40 秒,正常吗?

看硬件。NVMe SSD + 现代 CPU,冷启动到登录界面 10--20 秒属正常(固件阶段常占其中一半以上);机械硬盘 40--60 秒也不算异常。要优化,先量化:用事件查看器的 Diagnostics-Performance 事件 100 看 BootTime 分解,再决定是固件阶段(开 Fast Boot、减 Option ROM)还是系统阶段(减启动项、减自动启动服务)的问题。

Q15:双系统一定要关快速启动吗?

强烈建议关。快速启动后 Windows 的 NTFS 卷处于"已挂载"状态(内核认为自己还持有该卷),Linux 再挂载并写入可能造成元数据不一致,尤其在启用 NTFS-3G 写入支持时。同理,双系统共享的 NTFS 数据盘也受影响。

Q16: bcdedit /set testsigning on为什么会触发 BitLocker?

因为 testsigning(0x16000049)被列在 BitLocker 的 BCD 验证配置文件中,修改它改变了度量值。正确做法是先 manage-bde -protectors -disable C:,改完再 -enable

Q17:删掉 ESP 会怎样?

立刻起不来。ESP 上没有你的个人文件,但它是唯一存放引导管理器与 BCD 的地方。恢复方式:diskpart 重建一个 FAT32 分区(100 MB 足够)→ bcdboot C:\\Windows /s S: /f UEFI。数据都在 Windows 分区里,不会丢。

Q18:UEFI 变量(NVRAM 启动项)会自己坏吗?

会。固件 bug、错误的变量写操作、CMOS 清除、固件更新都可能清空或破坏 NVRAM 启动项。症状是"固件找不到任何可启动项"。因为 Boot#### 里存的是设备路径(含分区 GUID),只要磁盘分区没变,bcdboot 就能重建它。

Q19:Secure Boot 的 2011 证书 2026 年到期后,我的电脑会开不了机吗?

不会开不了机。 已签名的文件仍然有效,固件不会检查证书的"有效期"。影响是安全冻结:将来用 2023 证书签署的新版 bootmgfw.efi、以及新的 dbx 更新,将不被只含 2011 证书的老机器信任。也就是说它无法再获得引导器安全更新。应对方式是确保 db 中包含 2023 系列证书(Windows Update 与固件更新通常会推送)。

Q20:我该不该自己注册 Secure Boot 密钥(PK/KEK/db)?

除非你明确知道自己在做什么(比如要运行自签名的自制引导器),否则不该。自注册密钥后会失去微软签名的第三方引导器兼容性,且不同厂商固件的实现质量参差不齐,容易把机器搞成需要清 CMOS 才能恢复的状态。


附录 A:关键 GUID 速查

附录 B:UEFI 引导期关键路径与文件名

附录 C:错误码全称对照

附录 D:BCD 元素类型码编码速算

复制代码
元素类型码 = (Class << 28) | (Format << 24) | SubType

Class:  1=Library  2=Application  3=Device  5=OEM
Format: 1=Device   2=String       3=Object  4=ObjectList
        5=Integer  6=Boolean      7=IntegerList

例:0x25000020 → Class=2(Application), Format=5(Integer), SubType=0x20 → nx
    0x11000001 → Class=1(Library),     Format=1(Device),   SubType=0x01 → device
    0x26000022 → Class=2(Application), Format=6(Boolean),  SubType=0x22 → winpe

结语:把链路变成直觉

回到开篇那条链路:

复制代码
固件初始化 → EFI\\Microsoft\\Boot\\bootmgfw.efi → BCD → winload.efi
→ ntoskrnl.exe(含已合并的 HAL)、hal.dll(兼容存根)

现在再看它,希望它不再是一串名词,而是一组可以被逐条追问的判断:

  • 固件为什么能找到那个文件? 因为 NVRAM 的 Boot#### 里存了一条设备路径,用分区 GUID 而非盘符定位,最后一个节点是文件路径;找不到时固件会回退到 \\EFI\\BOOT\\bootx64.efi
  • 它凭什么相信那个文件? 因为 db 里有微软的证书,dbx 里没有这个哈希;验证通过后,它的摘要被扩展进 PCR 4,Secure Boot 状态被扩展进 PCR 7。
  • bootmgfw 怎么找到 BCD? 不是配置项告诉它的,是它从"自己被放在哪个分区"反推出来的 \\EFI\\Microsoft\\Boot\\BCD------所以引导管理器和 BCD 必须在同一个 ESP 上。
  • winload 为什么能做那么多事? 因为它同时握着固件的 Boot Services(读文件、画屏幕)和微软自己的 NTFS 实现、代码完整性模块、以及 BitLocker 解封能力。
  • ExitBootServices之后发生了什么不可逆的事? Boot Services 全部失效,内存所有权交给操作系统,运行时服务被搬迁到虚拟地址------从此内核再也不能"回头"用固件的能力,只能通过 Runtime Services 读写变量、复位机器。
  • 内核为什么需要 LoaderBlock? 因为在它完成 Phase 0 之前,它不知道自己在哪里、内存有多大、加载了哪些驱动、磁盘在哪------LoaderBlock 是它全部的"先天知识"。
  • hal.dll 现在是什么? 一个几十 KB 的兼容存根;真正的 HAL 自 Windows 10 2004 起已静态链接进 ntoskrnl.exe。这个变化反映了微软把平台差异收敛、把热路径内联的方向。

最后三条实践建议,按重要性排序:

  1. 动手改任何东西之前,先做两件事bcdedit /export 导出 BCD,manage-bde -protectors -disable C: 挂起 BitLocker。这两条能把你从 90% 的"越修越糟"里救出来。
  2. 永远先判断"死在哪一段",再决定用什么工具 。屏幕上的内容就是最好的证据:固件提示、深色背景白字、蓝屏停止码,分别对应三个完全不同的故障域。用 bcdboot 去修一个 0x7B 蓝屏,是浪费时间。
  3. UEFI 环境下的第一修复工具是 bcdboot,不是 bootrec。后者是为 Legacy BIOS 设计的,在 UEFI 机器上 /fixboot 报 "Access is denied" 是正常现象,不要被它误导。

Windows 的启动链是三十多年操作系统演进沉积下来的结果------它同时保留着 NT 时代的设计(ARC 路径、ControlSet、阶段 0/阶段 1)、UEFI 时代的规范(设备路径、GOP、Secure Boot)、以及云计算与安全时代的机制(Measured Boot、ELAM、VBS、远程证明)。它不优雅,但它有极强的向后兼容性,而且每一个看起来"多余"的环节,几乎都能追溯到某个具体的历史问题。

理解它,不会让你开机更快。但当它坏掉的那天,你会知道自己该看哪里。

相关推荐
ZnS_oscar2 小时前
不用下载字体,获得仿宋字体
windows·word
霸道流氓气质2 小时前
通义千问 Chat 模型高级用法:Function Calling / Structured Output / 多轮对话
linux·运维·windows
Boop_wu3 小时前
[LangGraph] 案例 2 : 支持搜索的智能代理系统
服务器·windows·python·langchain
啦啦啦~~~2223 小时前
PC端+安卓端阅读器推荐!开源本地小说阅读器软件,
android·论文阅读·windows·开源软件·福昕阅读器
C++ 老炮儿的技术栈4 小时前
Qt5 使用 QPainter 绘制阿基米德螺线
开发语言·c++·windows·qt·代码化
今日热点5 小时前
Windows 系统 C 盘深度清理教程:安全释放存储空间,避开误删风险
windows·安全
gsls2008086 小时前
告别 Vault 的复杂度:用 Go 标准库给 Windows 凭据管理器装上 MCP
windows·golang·mcp
Mrs_DongDong6 小时前
华为云 Windows 服务器远程桌面自定义端口排障指南
服务器·windows·华为云
Sagittarius_A*6 小时前
CVE-2026-42271:从 MCP stdio 测试接口看 LiteLLM 的命令执行风险与防御闭环
网络·windows·安全·cve·rce