调试Windows11启动过程

调试Windows11启动过程

本文的我复现《软件调试卷2:Windows平台调试》(以下简称《软件调试》)中相关实验的总结。《软件调试》中调试的是32位系统,我这里使用64位Windows 11系统进行复现。

由于笔者水平十分有限,文中难免出现错误,恳请广大读者斧正。

启动概览

系统上电之后,CPU首先执行固化在系统主板上的固件(firmware)代码,目的是检测和初始化基本硬件。

固件程序分为两种:

  • UEFI
  • BIOS

这里不多讨论两种固件程序的实现细节和区别,感兴趣的读者可以阅读《x86汇编:从实模式到保护模式》与《Rootkit和Bootkit:现代恶意软件逆向分析和下一代威胁》(以下简称《R&B》)。现在大多使用UEFI。

下图来自《R&B》:

现在我们使用windbg对Windows的启动过程进行调试。

Boot Loader和UEFI

UEFI是整个启动过程的开始。这里不会太深入的讲解UEFI,但是对UEFI有一点了解可以更好的理解接下来的内容。

Boot Loader

首先说说Boot Loader。

boot一词的词源是

Pull oneself up by one's bootstraps,译为"靠自己的靴子拉带把自己提起来"

  • 字面意思:想象一个人抓住自己靴子上的带子,试图把自己提离地面。

  • 引申含义:这是一个不可能完成的任务,后来被用来形容"不靠外界帮助,完全靠自己完成某事"。

计算机启动的过程非常像那个"提靴子"的动作:

  1. 硬件通电:CPU 刚通电时是一片空白,它不知道内存怎么用,不知道硬盘在哪里,甚至不知道屏幕怎么亮。
  2. 极小程序(Bootloader):CPU 只能执行固化在主板上一小块 ROM 里的代码(BIOS/UEFI)。这段代码非常小,功能有限。
  3. 自我加载 :这段小程序(Bootloader)的唯一任务,就是去硬盘上读取一个稍大一点的程序(如 bootmgr),把它加载到内存里。
  4. 接力棒 :这个稍大的程序(Bootmgr)再去加载更大的程序(如 winload.efi)。
  5. 最终启动:最终加载操作系统内核。

计算机必须依靠一个极其微小 的、预先写死的"种子"程序,去一步步把庞大的操作系统"拉"起来。这就像抓住自己的靴带把自己提起来一样,因此这个过程被称为 Booting (引导/启动)

  1. UEFI固件执行UEFI平台初始化,执行CPU和芯片组初始化,并加载UEFI平台模块
  2. UEFI引导管理器枚举外部总线上的设备,加载UEFI设备驱动程序,然后加载引导应用程序
  3. Windows引导管理器(bootmgfw.efi)加载Windows引导加载程序
  4. Windows引导加载程序(winload.efi)加载Windows操作系统

步骤一和步骤二的代码驻留在主板的闪存芯片上(SPI闪存),步骤3和步骤4的代码从硬盘驱动器的特殊UEFI分区的文件系统中提取。

**操作系统加载程序实际上依赖于UEFI固件提供的EFI引导服务和EFI运行时服务来引导和管理系统。**这里提到了两个概念:

  • EFI引导服务
  • EFI运行时服务

在UEFI架构中,EFI引导服务EFI运行时服务 是固件提供给操作系统加载器(如bootmgfw.efi)的两套核心API接口,它们的定义与区别如下:

  • EFI 引导服务 (Boot Services)

    这是一套临时性的API接口,存在于系统启动阶段。它的核心任务是协助操作系统加载器(如Windows Boot Manager)完成启动准备工作,包括分配内存、加载驱动、读取文件等。一旦操作系统内核接管控制权,这些服务就用不了了。

  • EFI 运行时服务 (Runtime Services)

    这是一套永久性的API接口,在操作系统运行期间持续有效。它的核心任务是为操作系统提供底层的硬件访问能力,例如读取/设置硬件时钟、电源管理(关机/重启)、以及读写NVRAM中的EFI变量(如BCD配置)。

维度 EFI 引导服务 (Boot Services) EFI 运行时服务 (Runtime Services)
生命周期 临时性 。在操作系统启动完成、调用ExitBootServices()后即失效。 永久性。从系统加电到关机,全程可用。
主要功能 内存分配、驱动加载、文件系统访问、事件与定时器。 时间管理、电源管理、EFI变量读写。
访问权限 仅操作系统加载器(如Bootmgr)在启动早期可用。 操作系统内核及驱动在运行期间均可调用。
设计目的 确保操作系统能安全、独立地接管硬件控制权,避免固件干扰。 提供标准化的底层硬件接口,屏蔽硬件差异。

使Windows在启动时断下

要调试win11的启动,首先我们要让Win11在启动的瞬间停下来,将控制权移交给调试器。

bcdedit的原理

为了达到这个目的,我们需要使用bcdedit.exe程序,它的全称为Boot Configuration Data Editor(启动配置数据编辑器)。它是一个命令行工具,用来修改硬盘上的一个数据库文件(BCD文件)。这个数据库里存放着Windows启动所需的参数(配置)。

BCD文件存放在EFI系统分区(ESP)中。我们无法在Windows资源管理器中看到它,因为它位于一个隐藏的、未分配盘符的特殊分区里(如下图):

当运行 bcdedit 修改了配置后,系统下次启动时就会按照修改后的BCD文件执行启动。

使用bcdedit修改BCD数据库文件

在《软件调试》和大部分资料中,都是给出下面的命令启用bootmgr和winload的调试引擎:

powershell 复制代码
bcdedit /set {bootmgr} bootdebug on

经过实验证明,在win11中大概是行不通的。在win11中执行上述命令之后,大概率会得到一行报错:

