本文的我复现《软件调试卷2:Windows平台调试》(以下简称《软件调试》 )中相关实验的总结。《软件调试》中调试的是32位系统,我这里使用64位Windows 11系统进行复现。
由于笔者水平十分有限,文中难免出现错误,恳请广大读者斧正。
系统上电之后,CPU首先执行固化在系统主板上的固件(firmware)代码,目的是检测和初始化基本硬件。
固件程序分为两种:
这里不多讨论两种固件程序的实现细节和区别,感兴趣的读者可以阅读《x86汇编:从实模式到保护模式》与《Rootkit和Bootkit:现代恶意软件逆向分析和下一代威胁》(以下简称《R&B》 )。现在大多使用UEFI。
下图来自《R&B》:
现在我们使用windbg对Windows的启动过程进行调试。
UEFI是整个启动过程的开始。这里不会太深入的讲解UEFI,但是对UEFI有一点了解可以更好的理解接下来的内容。
首先说说Boot Loader。
boot一词的词源是
Pull oneself up by one's bootstraps,译为“靠自己的靴子拉带把自己提起来”
字面意思:想象一个人抓住自己靴子上的带子,试图把自己提离地面。
引申含义:这是一个不可能完成的任务,后来被用来形容“不靠外界帮助,完全靠自己完成某事”。
计算机启动的过程非常像那个“提靴子”的动作:
计算机必须依靠一个极其微小 的、预先写死的“种子”程序,去一步步把庞大的操作系统“拉”起来。这就像抓住自己的靴带把自己提起来一样,因此这个过程被称为 Booting (引导/启动) 。
步骤一和步骤二的代码驻留在主板的闪存芯片上(SPI闪存),步骤3和步骤4的代码从硬盘驱动器的特殊UEFI分区的文件系统中提取。
**操作系统加载程序实际上依赖于UEFI固件提供的EFI引导服务和EFI运行时服务来引导和管理系统。**这里提到了两个概念:
在UEFI架构中,EFI引导服务 和EFI运行时服务 是固件提供给操作系统加载器(如bootmgfw.efi)的两套核心API接口,它们的定义与区别如下:
EFI 引导服务 (Boot Services) : 这是一套临时性 的API接口,存在于系统启动阶段。它的核心任务是协助操作系统加载器(如Windows Boot Manager)完成启动准备工作,包括分配内存、加载驱动、读取文件等。一旦操作系统内核接管控制权,这些服务就用不了了。
EFI 运行时服务 (Runtime Services) : 这是一套永久性 的API接口,在操作系统运行期间持续有效。它的核心任务是为操作系统提供底层的硬件访问能力,例如读取/设置硬件时钟、电源管理(关机/重启)、以及读写NVRAM中的EFI变量(如BCD配置)。
要调试win11的启动,首先我们要让Win11在启动的瞬间停下来,将控制权移交给调试器。
为了达到这个目的,我们需要使用bcdedit.exe程序,它的全称为Boot Configuration Data Editor(启动配置数据编辑器)。它是一个命令行工具,用来修改硬盘上的一个数据库文件(BCD文件)。这个数据库里存放着Windows启动所需的参数(配置)。
BCD文件存放在EFI系统分区(ESP)中。我们无法在Windows资源管理器中看到它,因为它位于一个隐藏的、未分配盘符的特殊分区里(如下图):
当运行 bcdedit 修改了配置后,系统下次启动时就会按照修改后的BCD文件执行启动。
在《软件调试》和大部分资料中,都是给出下面的命令启用bootmgr和winload的调试引擎:
经过实验证明,在win11中大概是行不通的 。在win11中执行上述命令之后,大概率会得到一行报错:
参考微软相关文档 以及实验证明,以一般的方式(管理员命令行)运行bcdedit程序,只有下面这条指令可以开启调试引擎,并且开启的还不是bootmgr的,而是bootmgr下一阶段的winload:
为什么不能开启bootmgr的调试引擎呢?还是翻阅微软的文档:5efK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9k6h3q4J5L8W2)9J5k6h3#2A6j5%4u0G2M7$3!0X3N6q4)9J5k6h3y4G2L8g2)9J5c8Y4A6Z5i4K6u0V1j5$3&6Q4x3V1k6%4K9h3&6V1L8%4N6K6i4K6u0V1K9r3q4J5k6s2N6S2M7X3g2Q4x3V1k6V1M7X3W2$3k6i4u0K6i4K6u0r3k6r3g2$3N6r3g2K6N6q4)9J5c8X3u0U0k6r3g2V1K9i4c8Q4x3X3c8Q4x3X3c8V1k6h3u0#2k6H3`.`.
里面提到:
设置 BCDEdit 选项之前,可能需要禁用或暂停计算机上的 BitLocker 和安全启动。
我们遇到的 bcdedit /set {bootmgr} bootdebug on 报错,并非语法错误,而是 Windows 11 的 UEFI 安全机制 在作祟。
在 Windows 11 中,BCD 存储被底层固件(UEFI)锁定了。任何试图直接修改 {bootmgr} 标识符的操作都会被系统拦截,导致“参数错误”。因此,调试器只能在 winload.efi 阶段才成功接管,完美错过了 bootmgr 阶段。
既然无法在系统运行时修改,我们必须**重启进入“高级启动模式”**来绕过这个锁。请按以下步骤操作:
命令的含义不多解释,做过Windows内核驱动开发的都懂。如果先前windbg是可以在Windows内核中断下来,后三条指令应该是可以不执行的。
现在重启虚拟机,虚拟机屏幕左上角中会一闪而过类似Windows bootmgr的字样(速度过快我没有截图下来),接着虚拟机黑屏,中断运行,WinDbg 就在 bootmgr 阶段成功断下了:
虚拟机启动后会等待调试器的连接,如果不需要进行调试了使用下面的命令关闭对bootmgr的调试引擎:
关于其它调试引擎的开启和关闭,这里不多赘述,请参考微软官方文档:
启动选项标识符
BCDEdit /bootdebug
这是Windows启动最早的代码,UEFI就是将控制权移交给该模块的。bootmgfw被称为Windows引导管理器(参考《R&B》)。
先前的{bootmgr}(BCD 标识符)是一个逻辑概念,它是 BCD 配置数据库里的一个条目。而bootmgfw.efi(物理文件):是实体,它是被加载到内存中运行的代码本身。
bootmgfw.efi保存在\EFI\Microsoft\Boot\bootmgfw.efi中,简单来说就是在EFI系统分区(ESP)中。
还记得前面提到的UEFI保存在哪里吗?保存在SPI中。
有人可能会问,为什么不将bootmgfw.efi保存在C盘中。这是因为UEFI 固件(BIOS)启动电脑时,只能读取 FAT32 格式的分区。而Windows 系统盘(C盘)通常是 NTFS 格式,UEFI 读不懂。系统盘存放真正的 Windows 内核和系统文件。
我们以栈回溯为线索看看bootmgfw.efi做了什么。
**EFI引导管理器在内存中创建bootmgfw.efi的运行时映像。该模块加载后,UEFI固件引导管理器跳转到bootmgfw.efi的入口点:EfiEntry。**这是操作系统引导过程的开始。
EfiEntry原型如下(由UEFI标准规范得到):
EfiEntry是编译链接时的符号名,源码中通常叫 UefiMain或 ShellAppMain
还记得在Boot Loader一节中提到的UEFI的两种服务吗?在bootmgfw.efi的EfiEntry函数中,通过EFI_SYSTEM_TABLE获取这两套服务的指针。引导服务负责将winload.efi加载到内存并读取BCD配置,而运行时服务则确保Windows在启动后仍能通过标准接口与固件交互。
EfiEntry函数引用bootmgfw.efi,为winlaod.efi配置UEFI固件回调,这些回调连接winload.efi与UEFI运行时服务。
结合栈回溯,看看如何调用EfiEntry:
000000000ffcd4a7应该就是调用EfiEntry(观察传参和返回地址)。
ImageHandle参数指向bootmgfw.efi模块,该参数复杂继续引导过程并调用winload.efi。Systemtable指向UEFI配置表(EFI_SYSTEM_TABLE)的指针,它是 UEFI 应用程序与硬件固件之间沟通的唯一桥梁,其结构如下:
看看EfiEntry的反汇编:
上面有两个关键调用 :EfiInitCreateInputParametersEx和BmMain。
它有两个作用:
BmMain 是 Windows 引导管理器(Bootmgfw)的核心 。BmMain 的主要工作是读取 BCD 配置,并决定下一步该做什么:
处理休眠 :
显示启动菜单 :
处理错误 :
加载内核 :
BmMain 执行路径如下:
BmMain 是 Windows 启动过程中最后一段通用代码 ,之后的所有代码(winload.efi, ntoskrnl.exe)都高度依赖具体的 Windows 版本。它是连接“firmware”和“Windows”的桥梁。
BmMain函数会检查上次启动是否失败,如果失败就进入“自动修复”或“启动恢复”模式,尝试修复问题。这是通过检查BCD中一个特殊的标志位(LastBootSucceeded)来判断上去启动是否成功的。
LastBootSucceeded这个标志很有用。比如我们开发了一个开机自启的驱动程序,但是这个驱动程序存在缺陷会导致系统启动失败。那么我们就可以在驱动中检查LastBootSucceeded,如果上一次启动失败,我们的驱动就先不加载。这样可以让系统正常启动,便于排查启动失败的原因。
随后Bootmgfw会寻找WinLoad.exe文件,并校验文件的完整性,将其加载。BootMgfw还会做更新GDT、IDT,随后用调试平台的相关控制权移交给WinLoad。
**就在bootmgfw将控制权交给winload之前,其会执行一项重要的操作:开启并初始化分页模式。**具体是怎么开启的可以看我的这篇博客:f18K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8V1!0G2M7X3y4Z5K9g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6V1k6i4c8S2K9h3I4K6i4K6u0r3x3e0b7^5y4K6l9J5x3e0x3#2i4@1f1K6i4K6R3H3i4K6R3J5i4@1f1@1i4@1u0p5i4K6R3$3i4@1f1$3i4K6V1^5i4@1q4r3i4@1f1^5i4@1u0r3i4K6V1&6i4@1f1%4i4@1q4r3i4K6R3%4i4@1f1#2i4K6S2p5i4K6W2m8i4@1f1#2i4@1q4q4i4@1p5J5i4@1f1$3i4K6V1^5i4@1q4r3i4@1f1#2i4K6W2o6i4@1p5^5x3K6u0Q4c8e0c8Q4b7V1c8Q4z5p5c8Q4c8e0N6Q4z5p5g2Q4b7f1k6Q4c8e0g2Q4b7e0u0Q4z5o6y4Q4c8e0c8Q4b7U0S2Q4z5p5u0Q4c8e0g2Q4z5o6S2Q4z5o6k6Q4c8e0k6Q4z5f1g2Q4z5e0m8Q4c8e0N6Q4z5f1q4Q4z5o6c8Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0g2Q4z5f1y4Q4b7e0R3$3y4q4!0q4y4q4!0n7c8q4)9^5c8q4!0q4y4#2)9^5c8g2!0m8c8W2!0q4y4g2!0m8x3W2)9^5x3#2!0q4y4q4!0n7z5q4)9^5b7W2!0q4c8W2!0n7b7#2)9^5b7#2!0q4y4q4!0n7c8W2)9&6c8q4!0q4y4W2)9^5b7g2!0m8y4q4!0q4y4W2!0m8z5q4!0m8x3g2!0q4y4g2!0n7b7#2)9^5c8W2!0q4y4W2)9&6z5q4!0m8c8W2g2q4c8V1W2Q4c8e0g2Q4z5f1u0Q4b7V1q4Q4c8e0c8Q4b7V1u0Q4b7U0k6Q4c8e0g2Q4z5f1y4Q4b7e0S2T1L8$3!0@1L8h3N6X3N6#2!0q4z5q4!0m8x3W2!0m8b7W2!0q4y4g2)9^5b7g2!0m8x3q4!0q4z5q4!0n7c8q4!0n7c8q4!0q4y4q4!0n7z5g2)9^5b7W2!0q4y4g2)9^5z5g2)9^5c8q4!0q4y4g2!0n7x3q4!0n7x3g2!0q4y4g2!0n7b7#2)9^5x3q4!0q4y4g2)9&6x3q4!0m8c8W2!0q4y4q4!0n7b7g2)9^5y4W2!0q4x3#2)9^5x3q4)9^5x3W2g2q4c8V1V1`. 固件在初始化过程中,自己就会切换到保护模式。
winload主要任务是把操作系统内核加载到内存中。winload.efi是连接固件(UEFI)与 Windows 内核(ntoskrnl.exe)的桥梁。它的核心任务不是运行系统,而是为内核运行准备必要的环境。
开启winload.efi断下来时,回溯一下:
看看winload.efi模块:
该模块最主要的工作就是加载各种模块到内存中:
加载内核:将 ntoskrnl.exe(内核文件)和 hal.dll(硬件抽象层)读入内存。
加载驱动:扫描注册表,将启动类型(Start值为 0)的驱动程序加载到内存。
传递数据:准备 LOADER_PARAMETER_BLOCK结构体,这个结构体包含了内存布局、ACPI 表等关键信息。
winload最后的工作就是将CPU的控制权交给内核。winload会准备一个LOADER_PARAMETER_BLOCK的结构,传递给KiSystemStartup(内核模块的入口函数)。这个结构体中记录了winload在加载过程中获取的各种信息,KiSystemStartup会利用这些信息初始化内核。
现在重要来到了Windows内核中,我们看看Windows内核的模块:
ntkrnlmp末尾的mp为Multi-Processor(多处理器)之意。
回溯栈:
KiSystemStartup为内核的入口函数。它会将winload所传递的LOADER_PARAMETER_BLOCK结构体保存到KeLoaderBlock结构体中:
我们看看LOADER_PARAMETER_BLOCK结构体(利用KeLoaderBlock的地址):
总的来说,KiSystemStartup会完成内核最底层的初始化。这包括设置中断描述符表 (IDT) 让 CPU 能响应硬件中断,配置全局描述符表 (GDT) 确立内存访问权限,以及初始化内核调试引擎。这一步是让内核能够“思考”和“感知”外界的基础。现代 CPU 通常是多核的。KiSystemStartup 在初始化完当前核心(通常是 0 号核心)后,会立即向其他核心发送信号,让它们也从休眠中醒来,并把自己初始化到待命状态。这确保了系统启动后所有算力都能立即投入使用。当底层的硬件准备工作全部完成后,它会调用 KiInitializeKernel。这个函数负责初始化进程、线程、内存管理等高层对象。完成后,系统就正式进入了“多任务”时代,开始加载系统进程(如 System进程)并准备启动用户界面的登录程序。
关于这个函数的代码和具体执行步骤,可以参考wrk中的源代码和《软件调试》一书。
摘自《Windows内核原理与实现》:
内核的初始化主要是内核各个组件的初始化,但由于这些内核组件之间有紧密的耦合关系,所以它们的初始化并不是简单地顺序执行初始化。为了解决在初始化过程中的相互依赖性问题,内核的初始化分两个阶段进行,称为阶段 0 和阶段 1。大多数内核组件的初始化函数相应地带有一个整数参数,以指明一次调用是阶段 0 初始化还是阶段 1 初始化,而有些组件的初始化函数通过检查一个全局变量 InitializationPhase 的值来判断当前处于哪个阶段。
简单来说,阶段0初始化是为了建立阶段1初始化所需的各种基本数据。在阶段 0 初始化过程中,中断被禁止,因此处理器可以顺序地执行自己的初始化逻辑。
0阶段对应的过程是KiInitializeKernel。KiInitializeKernel 的所有操作都是在中断关闭 (IRQL = HIGH_LEVEL) 的状态下执行的,以确保绝对原子性。当它完成了所有关键的、不可中断的初始化工作后,便会降低中断请求级别 (IRQL),退出 Phase 0,将系统控制权交还给主处理器。此时,Phase 1 开始。在这个阶段,中断重新被打开,其他处理器核心被正式激活,系统的其余部分(如设备驱动程序、子系统)才会被逐步加载和启动。
另外,Phase 0创建了Idle和System这两个特殊进程。它们是最原始的进程。关于它们的更多讨论,请参考《软件调试》一书。
Phase 1 的核心任务是将内核从单一核心的基础框架扩展为完整的多处理器运行环境,并正式启动设备驱动、建立系统进程,为后续的用户界面加载做好准备。
这一阶段标志着系统从底层自举转向功能服务,它首先唤醒所有待命的核心处理器实现真正的多核并行,接着I/O管理器会加载并初始化关键的启动设备驱动程序,让硬件与内核建立连接,随后系统创建了第一个用户模式进程smss.exe,这标志着内核开始向用户态过渡,为后续的Win32子系统、登录管理器等组件的启动铺平了道路。
SMSS(Session Manager Subsystem)作为 Windows 启动链中承上启下的关键进程,它从内核手中接管系统,负责搭建用户态的运行环境。它的工作重心在于“环境配置”与“进程孵化”,具体执行流程如下:
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2天前
被mb_nnqgphwf编辑
,原因: 更新公众号图片