Windows软件调试初探-启动过程
Windows软件调试初探-启动过程
本篇没特殊说明就是按照时间先后顺序写。
BootMgr
系统上电后执行固化在系统主板上的固件代码,检测和初始化基本硬件,如CPU、内存、键盘、显卡和磁盘等。之后加载并将执行权移交给操作系统启动程序,即NTLDR。NTLDR依次分为BootMgr、WinLoad、WinResume。BootMgr从系统引导配置数据BCD中读取启动设置,当有多个启动选线则显示启动菜单让用户选择。现有图形界面启动程序BootIM.exe,启用经典启动菜单可以:
1 | bcdedit /set bootmenupolicy Legacy |
为了启用BootMgr和WinLoad的调试引擎,执行以下命令。调试时先用WinDBG监听,之后会自动挂靠。打开调试模式后无法正常进入系统,只能进入安全模式将on改为off。NT内核加载后调试引擎在配置双机调试时已打开。
1 | bcdedit /set {bootmgr} bootdebug on |
查看栈帧:
1 | kd> kn |
其中bootmgfw!BmMain
为BootMgr的64位代码入口函数,栈帧#07及往下为实模式执行痕迹。加载WinLoad.exe后,BootMgr启用新GDT和IDT,并把调用平台控制权移交给WinLoad,这一步在x86下用Archx86TransferTo32BitApplicationAsm
函数。
此外NTLDR调用NTDETECT.COM做基本硬件检查并收集硬件信息,放入注册表。找不到该文件则直接重启,若发现缺少必须的硬件或固件如ACPI支持等则无法启动。
WinLoad
WinLoad启用CPU分页机制并改善运行环境,再初始化自己的支持库,当启用引导调试支持是初始化调试引擎。当用户按F8或上次没正常关机则显示“高级启动”菜单。函数OslpLoadSystemHive
读取加载注册表System Hive。
WinLoad核心任务是加载操作系统内核文件和引导类型的设备驱动程序。首先加载NTOSKRNL.EXE。此时磁盘和文件系统驱动程序还没加载,WinLoad用的私有文件访问函数如FileIoOpen
打开文件,打开失败则调用它的BlpFileOpen
返回错误码0C000000Dh,否则成功返回0。
接下来加载硬件抽象层模块HAL.DLL、调试KDCOM.DLL以及依赖模块如PSHED.DLL、引导期间蓝屏显示BOOTVID.DLL、日志CLFS.SYS、模块完整性检查CI.DLL。然后加载引导类型的设备驱动程序。
其次用OslArchpKernelSetupPhase0
位内核准备新GDT和IDT,用OslBuildKernelMemoryMap
建立内存映射,用OslArchTranserToKernel
把GDT和IDT地址加载到CPU并调用内核入口,把控制权移交给内核。
内核初始化
KiSystemStartup
为NT内核模块入口函数,之前WinLoad为它传入LOADER_PARAMETER_BLOCK结构。该结构地址保存在全局变量KeLoaderBlock
中但启动后被释放为0。
1 | 3: kd> dt !_LOADER_PARAMETER_BLOCK |
接下来检测CPU特征和初始化CPU、设置中断描述符表、建立和初始化处理器控制区PCR、建立任务状态段TSS、设置用户调用内核服务的MSR,这些操作在nt!KiInitializeBootStructures
中,则nt!KiSystemStartup
伪代码为:
1 | void KiSystemStartup(LOADER_PARAMETER_BLOCK* pLoaderParaBlock) { |
在KiInitializeBootStructures
会第一次用-1参数调用MmInitSystem
,即只做最基本初始化以满足启动阶段内存分配需求。KiInitializebootStructures
返回后可以动态分配内存且已初始化PRC、GDT、IDT、TSS等。接下来用KdInitSystem
初始化内核调试引擎。用Ctrl+Alt+K启用/停用内核调试引擎初始化断点,此时在启动进入NT内核时快速挂靠WinDBG,则主动挂靠到调试器,运行则需调试器脱离。
1 | kd> kc |
接下来KiInitializeKernel
初始化NT内核。调试方法是用bp
对它下断点,恢复执行并再次命中后,即可单步跟踪。每个CPU唤醒后都执行KiInitializeKernel
但最先执行的0号CPU即启动处理器的工作较全面,主要工作有:
- 用
HvlPhase0Initialize
检查是否在虚拟机中,如果是则检查虚拟机监视器VMM是否是微软公司的Hyper-V,是的话检查版本号,再用HvlEnglightenments
得到VMM的优待以提高性能。 - 用
KiDetectFpuLeakage
检查与浮点指令有关的安全漏洞。 - 用
KiSetPageAttributesTable
初始化页属性表。 - 用
KiConfigureInitialNodes
和KiConfigureProcessorBlock
检查系统拓扑结构,配置初始节点和处理器块。 - 用
KeAddProcessorAffinityEx
不知道干啥。 - 用
KeCompactServiceTable
初始化系统服务表。 - 用
KiSetCacheInformation
初始化CPU高速缓存信息。 - 用
KiInitSystem
初始化内核部件全局数据结构,如蓝屏回调函数链表KeBugCheckCallbackListHead
、性能堪察器列表KiProfileListHead
和各种同步对象。 - 用
HviGetHypervisorFeatures
获取VMM特征。 - 用
KeInitializeProcess
初始化全局结构KiInitialProcess
描述的初始进程。 - 用
KiEnableXSave
通过设置IA32_XSS寄存器,索引0xDA0设置CPU浮点协处理器状态保存选项。 - 用
KiInitializeIdleThread
创建空闲进程。 - 用
HalInitSystem
为当前CPU做硬件抽象层初始化。 - 用
InitBootProcessor
执行只需要启动处理器(0号处理器)执行的动作。调用每个执行体初始化函数,称为阶段0初始化。 - 在
KiCompleteKernelInit
中:用nt!PsInitialSystemProcess
把一个初始化线程附加到系统进程中让其接班,等当前线程跳入空闲循环后CPU转去执行新系统线程,开始新一轮执行体初始化;用KeInitializeDpc
初始化延迟过程调用DPC对象KiProcessPendingForegroundBoosts
和KiTriggerForegroundBoostDpc
。 - 用
KiIdleLoop
进入空闲循环休息,永不返回。
执行体的阶段0初始化
InitBootProcessor
调用进程管理器的阶段0初始化函数PspInitPhase0
:
1 | kd> k |
内核全局变量InitializationPhase
记录当前哪一阶段初始化,0代表阶段0:
1 | 3: kd> dd nt!InitializationPhase L1 |
InitBootProcessor
主要动作有:
- 解析内核启动参数字符串,寻找是否包含用于测试和验证使用的选项,如“PERFMEM”、“BURNMEMORY”、“PORCEGROUPAWARE”等。
- 用
RtlInitNlsTable
和RtlResetRtlTranslations
初始化支持多语言设施。 - 用
WheaInitializeServices
初始化WHEA服务。 - 以参数0调用
HalInitSystem
。 - 用
KeInitializeClock
设置CPU时钟中断并判断是否启用动态时钟,不启用则原因写入KiDynamicTickDisableReason
中,动态时钟指系统空闲时减少时钟中断次数来降低系统功耗。 - 用
PsInitializeQuotaSystem
初始化配额管理设备。 - 用
CmGetSystemControlValues
读取注册表系统控制数据。 - 用
KeInitializeTimerTable
初始化定时器表。 - 用
ExComputeTickCountMultiplier
计算时钟计数器换算因子。 - 用
ExInitSystem
对执行体运行时库初始化。 - 用
KeNumaInitialize
初始化NUMA有关设施。 - 用
VerifierInitSystem
初始化内核验证器。 - 再次用
MmInitSystem
初始化内存管理器,其中并内存管理器调用MiReloadBootLoadedDrivers
重新加载WinLoad加载到内存中引导类型驱动程序。 - 用
HalInitializeBios
初始化固件有关信息。 - 用
InbvDriverInitialize
初始化系统自带显示驱动程序。 - 若启动了内核调试,则反复用
DbgLoadImageSymbols
向内核调试器发送WinLoad模块加载通知。 - 用
HeadlessInit
不知道干啥。 - 用
BootApplicationPersistentDataInitialize
处理固件等早期启动程序希望持久化的数据。某些启动程序没有磁盘这样的持久存储设备,所以在ACPI标准中定义了接口让操作系统帮助固件保存一些信息。 - 用
HalQueryMaximumProcessorCount
查询当前处理器组所支持的逻辑处理器总数。 - 用
ObInitSystem
初始化对象管理器。 - 用
SeInitSystem
初始化安全管理器。 - 用
PspInitPhase0
使进程管理器阶段0初始化,下面详细讲。 - 用
DbgkInitialize
初始化支持用户态调试的内核设施。
PspInitPhase0
初始化进程链表结构,链表头结构记录到全局变量PsActiveProcessHead
中,这也是!process
命令的原理。之后创建进程和线程对象类型,有了类型后才能创建该内核类型的内核对象。KiInitializeKernel
用KeInitializeProcess
初始化非正式进程对象KiInitialProcess
,这个进程对象是直接静态变量定义的。之后又有PspInitializeJobStructures
、PspInitializeSiloStructures
、ExCreateHandleTable
、PspInitializeSystemPartitionPhase
等,然后用PspCreateProcess
创建第一个真正的进程对象即系统进程,ID为4。然后给这个新进程赋予名字System,进程对象地址赋给全局变量PsInitialSystemProcess
,把KiInitialProcess
地址赋给PsIdleProcess
。然后为System进程创建第一个线程,起始地址为Phase1Initialization
函数。
1 | fffff804`03a3aa4e 488d054b9cd5ff lea rax, [nt!Phase1Initialization (fffff804`037946a0)] |
该线程中断请求级别IRQL较高而无法执行。在KiInitializeKernel
返回后,KiSystemStartup
将当前CPU的IRQL降到DISPATCH_LEVEL,并跳转到KiIdleLoop
退化成空闲进程第一个空闲线程。下次发生时钟中断、内核调度线程时系统线程执行,开始阶段1初始化。虽然空闲线程属于空闲进程,但生活在系统进程的进程空间中。
执行体阶段1初始化
系统线程中第一个线程的工作函数为Phase1Initialization
,冗长的部分在其中Phase1InitializationDiscard
中执行,然后又调用nt!IoInitSystem
、nt!Phase1InitializationIoReady
、nt!MmFreeBootDriverInitializationCode
。
1 | # Child-SP RetAddr Call Site |
Phase1InitializationDiscard
用HalInitSystem
再一次给HAL初始化机会,用KeInitializeClock
初始化时钟和系统时间。之后开始用KeStartAllProcessors
来唤醒同伴,简称KSAP。KSAP用HAL的HalEnumerateProcessors
来枚举系统里所有处理器。对于每个处理器都提供IDT、PCR、ISR栈。ISR栈指中断服务例程栈,专门负责处理NMI、双误、机器检查异常等特殊中断情况,因为CPU需切换到全新线程上下文。最后KSAP用HalStartNextProcessor
唤醒一个新CPU。
1 | kd> k |
唤醒后的CPU都会从内核入口开始执行,如KiInitializeKernel
等,但只有第一个CPU会执行所有逻辑如KiInitSystem
。初始化空闲进程也只有0号CPU执行,但每个CPU都需要用KiInitializeIdleThread
为字节创建一个空闲线程。全局变量KeNumberProcessors
为系统CPU个数,初始值为0,每初始化结束一个CPU该值自增1。其他CPU依次返回KiInitializeKernel
和KiSystemStartup
直接开始KiIdleLoop
执行自己的空闲线程。
1 | 1: kd> k |
下一步进行I/O初始化,即建立设备树,枚举系统各种设备并加载驱动程序。总线驱动程序还需要进一步枚举自己的子设备。例如观察设备树:
1 | 3: kd> !devnode |
设备树以及子节点在注册表中HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\Root,一级子节点中必然有ACPI_HAL。如ACPI_HAL的Service键值为“\Driver\ACPI_HAL”,告诉I/O管理器为该设备安装ACPI驱动程序ACPI.sys。该驱动程序读取系统固件中设备表并枚举,分别安装驱动程序。其中PCI总线设备会触发加载其驱动PCL.sys,加载后按照PCI协议枚举PCI设备如子总线控制器、USB总线控制器等,由此不断加入设备树。该I/O阶段1初始化 的函数为IoInitSystem
,其中IoInitSystemPreDrivers
负责初始化内建的和启动类型的驱动程序,IopInitializeSystemDrivers
初始化系统类型驱动程序。
nt!InbvIndicateProgress
返回启动总进程,但功能几乎丧失。
至此内核空间初始化完毕。执行体的阶段1初始化结束前,Phase1Initialization
创建第一个用映像文件创建的进程,即会话管理器进程SMSS.EXE。
创建用户空间
用StartFirstUserProcess
创建会话管理器进程,SMSS会话管理器程序全程叫会话管理器子系统。
1 | 0: kd> k |
失败则触发0x6F蓝屏SESSION3_INITIALIZATION_FAILED。Phase1Initialization
通过等待SMSS进程句柄确保它持续运行。等待超时则SMSS进程还在运行,否则SMSS意外退出,这时引发0x71号蓝屏SESSION5_INITIALIZAION_FAILED。等待SMSS几秒没发现退出则确信正常启动了。此后Phase1Initialization
直接返回导致该线程退出。
SMSS初始化后创建一个LPC端口\SmApiPort用于对外提供服务,如切换或建立新会话请求。注册表中配置SMSS的表键为HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\ControlSet\Control\Session Manager。其中PendingFileRenameOperations子键记录之前挂起的文件删除/改名任务。然后执行BootExecute表键的命令,一般是磁盘检查程序autochk.exe。之后SMSS建立虚拟内存机制所需的页面交换文件。然后建立SubSystems表键下的环境子系统,先加载Win32K.sys,再根据键值内容创建Windows子系统服务进程CSRSS.exe,如下:
1 | %SystemRoot%\system32\csrss.exe ObjectDirectory=\Windows SharedSection=1024,20480,768 Windows=On SubSystemType=Windows ServerDll=basesrv,1 ServerDll=winsrv:UserServerDllInitialization,3 ServerDll=sxssrv,4 ProfileControl=Off MaxRequestThreads=16 |
ServerDll是CSRSS要加载的服务模块,病毒或恶意软件可能会把自己模块加进来。
建立Windows子系统后,SMSS创建WinLoad,负责安全登录工作,掌控登录、重启、关机和屏幕保护程序等。
WinLogon创建0号窗口站和默认桌面对象。窗口站是会话的下一层组织结构,一个会话中可有多个窗口站,同一时刻每个会话只有一个窗口站可与用户交互。每个窗口站有自己的剪贴板,可有多个桌面。WinLogon用Windows子系统内核模块Win32K服务的NtUserCreateWindowStation
创建窗口站,之后用NtUserCreateDesktop
创建桌面。首先创建一个WinLogon桌面供自己使用,再创建Default桌面供应用程序使用。之后WinLogon用SetActiveDesktop
将自己桌面设为当前活动桌面,于是登录桌面便呈现在用户面前。
1 | 0: kd> sxe ld:win32k |
当用户退出或锁定屏幕时,WinLogon将自己桌面切到前台(设为活动的),该桌面又称安全桌面。创建桌面后WinLogon创建服务管理器Services.exe和本地安全认证子系统LSASS.exe。
Credential Provider模型接收用户登录信息,如用户名、密码或指纹,与LSASS进程交互。接下来WinLogon引发用户初始化动作,执行“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\UserInit”,内容一般是UserInit程序:
1 | C:\Windows\system32\userinit.exe, |
UserInit启动后运行“HKEY_CURRENT_USER\SOFTWARE\Policies\Microsoft\Windows\SystemScripts”和“HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System\Scripts”表键下定义的登录脚本。接下来UserInit启动外壳程序,先后在“HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon”和“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon”表键中找Shell键值,内容是Explorer.exe。