txt 复制代码
指定的设置命令无效。
运行 "bcdedit /?" 获得命令行帮助。
参数错误

参考微软相关文档以及实验证明,以一般的方式(管理员命令行)运行bcdedit程序,只有下面这条指令可以开启调试引擎,并且开启的还不是bootmgr的,而是bootmgr下一阶段的winload:

powershell 复制代码
bcdedit /bootdebug on

为什么不能开启bootmgr的调试引擎呢?还是翻阅微软的文档:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/devtest/bcdedit--debug

里面提到:

设置 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的调试引擎:

bootmgfw.efi

这是Windows启动最早的代码,UEFI就是将控制权移交给该模块的。bootmgfw被称为Windows引导管理器(参考《R&B》)。

先前的{bootmgr}(BCD 标识符)是一个逻辑概念,它是 BCD 配置数据库里的一个条目。而bootmgfw.efi(物理文件):是实体,它是被加载到内存中运行的代码本身。

txt 复制代码
kd> lmDvm bootmgfw
Browse full module list
start             end                 module name
00000000`10000000 00000000`10327000   bootmgfw   (pdb symbols)
    Loaded symbol image file: bootmgfw.efi
    Image path: bootmgfw.efi
    Image name: bootmgfw.efi
    Browse all global symbols  functions  data  Symbol Reload
    Image was built with /Brepro flag.
    Timestamp:        47A4166B (This is a reproducible build file hash, not a timestamp)
    CheckSum:         002DEFCB
    ImageSize:        00327000
    Mapping Form:     Loaded
    File version:     10.0.28000.317
    Product version:  10.0.28000.317
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0409.04b0
    Information from resource tables:
        CompanyName:      Microsoft Corporation
        ProductName:      Microsoft® Windows® Operating System
        InternalName:     bootmgr.exe
        OriginalFilename: bootmgr.exe
        ProductVersion:   10.0.28000.317
        FileVersion:      10.0.28000.317 (WinBuild.160101.0800)
        FileDescription:  Boot Manager
        LegalCopyright:   © Microsoft Corporation. All rights reserved.

bootmgfw.efi保存在\EFI\Microsoft\Boot\bootmgfw.efi中,简单来说就是在EFI系统分区(ESP)中。

还记得前面提到的UEFI保存在哪里吗?保存在SPI中。

有人可能会问,为什么不将bootmgfw.efi保存在C盘中。这是因为UEFI 固件(BIOS)启动电脑时,只能读取 FAT32 格式的分区。而Windows 系统盘(C盘)通常是 NTFS 格式,UEFI 读不懂。系统盘存放真正的 Windows 内核和系统文件。

txt 复制代码
kd> k
 # Child-SP          RetAddr               Call Site
00 00000000`0ffc95d8 00000000`101e4340     bootmgfw!DebugService2+0x5
01 00000000`0ffc95e0 00000000`10226be5     bootmgfw!DbgLoadImageSymbols+0x70
02 00000000`0ffc9630 00000000`102271a3     bootmgfw!BlBdStart+0x1c9
03 00000000`0ffc9700 00000000`1019f6d8     bootmgfw!BlBdInitialize+0x3ff
04 00000000`0ffc9880 00000000`1019d4f4     bootmgfw!ReinitializeLibrary+0xd4
05 00000000`0ffc98b0 00000000`1003f7eb     bootmgfw!BlInitializeLibrary+0x54
06 00000000`0ffc98e0 00000000`1003f0ba     bootmgfw!BmMain+0x5d3
07 00000000`0ffc9b10 00000000`0ffcd4ac     bootmgfw!EfiEntry+0x6a
08 00000000`0ffc9b50 00000000`00000000     0xffcd4ac

我们以栈回溯为线索看看bootmgfw.efi做了什么。

EfiEntry

**EFI引导管理器在内存中创建bootmgfw.efi的运行时映像。该模块加载后,UEFI固件引导管理器跳转到bootmgfw.efi的入口点:EfiEntry。**这是操作系统引导过程的开始。

EfiEntry原型如下(由UEFI标准规范得到):

cpp 复制代码
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,   // 应用程序UEFI映像的句柄,代表bootmgfw.efi自己
  IN EFI_SYSTEM_TABLE  *SystemTable   // UEFI系统表的指针
  );

EfiEntry是编译链接时的符号名,源码中通常叫 UefiMain或 ShellAppMain

还记得在Boot Loader一节中提到的UEFI的两种服务吗?bootmgfw.efiEfiEntry函数中,通过EFI_SYSTEM_TABLE获取这两套服务的指针。引导服务负责将winload.efi加载到内存并读取BCD配置,而运行时服务则确保Windows在启动后仍能通过标准接口与固件交互。

EfiEntry函数引用bootmgfw.efi,为winlaod.efi配置UEFI固件回调,这些回调连接winload.efi与UEFI运行时服务。

结合栈回溯,看看如何调用EfiEntry:

txt 复制代码
kd> ub 0xffcd4ac
00000000`0ffcd48a 0f94c2          sete    dl
00000000`0ffcd48d 8d541207        lea     edx,[rdx+rdx+7]
00000000`0ffcd491 4c89e1          mov     rcx,r12
00000000`0ffcd494 e861f80000      call    00000000`0ffdccfa
00000000`0ffcd499 41c644241801    mov     byte ptr [r12+18h],1
00000000`0ffcd49f 498b542438      mov     rdx,qword ptr [r12+38h]
00000000`0ffcd4a4 4c89e9          mov     rcx,r13
00000000`0ffcd4a7 41ff542420      call    qword ptr [r12+20h] ;这里应该是call EfiEntry?
kd> u 0xffcd4ac
00000000`0ffcd4ac 49898424a8000000 mov     qword ptr [r12+0A8h],rax
00000000`0ffcd4b4 e8b5940000      call    00000000`0ffd696e
00000000`0ffcd4b9 498b9424a8000000 mov     rdx,qword ptr [r12+0A8h]
00000000`0ffcd4c1 4531c9          xor     r9d,r9d
00000000`0ffcd4c4 4531c0          xor     r8d,r8d
00000000`0ffcd4c7 4c89e9          mov     rcx,r13
00000000`0ffcd4ca e894fdffff      call    00000000`0ffcd263
00000000`0ffcd4cf 498b8c24a0000000 mov     rcx,qword ptr [r12+0A0h]

000000000ffcd4a7应该就是调用EfiEntry(观察传参和返回地址)。

ImageHandle参数指向bootmgfw.efi模块,该参数复杂继续引导过程并调用winload.efi。Systemtable指向UEFI配置表(EFI_SYSTEM_TABLE)的指针,它是 UEFI 应用程序与硬件固件之间沟通的唯一桥梁,其结构如下:

看看EfiEntry的反汇编:

txt 复制代码
kd> uf bootmgfw!EfiEntry
bootmgfw!EfiEntry:
00000000`1003f050 48895c2408      mov     qword ptr [rsp+8],rbx
00000000`1003f055 57              push    rdi
00000000`1003f056 4883ec30        sub     rsp,30h
00000000`1003f05a 0f1005a74ffdff  movups  xmm0,xmmword ptr [bootmgfw!EfiBootmgrDbxSvnGuid (00000000`10014008)]
00000000`1003f061 488bda          mov     rbx,rdx
00000000`1003f064 488bf9          mov     rdi,rcx
00000000`1003f067 41b800000700    mov     r8d,70000h
00000000`1003f06d 488d542420      lea     rdx,[rsp+20h]
00000000`1003f072 488bcb          mov     rcx,rbx
00000000`1003f075 f30f7f442420    movdqu  xmmword ptr [rsp+20h],xmm0
00000000`1003f07b e8643d0900      call    bootmgfw!DbxSvnCheck (00000000`100d2de4)
00000000`1003f080 4885c0          test    rax,rax
00000000`1003f083 790f            jns     bootmgfw!EfiEntry+0x44 (00000000`1003f094)  Branch

bootmgfw!EfiEntry+0x35:
00000000`1003f085 48b90e00000000000080 mov rcx,800000000000000Eh
00000000`1003f08f 483bc1          cmp     rax,rcx
00000000`1003f092 752d            jne     bootmgfw!EfiEntry+0x71 (00000000`1003f0c1)  Branch

bootmgfw!EfiEntry+0x44:
00000000`1003f094 488bd3          mov     rdx,rbx
00000000`1003f097 c605d3e92b0001  mov     byte ptr [bootmgfw!DbxSvnChecked (00000000`102fda71)],1
00000000`1003f09e 488bcf          mov     rcx,rdi
00000000`1003f0a1 e8da600100      call    bootmgfw!EfiInitCreateInputParametersEx (00000000`10055180) ;关键调用
00000000`1003f0a6 4885c0          test    rax,rax
00000000`1003f0a9 7507            jne     bootmgfw!EfiEntry+0x62 (00000000`1003f0b2)  Branch

bootmgfw!EfiEntry+0x5b:
00000000`1003f0ab b80d0000c0      mov     eax,0C000000Dh
00000000`1003f0b0 eb08            jmp     bootmgfw!EfiEntry+0x6a (00000000`1003f0ba)  Branch

bootmgfw!EfiEntry+0x62:
00000000`1003f0b2 488bc8          mov     rcx,rax
00000000`1003f0b5 e85e010000      call    bootmgfw!BmMain (00000000`1003f218) ;关键调用

bootmgfw!EfiEntry+0x6a:
00000000`1003f0ba 8bc8            mov     ecx,eax
00000000`1003f0bc e89f6d0100      call    bootmgfw!EfiGetEfiStatusCode (00000000`10055e60)

bootmgfw!EfiEntry+0x71:
00000000`1003f0c1 488b5c2440      mov     rbx,qword ptr [rsp+40h]
00000000`1003f0c6 4883c430        add     rsp,30h
00000000`1003f0ca 5f              pop     rdi
00000000`1003f0cb c3              ret

上面有两个关键调用:EfiInitCreateInputParametersEx和BmMain。

EfiInitCreateInputParametersEx

它有两个作用:

  1. 将EfiEntry参数转化为bootmgfw.efi所需要的格式。UEFI 固件传给 EfiEntry的参数是通用的(比如 ImageHandle和 SystemTable)。这个函数负责把这些底层参数,转换成 Bootmgr 内部结构体(如 BL_APPLICATION_ENTRY)所需的格式。
  2. 它会调用内部函数(如 EfiInitpCreateApplicationEntry),去扫描并读取BCD文件。它会根据 BCD 中的配置(比如你之前设置的 bootdebug on),初始化引导环境。

BmMain

BmMainWindows 引导管理器(Bootmgfw)的核心BmMain 的主要工作是读取 BCD 配置,并决定下一步该做什么:

  1. 处理休眠

    • 检查 :硬盘上是否有休眠文件(hiberfil.sys)?
    • 执行 :如果有,就加载 winresume.efi 恢复系统,直接唤醒,跳过后续所有步骤。
  2. 显示启动菜单

    • 检查:BCD 中是否配置了多个系统或 F8 调试菜单?
    • 执行 :如果是,就在屏幕上显示启动菜单,等待用户选择
  3. 处理错误

    • 检查:之前的启动是否失败过?
    • 执行:如果是,进入**"自动修复"**模式(Windows Recovery Environment)。
  4. 加载内核

    • 如果上述情况都不满足 ,就进入正常启动 流程:
      a. 从 BCD 中读取默认启动项
      b. 加载对应的操作系统加载器(winload.efi
      c. 将控制权移交给它

BmMain 执行路径如下:

text 复制代码
BmMain
  └──> BlInitializeLibrary          // 初始化基础库
        └──> ReinitializeLibrary     // 重新初始化库
              └──> BlBdInitialize    // 初始化引导调试器
                    └──> BlBdStart   // 启动调试器
                          └──> DbgLoadImageSymbols
                                └──> DebugService2

BmMain 是 Windows 启动过程中最后一段通用代码 ,之后的所有代码(winload.efi, ntoskrnl.exe)都高度依赖具体的 Windows 版本。它是连接"firmware"和"Windows"的桥梁。

BmMain函数会检查上次启动是否失败,如果失败就进入"自动修复"或"启动恢复"模式,尝试修复问题。这是通过检查BCD中一个特殊的标志位(LastBootSucceeded)来判断上去启动是否成功的。

LastBootSucceeded这个标志很有用。比如我们开发了一个开机自启的驱动程序,但是这个驱动程序存在缺陷会导致系统启动失败。那么我们就可以在驱动中检查LastBootSucceeded,如果上一次启动失败,我们的驱动就先不加载。这样可以让系统正常启动,便于排查启动失败的原因。

随后Bootmgfw会寻找WinLoad.exe文件,并校验文件的完整性,将其加载。BootMgfw还会做更新GDT、IDT,随后用调试平台的相关控制权移交给WinLoad。

**就在bootmgfw将控制权交给winload之前,其会执行一项重要的操作:开启并初始化分页模式。**具体是怎么开启的可以看我的这篇博客:https://blog.csdn.net/Oorchi/article/details/148702135。但是这篇博客是在32位环境下分析的,在64位环境下,保护模式是UEFI固件在bootmgfw被加载之前就开启了。UEFI 固件在初始化过程中,自己就会切换到保护模式。

WinLoad.efi

winload主要任务是把操作系统内核加载到内存中。winload.efi是连接固件(UEFI)与 Windows 内核(ntoskrnl.exe)的桥梁。它的核心任务不是运行系统,而是为内核运行准备必要的环境。

开启winload.efi断下来时,回溯一下:

txt 复制代码
winload!DebugService2+0x5:
00000000`00d43185 cc              int     3
kd> k
 # Child-SP          RetAddr               Call Site
00 00000000`001a7b88 00000000`00b0166e     winload!DebugService2+0x5
01 00000000`001a7b90 00000000`00cc99f0     winload!DbgLoadImageSymbols+0x62
02 00000000`001a7be0 00000000`00cc9fbf     winload!BlBdStart+0x1c8
03 00000000`001a7cb0 00000000`00c39e41     winload!BlBdInitialize+0x40b
04 00000000`001a7e30 00000000`00c383ac     winload!InitializeLibrary+0x4bd
05 00000000`001a7ed0 00000000`00aac1ab     winload!BlInitializeLibrary+0x58
06 00000000`001a7f00 00000000`102a0d66     winload!OslMain+0x10b
07 00000000`001a7fe0 00000000`00000000     0x102a0d66
  • OslMainwinload 的主要入口函数,相当于 main()
  • 它调用了 BlInitializeLibrary 来初始化系统库。
  • BlInitializeLibraryBoot Library 的初始化入口。
  • InitializeLibrary:更底层的库初始化,为后续操作(如调试、内存管理)做准备。
  • BlBdInitialize:初始化启动调试引擎(Boot Debugger)。
  • BlBdStart:启动调试通道,建立与主调试器(WinDbg)的连接。
  • DbgLoadImageSymbols:加载镜像的调试符号(此时可能是 winload 自身)。
  • DebugService2 :一个内核调试服务函数,最终触发了 int 3 中断。

看看winload.efi模块:

txt 复制代码
kd> lmDvm winload
Browse full module list
start             end                 module name
00000000`00aab000 00000000`00e1b000   winload    (pdb symbols)          d:\windbgsymbols\winload_prod.pdb\B863EB290D0DE876EE414C943E2DE1542\winload_prod.pdb
    Loaded symbol image file: winload.efi
    Image path: winload.efi
    Image name: winload.efi
    Browse all global symbols  functions  data  Symbol Reload
    Image was built with /Brepro flag.
    Timestamp:        1500371F (This is a reproducible build file hash, not a timestamp)
    CheckSum:         00338E28
    ImageSize:        00370000
    Mapping Form:     Loaded
    File version:     10.0.26100.8036
    Product version:  10.0.26100.8036
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0409.04b0
    Information from resource tables:
        CompanyName:      Microsoft Corporation
        ProductName:      Microsoft® Windows® Operating System
        InternalName:     osloader.exe
        OriginalFilename: osloader.exe
        ProductVersion:   10.0.26100.8036
        FileVersion:      10.0.26100.8036 (WinBuild.160101.0800)
        FileDescription:  OS Loader
        LegalCopyright:   © Microsoft Corporation. All rights reserved.

该模块最主要的工作就是加载各种模块到内存中:

  1. 加载内核:将 ntoskrnl.exe(内核文件)和 hal.dll(硬件抽象层)读入内存。

  2. 加载驱动:扫描注册表,将启动类型(Start值为 0)的驱动程序加载到内存。

  3. 传递数据:准备 LOADER_PARAMETER_BLOCK结构体,这个结构体包含了内存布局、ACPI 表等关键信息。

txt 复制代码
kd> uf OslpMain
winload!OslpMain:
00000000`00ab377c 48895c2408      mov     qword ptr [rsp+8],rbx
00000000`00ab3781 4889742418      mov     qword ptr [rsp+18h],rsi
00000000`00ab3786 55              push    rbp
00000000`00ab3787 488dac2460f6ffff lea     rbp,[rsp-9A0h]
00000000`00ab378f 4881eca00a0000  sub     rsp,0AA0h
00000000`00ab3796 488bd9          mov     rbx,rcx
00000000`00ab3799 c685b809000000  mov     byte ptr [rbp+9B8h],0
00000000`00ab37a0 488d4c2470      lea     rcx,[rsp+70h]
00000000`00ab37a5 33d2            xor     edx,edx
00000000`00ab37a7 41b8300a0000    mov     r8d,0A30h
00000000`00ab37ad e8ce132a00      call    winload!memset (00000000`00d54b80)
00000000`00ab37b2 be10000000      mov     esi,10h
00000000`00ab37b7 4c8d05b2872d00  lea     r8,[winload!SymCryptTestKey32 (00000000`00d8bf70)]
00000000`00ab37be 448bce          mov     r9d,esi
00000000`00ab37c1 488d1538c72a00  lea     rdx,[winload!SymCryptAesBlockCipher_Fast (00000000`00d5ff00)]
00000000`00ab37c8 488d4c2470      lea     rcx,[rsp+70h]
00000000`00ab37cd e872062200      call    winload!SymCryptGcmExpandKey (00000000`00cd3e44)
00000000`00ab37d2 85c0            test    eax,eax
00000000`00ab37d4 0f852d010000    jne     winload!OslpMain+0x18b (00000000`00ab3907)  Branch

winload!OslpMain+0x5e:
00000000`00ab37da 4889742448      mov     qword ptr [rsp+48h],rsi
00000000`00ab37df 488d442453      lea     rax,[rsp+53h]
00000000`00ab37e4 4889442440      mov     qword ptr [rsp+40h],rax
00000000`00ab37e9 448d46fc        lea     r8d,[rsi-4]
00000000`00ab37ed 48c744243803000000 mov   qword ptr [rsp+38h],3
00000000`00ab37f6 488d442450      lea     rax,[rsp+50h]
00000000`00ab37fb 4889442430      mov     qword ptr [rsp+30h],rax
00000000`00ab3800 488d1579872d00  lea     rdx,[winload!SymCryptTestKey32+0x10 (00000000`00d8bf80)]
00000000`00ab3807 488d0582872d00  lea     rax,[winload!SymCryptTestMsg3 (00000000`00d8bf90)]
00000000`00ab380e 4533c9          xor     r9d,r9d
00000000`00ab3811 4889442428      mov     qword ptr [rsp+28h],rax
00000000`00ab3816 488d4c2470      lea     rcx,[rsp+70h]
00000000`00ab381b 488364242000    and     qword ptr [rsp+20h],0
00000000`00ab3821 e84a0b2200      call    winload!SymCryptGcmEncrypt (00000000`00cd4370)
00000000`00ab3826 448d4603        lea     r8d,[rsi+3]
00000000`00ab382a 488d15077e2d00  lea     rdx,[winload!`string'+0x48 (00000000`00d8b638)]
00000000`00ab3831 488d4c2450      lea     rcx,[rsp+50h]
00000000`00ab3836 e8950f2a00      call    winload!memcmp (00000000`00d547d0)
00000000`00ab383b 85c0            test    eax,eax
00000000`00ab383d 0f85cf000000    jne     winload!OslpMain+0x196 (00000000`00ab3912)  Branch

winload!OslpMain+0xc7:
00000000`00ab3843 4889742448      mov     qword ptr [rsp+48h],rsi
00000000`00ab3848 488d442453      lea     rax,[rsp+53h]
00000000`00ab384d 4889442440      mov     qword ptr [rsp+40h],rax
00000000`00ab3852 448d46fc        lea     r8d,[rsi-4]
00000000`00ab3856 48c744243803000000 mov   qword ptr [rsp+38h],3
00000000`00ab385f 488d442450      lea     rax,[rsp+50h]
00000000`00ab3864 4889442430      mov     qword ptr [rsp+30h],rax
00000000`00ab3869 488d1510872d00  lea     rdx,[winload!SymCryptTestKey32+0x10 (00000000`00d8bf80)]
00000000`00ab3870 488d442450      lea     rax,[rsp+50h]
00000000`00ab3875 4533c9          xor     r9d,r9d
00000000`00ab3878 4889442428      mov     qword ptr [rsp+28h],rax
00000000`00ab387d 488d4c2470      lea     rcx,[rsp+70h]
00000000`00ab3882 488364242000    and     qword ptr [rsp+20h],0
00000000`00ab3888 e8c70c2200      call    winload!SymCryptGcmDecrypt (00000000`00cd4554)
00000000`00ab388d 85c0            test    eax,eax
00000000`00ab388f 756b            jne     winload!OslpMain+0x180 (00000000`00ab38fc)  Branch

winload!OslpMain+0x115:
00000000`00ab3891 0fb7442450      movzx   eax,word ptr [rsp+50h]
00000000`00ab3896 663b05f3862d00  cmp     ax,word ptr [winload!SymCryptTestMsg3 (00000000`00d8bf90)]
00000000`00ab389d 755d            jne     winload!OslpMain+0x180 (00000000`00ab38fc)  Branch

winload!OslpMain+0x123:
00000000`00ab389f 8a442452        mov     al,byte ptr [rsp+52h]
00000000`00ab38a3 3a05e9862d00    cmp     al,byte ptr [winload!SymCryptTestMsg3+0x2 (00000000`00d8bf92)]
00000000`00ab38a9 7551            jne     winload!OslpMain+0x180 (00000000`00ab38fc)  Branch

winload!OslpMain+0x12f:
00000000`00ab38ab 488d95b8090000  lea     rdx,[rbp+9B8h]
00000000`00ab38b2 488bcb          mov     rcx,rbx
00000000`00ab38b5 e82a080000      call    winload!OslPrepareTarget (00000000`00ab40e4)
00000000`00ab38ba 8bd8            mov     ebx,eax
00000000`00ab38bc 85c0            test    eax,eax
00000000`00ab38be 7810            js      winload!OslpMain+0x154 (00000000`00ab38d0)  Branch

winload!OslpMain+0x144:
00000000`00ab38c0 80bdb809000000  cmp     byte ptr [rbp+9B8h],0
00000000`00ab38c7 7407            je      winload!OslpMain+0x154 (00000000`00ab38d0)  Branch

winload!OslpMain+0x14d:
00000000`00ab38c9 e892260100      call    winload!OslExecuteTransition (00000000`00ac5f60)
00000000`00ab38ce 8bd8            mov     ebx,eax

winload!OslpMain+0x154:
00000000`00ab38d0 e8cff00100      call    winload!OslVsmScrubSecrets (00000000`00ad29a4)
00000000`00ab38d5 48833db392340000 cmp     qword ptr [winload!EfiBS (00000000`00dfcb90)],0
00000000`00ab38dd 7505            jne     winload!OslpMain+0x168 (00000000`00ab38e4)  Branch

winload!OslpMain+0x163:
00000000`00ab38df e884c10300      call    winload!BlFwReboot (00000000`00aefa68)

winload!OslpMain+0x168:
00000000`00ab38e4 4c8d9c24a00a0000 lea     r11,[rsp+0AA0h]
00000000`00ab38ec 8bc3            mov     eax,ebx
00000000`00ab38ee 498b5b10        mov     rbx,qword ptr [r11+10h]
00000000`00ab38f2 498b7320        mov     rsi,qword ptr [r11+20h]
00000000`00ab38f6 498be3          mov     rsp,r11
00000000`00ab38f9 5d              pop     rbp
00000000`00ab38fa c3              ret

winload!OslpMain+0x180:
00000000`00ab38fc b9326d6367      mov     ecx,67636D32h
00000000`00ab3901 e86e411800      call    winload!SymCryptFatal (00000000`00c37a74)
00000000`00ab3906 cc              int     3

winload!OslpMain+0x18b:
00000000`00ab3907 b9306d6367      mov     ecx,67636D30h
00000000`00ab390c e863411800      call    winload!SymCryptFatal (00000000`00c37a74)
00000000`00ab3911 cc              int     3

winload!OslpMain+0x196:
00000000`00ab3912 b9316d6367      mov     ecx,67636D31h
00000000`00ab3917 e858411800      call    winload!SymCryptFatal (00000000`00c37a74)
00000000`00ab391c cc              int     3
00000000`00ab391d cc              int     3
00000000`00ab391e cc              int     3
00000000`00ab391f cc              int     3
00000000`00ab3920 48895c2408      mov     qword ptr [rsp+8],rbx
00000000`00ab3925 48896c2418      mov     qword ptr [rsp+18h],rbp
00000000`00ab392a 56              push    rsi
00000000`00ab392b 57              push    rdi
00000000`00ab392c 4156            push    r14
00000000`00ab392e 4883ec20        sub     rsp,20h
00000000`00ab3932 4533f6          xor     r14d,r14d
00000000`00ab3935 488be9          mov     rbp,rcx
00000000`00ab3938 4c89742448      mov     qword ptr [rsp+48h],r14
00000000`00ab393d e802461800      call    winload!BlGetApplicationEntry (00000000`00c37f44)
00000000`00ab3942 4c8d442448      lea     r8,[rsp+48h]
00000000`00ab3947 ba02000022      mov     edx,22000002h
00000000`00ab394c 488b4818        mov     rcx,qword ptr [rax+18h]
00000000`00ab3950 e8c3511800      call    winload!BlGetBootOptionString (00000000`00c38b18)
00000000`00ab3955 8bd8            mov     ebx,eax
00000000`00ab3957 85c0            test    eax,eax
00000000`00ab3959 0f888d000000    js      winload!OslpInitializeSystemRoot+0xcc (00000000`00ab39ec)  Branch

winload!OslpInitializeSystemRoot+0x3f:
00000000`00ab395f 488b7c2448      mov     rdi,qword ptr [rsp+48h]
00000000`00ab3964 4883c8ff        or      rax,0FFFFFFFFFFFFFFFFh

winload!OslpInitializeSystemRoot+0x48:
00000000`00ab3968 48ffc0          inc     rax
00000000`00ab396b 6644393447      cmp     word ptr [rdi+rax*2],r14w
00000000`00ab3970 75f6            jne     winload!OslpInitializeSystemRoot+0x48 (00000000`00ab3968)  Branch

winload!OslpInitializeSystemRoot+0x52:
00000000`00ab3972 66837c47fe5c    cmp     word ptr [rdi+rax*2-2],5Ch
00000000`00ab3978 744e            je      winload!OslpInitializeSystemRoot+0xa8 (00000000`00ab39c8)  Branch

winload!OslpInitializeSystemRoot+0x5a:
00000000`00ab397a 488d7002        lea     rsi,[rax+2]
00000000`00ab397e 488d0c36        lea     rcx,[rsi+rsi]
00000000`00ab3982 e8b9521c00      call    winload!BlMmAllocateHeap (00000000`00c78c40)
00000000`00ab3987 48890562973400  mov     qword ptr [winload!OslSystemRoot (00000000`00dfd0f0)],rax
00000000`00ab398e 4885c0          test    rax,rax
00000000`00ab3991 7507            jne     winload!OslpInitializeSystemRoot+0x7a (00000000`00ab399a)  Branch

winload!OslpInitializeSystemRoot+0x73:
00000000`00ab3993 bb170000c0      mov     ebx,0C0000017h
00000000`00ab3998 eb52            jmp     winload!OslpInitializeSystemRoot+0xcc (00000000`00ab39ec)  Branch

winload!OslpInitializeSystemRoot+0x7a:
00000000`00ab399a 4c8bc7          mov     r8,rdi
00000000`00ab399d 488bd6          mov     rdx,rsi
00000000`00ab39a0 488bc8          mov     rcx,rax
00000000`00ab39a3 e898262900      call    winload!wcscpy_s (00000000`00d46040)
00000000`00ab39a8 488b0d41973400  mov     rcx,qword ptr [winload!OslSystemRoot (00000000`00dfd0f0)]
00000000`00ab39af 4c8d05f68f2a00  lea     r8,[winload!`string' (00000000`00d5c9ac)]
00000000`00ab39b6 488bd6          mov     rdx,rsi
00000000`00ab39b9 e8f2252900      call    winload!wcscat_s (00000000`00d45fb0)
00000000`00ab39be 488bcf          mov     rcx,rdi
00000000`00ab39c1 e8b6541c00      call    winload!BlMmFreeHeap (00000000`00c78e7c)
00000000`00ab39c6 eb07            jmp     winload!OslpInitializeSystemRoot+0xaf (00000000`00ab39cf)  Branch

winload!OslpInitializeSystemRoot+0xa8:
00000000`00ab39c8 48893d21973400  mov     qword ptr [winload!OslSystemRoot (00000000`00dfd0f0)],rdi

winload!OslpInitializeSystemRoot+0xaf:
00000000`00ab39cf 488b151a973400  mov     rdx,qword ptr [winload!OslSystemRoot (00000000`00dfd0f0)]
00000000`00ab39d6 488bcd          mov     rcx,rbp
00000000`00ab39d9 e8b27b1800      call    winload!BlAppendUnicodeToString (00000000`00c3b590)
00000000`00ab39de 8bcb            mov     ecx,ebx
00000000`00ab39e0 84c0            test    al,al
00000000`00ab39e2 bb170000c0      mov     ebx,0C0000017h
00000000`00ab39e7 0f44cb          cmove   ecx,ebx
00000000`00ab39ea 8bd9            mov     ebx,ecx

winload!OslpInitializeSystemRoot+0xcc:
00000000`00ab39ec 488b6c2450      mov     rbp,qword ptr [rsp+50h]
00000000`00ab39f1 8bc3            mov     eax,ebx
00000000`00ab39f3 488b5c2440      mov     rbx,qword ptr [rsp+40h]
00000000`00ab39f8 4883c420        add     rsp,20h
00000000`00ab39fc 415e            pop     r14
00000000`00ab39fe 5f              pop     rdi
00000000`00ab39ff 5e              pop     rsi
00000000`00ab3a00 c3              ret

winload最后的工作就是将CPU的控制权交给内核。winload会准备一个LOADER_PARAMETER_BLOCK的结构,传递给KiSystemStartup(内核模块的入口函数)。这个结构体中记录了winload在加载过程中获取的各种信息,KiSystemStartup会利用这些信息初始化内核。

ntkrnlmp.exe

现在重要来到了Windows内核中,我们看看Windows内核的模块:

txt 复制代码
kd> lmDvm nt
Browse full module list
start             end                 module name
fffff806`87000000 fffff806`88450000   nt         (pdb symbols)          d:\windbgsymbols\ntkrnlmp.pdb\24DE61DB3A27C5B383DFF181CB51F7E11\ntkrnlmp.pdb
    Loaded symbol image file: ntkrnlmp.exe
    Image path: ntkrnlmp.exe
    Image name: ntkrnlmp.exe
    Browse all global symbols  functions  data  Symbol Reload
    Image was built with /Brepro flag.
    Timestamp:        4473D78D (This is a reproducible build file hash, not a timestamp)
    CheckSum:         00C7F356
    ImageSize:        01450000
    Mapping Form:     Loaded
    File version:     10.0.26100.8036
    Product version:  10.0.26100.8036
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0409.04b0
    Information from resource tables:
        CompanyName:      Microsoft Corporation
        ProductName:      Microsoft® Windows® Operating System
        InternalName:     ntkrnlmp.exe
        OriginalFilename: ntkrnlmp.exe
        ProductVersion:   10.0.26100.8036
        FileVersion:      10.0.26100.8036 (WinBuild.160101.0800)
        FileDescription:  NT Kernel & System
        LegalCopyright:   © Microsoft Corporation. All rights reserved.

Unable to enumerate kernel-mode unloaded modules, HRESULT 0x80004005

ntkrnlmp末尾的mp为Multi-Processor(多处理器)之意。

回溯栈:

txt 复制代码
kd> k
 # Child-SP          RetAddr               Call Site
00 fffff806`1afb3de8 fffff806`874a41bd     nt!DebugService2+0x5
01 fffff806`1afb3df0 fffff806`87b764b0     nt!DbgLoadImageSymbols+0x8d
02 fffff806`1afb3e40 fffff806`87b4b576     nt!KdInitSystem+0x680
03 fffff806`1afb3fc0 00000000`00000000     nt!KiSystemStartup+0x1d6

KiSystemStartup

KiSystemStartup为内核的入口函数。它会将winload所传递的LOADER_PARAMETER_BLOCK结构体保存到KeLoaderBlock结构体中:

txt 复制代码
kd> uf KiSystemStartup
nt!KiSystemStartup:
fffff806`87b4b3a0 4883ec38        sub     rsp,38h
fffff806`87b4b3a4 4c897c2430      mov     qword ptr [rsp+30h],r15
fffff806`87b4b3a9 4c8bfc          mov     r15,rsp
fffff806`87b4b3ac 48890d6da74700  mov     qword ptr [nt!KeLoaderBlock (fffff806`87fc5b20)],rcx
fffff806`87b4b3b3 4c8b9188000000  mov     r10,qword ptr [rcx+88h]
fffff806`87b4b3ba 41837a2400      cmp     dword ptr [r10+24h],0
fffff806`87b4b3bf 750c            jne     nt!KiSystemStartup+0x2d (fffff806`87b4b3cd)  Branch

我们看看LOADER_PARAMETER_BLOCK结构体(利用KeLoaderBlock的地址):

txt 复制代码
kd> dt nt!_LOADER_PARAMETER_BLOCK fffff806`87fc5b20
   +0x000 OsMajorVersion   : 0x1662c070
   +0x004 OsMinorVersion   : 0xfffff806
   +0x008 Size             : 0
   +0x00c OsLoaderSecurityVersion : 0
   +0x010 LoadOrderListHead : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x020 MemoryDescriptorListHead : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x030 BootDriverListHead : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00001f49 ]
   +0x040 EarlyLaunchListHead : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x050 CoreDriverListHead : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x060 CoreExtensionsDriverListHead : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x070 TpmCoreDriverListHead : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x080 KernelStack      : 0
   +0x088 Prcb             : 0
   +0x090 Process          : 0
   +0x098 Thread           : 0
   +0x0a0 KernelStackSize  : 0
   +0x0a4 RegistryLength   : 0x1000000
   +0x0a8 RegistryBase     : (null) 
   +0x0b0 ConfigurationRoot : (null) 
   +0x0b8 ArcBootDeviceName : (null) 
   +0x0c0 ArcHalDeviceName : (null) 
   +0x0c8 NtBootPathName   : (null) 
   +0x0d0 NtHalPathName    : 0x00000000`00000001  ""
   +0x0d8 LoadOptions      : (null) 
   +0x0e0 NlsData          : (null) 
   +0x0e8 ArcDiskInformation : (null) 
   +0x0f0 Extension        : (null) 
   +0x0f8 u                : <unnamed-tag>
   +0x108 FirmwareInformation : _FIRMWARE_INFORMATION_LOADER_BLOCK
   +0x148 OsBootstatPathName : (null) 
   +0x150 ArcOSDataDeviceName : (null) 
   +0x158 ArcWindowsSysPartName : (null) 
   +0x160 MemoryDescriptorTree : _RTL_RB_TREE

总的来说,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.exe

SMSS(Session Manager Subsystem)作为 Windows 启动链中承上启下的关键进程,它从内核手中接管系统,负责搭建用户态的运行环境。它的工作重心在于"环境配置"与"进程孵化",具体执行流程如下:

1. 系统环境初始化

SMSS 启动后的首要任务是根据注册表配置完善系统环境。它会读取注册表 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager 下的配置,执行以下关键操作:

  • 分页文件管理:根据配置创建或调整系统的虚拟内存页面文件(Pagefile.sys),为后续运行大型程序提供内存交换空间。
  • 文件系统检查:检查注册表中标记为"脏"的卷(Dirty Volume),触发磁盘检查(Autochk)以确保文件系统的一致性。
  • 已知DLL映射 :建立系统核心动态链接库(如 ntdll.dll, kernel32.dll)的 Section 对象,确保后续启动的进程能高效共享这些基础库。

2. 核心子系统与登录进程启动

完成基础配置后,SMSS 负责孵化 Windows 图形界面和交互所需的"双子星"进程,这是系统可视化的前提:

  • 启动 CSRSS:创建客户端/服务器运行时子系统(CSRSS),这是 Windows 图形界面(Win32 子系统)的基础守护进程,负责控制台窗口管理和进程/线程的创建与删除。
  • 启动 WinLogon:创建 Windows 登录进程(WinLogon),该进程专门负责处理用户的登录认证、桌面切换以及安全桌面的显示。

3. 会话管理与生命周期维护

SMSS 不仅仅负责启动,还负责后续会话的动态管理。当用户登录或远程桌面连接时,SMSS 会创建新的会话(Session),并为此会话分配专用的 csrss.exewinlogon.exe 实例。它持续监控这两个关键进程的状态,一旦它们意外终止,SMSS 会根据系统策略决定是否触发蓝屏或重启,从而维护系统的稳定性。

Session是Windows系统和用户交互环境的逻辑容器。简单来说就是系统为每个用户分配的独立的空间。当用户登录时,系统会创建一个新的 Session,这个空间里包含了该用户专属的桌面、窗口站、环境变量以及访问权限。

下图来自《Windows内核原理与实现》:

总结

建议还是跟着张银奎老师的步伐,亲自调试一遍。先对整个过程有个大致的映像,以后需要在哪个步骤中做文章的时候,再仔细研究该步骤的细节。

下图摘自《软件调试》:

相关推荐
杂家4 小时前
Windows部署Redis
数据库·windows·redis
酿情师5 小时前
FinalShell 下载与安装指南
linux·服务器·windows·ssh
小侯不躺平.6 小时前
C++ Boost库【4】 --分词器的使用
c++·windows·microsoft
beyond阿亮7 小时前
Hermes Agent 在Windows上接入飞书完整指南
人工智能·windows·ai·hermes agent
ba_pi7 小时前
windows Claude Code接入deepseek,全局配置
windows·claude·deepseek
stanleyrain7 小时前
Windows 实现 Linux 风格“选中即复制,中键即粘贴”操作指南
linux·运维·windows
开开心心就好8 小时前
免费开源的网课教学屏幕画板工具
windows·eureka·计算机外设·word·excel·etcd·csdn开发云
周杰伦fans8 小时前
C# 从 List 中移除另一个集合
windows·c#
qxl_79991511 小时前
Windows 显卡掉线无报警|模型推理全套防呆方案(实操完整版)
windows·stm32·单片机·推理显卡掉线误报警防呆
数智工坊1 天前
VMware 17 Pro 中 Ubuntu 虚拟机共享 Windows 文件夹(完美踩坑版)
linux·人工智能·windows·ubuntu