KiSystemStartupBootStack 函数分析
1. 概述
KiSystemStartupBootStack 是 KiSwitchToBootStack 完成栈切换后跳转的目标函数。它在内核自己的启动栈 (P0BootStack) 上运行,负责调用 KiInitializeKernel 初始化内核执行体子系统,然后将当前线程优先级设为 0 以触发调度器切换到 Phase1 初始化线程,最终进入空闲循环。这个函数永不返回。
文件位置: ntoskrnl/ke/i386/kiinit.c#L691-L728(file:///d:/reactos/ntoskrnl/ke/i386/kiinit.c#L691)
c
CODE_SEG("INIT")
DECLSPEC_NORETURN
VOID
NTAPI
KiSystemStartupBootStack(VOID)
函数属性
| 属性 | 含义 |
|---|---|
CODE_SEG("INIT") |
代码位于 INIT 段,初始化完成后可丢弃 |
DECLSPEC_NORETURN |
告知编译器此函数永不返回(最终调用 KiIdleLoop) |
NTAPI |
标准 stdcall 调用约定 |
VOID |
无参数 |
2. 调用上下文
2.1 调用方式
此函数不是通过普通的 call 指令调用的,而是由 KiSwitchToBootStack 通过**jmp 跳转**进入:
KiSwitchToBootStack(InitialStack)
mov esp, InitialStack ; 切换栈
sub esp, 0x2A0 ; 预留 NPX_FRAME + ALIGN + KTRAP_FRAME
push (CR0_EM|CR0_TS|CR0_MP) ; 压入 CR0 初始标志
jmp KiSystemStartupBootStack ; ★ 直接跳转,不返回
jmp 意味着:
- 没有压入返回地址
- 栈上只有
KiSwitchToBootStack预留的 NPX 帧空间 + 陷阱帧空间 + CR0 标志值 - 函数执行完后不会返回到
KiSwitchToBootStack
2.2 调用链中的位置
KiSystemStartup (在 FreeLDR 的栈上运行)
└─ KiSwitchToBootStack (内联, 切换 ESP 到 P0BootStack)
└─ jmp KiSystemStartupBootStack [kiinit.c:691] ←★ 本文分析
└─ KiInitializeKernel() [kiinit.c:433]
│ └─ ExpInitializeExecutive() ← 执行体子系统初始化
│ ├─ HalInitSystem(0, ...) --- HAL Phase 0
│ ├─ ObInitSystem() --- 对象管理器
│ ├─ SeInitSystem() --- 安全引用监视器
│ ├─ PsInitSystem(...) --- 进程/线程管理器
│ │ └─ 创建 Phase1Initialization 线程
│ ├─ CmInitSystem() --- 配置管理器 (注册表)
│ └─ ...
│ └─ KeSetPriorityThread(..., 0) → 触发上下文切换
│ └─ Phase1Initialization 线程开始运行
│ ├─ HalInitSystem(1, ...) --- HAL Phase 1
│ ├─ IoInitSystem(...) --- I/O 管理器
│ └─ ... → 最终创建 SMSS → Win32 子系统
└─ KiIdleLoop [thrdini.c:260]
└─ while(1) { YieldProcessor(); ... }
3. 函数体详解
3.1 调用 KiInitializeKernel
c
KiInitializeKernel(&KiInitialProcess.Pcb,
(PKTHREAD)KeLoaderBlock->Thread,
(PVOID)(KeLoaderBlock->KernelStack & ~3),
(PKPRCB)__readfsdword(KPCR_PRCB),
KeNumberProcessors - 1,
KeLoaderBlock);
参数 1: &KiInitialProcess.Pcb --- 初始进程的 PCB
c
// kiinit.c 中定义的全局初始进程和初始线程
extern KPROCESS KiInitialProcess;
extern KTHREAD KiInitialThread;
KiInitialProcess 是内核编译时分配的静态初始进程对象。它是整个系统的根进程 --- 所有后续进程(包括 System 进程)都由它派生。传递 Pcb (Process Control Block) 而不是整个 KPROCESS,因为 KiInitializeKernel 需要的是调度器级别的进程控制结构。
参数 2: (PKTHREAD)KeLoaderBlock->Thread --- 初始空闲线程
这是引导加载程序在 LoaderBlock->Thread 中设置的初始线程指针。在 BSP 上,KiSystemStartup 已将其设为 &KiInitialThread:
c
LoaderBlock->Thread = (ULONG_PTR)&KiInitialThread; // kiinit.c:764
KiInitialThread 是编译时分配的初始线程对象,代表系统的第一个线程 --- 空闲线程。它将在 KiInitializeKernel 中被初始化为每个 CPU 的空闲线程。
参数 3: (PVOID)(KeLoaderBlock->KernelStack & ~3) --- 空闲栈
KernelStack 的低 2 位被引导加载程序用来存放 CR0 标志位(CR0_EM | CR0_TS | CR0_MP),所以需要 & ~3 清除对齐。这个栈实际就是 P0BootStack 数组的顶部。
这个栈将被用作空闲线程的内核栈。KiInitializeKernel 会在栈上构造一个陷阱帧,使空闲线程看起来就像刚被中断/异常唤醒一样。
参数 4: (PKPRCB)__readfsdword(KPCR_PRCB) --- 当前 CPU 的 PRCB
通过 FS 段寄存器从 PCR (Processor Control Region) 中读取 PRCB 字段。x86 上 FS 指向 KGDT_R0_PCR 段 (基址 0xFFDFF000),KPCR_PRCB 偏移量处存放的就是 PRCB 指针。
这一步获取的是当前 CPU 的 PRCB,而 KiProcessorBlock[Number] 中则保存着所有 CPU 的 PRCB 数组。KiInitializeKernel 使用这个 PRCB 初始化该 CPU 的调度器状态。
参数 5: KeNumberProcessors - 1 --- CPU 编号
在调用时 KeNumberProcessors 已经是 BSP 递增后的值(最小为 1),所以 KeNumberProcessors - 1 就是当前 CPU 的编号(BSP 为 0,第一个 AP 为 1,依此类推)。
参数 6: KeLoaderBlock --- 全局 LoaderBlock
KeLoaderBlock 在 KiSystemStartup 的第一行被设为 LoaderBlock 参数:
c
KeLoaderBlock = LoaderBlock;
它包含所有来自引导加载程序的信息:内存描述符列表、启动驱动列表、ARC 路径、启动选项、注册表基址、NLS 数据等。KiInitializeKernel 中的 ExpInitializeExecutive 会使用这些信息初始化各个子系统。
3.2 设置空闲线程优先级为 0
c
Thread = KeGetCurrentThread();
Thread->Priority = 0;
KeGetCurrentThread() 展开为:
c
#define KeGetCurrentThread() KeGetCurrentPrcb()->CurrentThread
即从当前 CPU 的 PCR → PRCB → CurrentThread 指针获取当前线程。在当前场景下,它就是 KiInitialThread(空闲线程)。
将优先级设为 0 是关键步骤 :KiInitializeKernel 内部的 ExpInitializeExecutive 中,PsInitSystem 创建了 Phase1Initialization 线程(优先级为 13 或更高)。当 KeSetPriorityThread(InitThread, 0) 被调用时(在 KiInitializeKernel 末尾),调度器发现空闲线程优先级 (0) 低于 Phase1Initialization 线程,于是执行上下文切换:
当前运行: 空闲线程 (Priority=0, 刚被设为 0)
就绪队列: Phase1Initialization 线程 (Priority=13)
调度器: Phase1Initialization 优先级更高 → 切换!
→ 从空闲线程切换到 Phase1Initialization 线程
→ Phase1Initialization 开始 Phase 1 初始化
→ 当 Phase1Initialization 阻塞等待时,空闲线程恢复运行
3.3 启用中断并降低 IRQL
c
_enable();
KeLowerIrql(DISPATCH_LEVEL);
| 操作 | 目的 |
|---|---|
_enable() |
启用硬件中断(STI 指令)。KiSystemStartup 在早期禁用了中断(cli),现在重新开启 |
KeLowerIrql(DISPATCH_LEVEL) |
将 IRQL 从 HIGH_LEVEL 降到 DISPATCH_LEVEL。DISPATCH_LEVEL 允许普通硬件中断,但禁止线程调度相关操作的中断 |
KeLowerIrql(DISPATCH_LEVEL) 和后续的 WaitIrql = DISPATCH_LEVEL 一起,使空闲线程处于"可被 DPC 中断唤醒"的状态。当 DPC 队列中有待处理的 DPC 时,CPU 会收到 DISPATCH_LEVEL 的软件中断,触发调度。
3.4 设置等待 IRQL
c
Thread->WaitIrql = DISPATCH_LEVEL;
设置线程的等待 IRQL。当空闲线程处于空闲循环中时,其他代码可以通过在这个 IRQL 上触发中断来唤醒它(例如向空闲线程的 DPC 列表添加项)。
3.5 进入空闲循环
c
KiIdleLoop();
调用 KiIdleLoop() 进入永不返回的空闲循环。
4. KiIdleLoop --- 空闲循环
ntoskrnl/ke/i386/thrdini.c#L260(file:///d:/reactos/ntoskrnl/ke/i386/thrdini.c#L260)
c
KiIdleLoop(VOID)
{
PKPRCB Prcb = KeGetCurrentPrcb();
PKTHREAD OldThread, NewThread;
while (TRUE)
{
/* 启用中断,执行暂停指令 */
_enable();
YieldProcessor();
YieldProcessor();
_disable();
/* 检查是否有待处理的 DPC、定时器或就绪线程 */
if ((Prcb->DpcData[0].DpcQueueDepth) ||
(Prcb->TimerRequest) ||
(Prcb->DeferredReadyListHead.Next))
{
/* 清除 DPC 软件中断并处理 DPC 列表 */
HalClearSoftwareInterrupt(DISPATCH_LEVEL);
KiRetireDpcList(Prcb);
}
/* 检查是否有新的线程需要调度 */
if (Prcb->NextThread)
{
_enable();
KiSwapProcess(Prcb->NextThread, ...); // 上下文切换
}
}
}
空闲循环的核心逻辑:
┌──────────────────────────────────────────────────┐
│ 空闲循环 (KiIdleLoop) │
│ │
│ 1. 启用中断 (STI) │
│ 2. 暂停 CPU 流水线 (HLT/YieldProcessor) │
│ 3. 禁用中断 (CLI) │
│ 4. 检查 DPC 队列 → 有 → KiRetireDpcList() │
│ 5. 检查就绪线程 → 有 → KiSwapProcess() 切换线程 │
│ 6. 无 → 回到步骤 1 │
└──────────────────────────────────────────────────┘
当系统完全启动后,空闲线程占据 CPU 的所有空闲时间,在所有其他线程阻塞或等待时运行。
5. 关键数据流分析
5.1 从 LoaderBlock 到内核初始化
引导加载程序 (FreeLDR)
│ 填充 LoaderBlock
▼
KeLoaderBlock (全局变量) ← KiSystemStartup 保存
│
├─ KeLoaderBlock->Thread → KiInitialThread (空闲线程)
├─ KeLoaderBlock->KernelStack → P0BootStack (内核栈)
├─ KeLoaderBlock->LoadOptions → 启动选项字符串
├─ KeLoaderBlock->RegistryBase → SYSTEM 配置单元
├─ KeLoaderBlock->MemoryDescriptorListHead → 物理内存映射
├─ KeLoaderBlock->ArcBootDeviceName → 启动设备 ARC 路径
└─ ...
▼
KiInitializeKernel(KeLoaderBlock)
└─ ExpInitializeExecutive(Number, KeLoaderBlock)
├─ HalInitSystem(0, KeLoaderBlock)
├─ ObInitSystem()
├─ SeInitSystem()
├─ PsInitSystem(KeLoaderBlock)
│ └─ 从 KeLoaderBlock 获取初始进程/线程
├─ CmInitSystem()
└─ ...
5.2 处理器控制流切换
BSP 控制流:
KiSystemStartup (BSP)
→ KiSwitchToBootStack (P0BootStack)
→ KiSystemStartupBootStack
→ KiInitializeKernel(InitProcess, InitThread, Prcb, 0, LoaderBlock)
→ ExpInitializeExecutive(0, LoaderBlock) ← Phase 0 初始化
→ PsInitSystem → 创建 Phase1InitializationThread
→ KeSetPriorityThread(InitThread, 0) ← 触发调度
→ KiIdleLoop() ← BSP 进入空闲
Phase1Initialization 线程 (系统线程 #2):
→ Phase1Initialization()
→ HalInitSystem(1, KeLoaderBlock) ← Phase 1
→ IoInitSystem(KeLoaderBlock)
→ KdInitSystem(1, KeLoaderBlock)
→ ... → SMSS → CSRSS → WINLOGON → ...
6. 与 KiSwitchToBootStack 的栈衔接
当 KiSystemStartupBootStack 被 jmp 进入时,栈状态如下:
P0BootStack 顶部 (高地址)
┌─────────────────────────────┐ ← P0BootStack (InitialStack)
│ │
│ ← 空闲空间 │
│ │
├─────────────────────────────┤
│ NPX_FRAME (528 字节) │ ← FX_SAVE_AREA
│ 用于延迟 FPU 上下文切换 │
├─────────────────────────────┤
│ KTRAP_FRAME_ALIGN (4 字节)│
├─────────────────────────────┤
│ KTRAP_FRAME (140 字节) │ ← 陷阱帧
│ 用于构造初始线程上下文 │
├─────────────────────────────┤
│ CR0_FLAGS (4 字节) 0x0E │ ← ESP 在此 (KiSystemStartupBootStack 进入时)
├─────────────────────────────┤
│ ← 空闲栈空间 (向下增长) │
│ KiSystemStartupBootStack │
│ 和 KiInitializeKernel │
│ 使用的栈帧 │
└─────────────────────────────┘
KiInitializeKernel 会在 NPX 帧和陷阱帧空间中构造初始线程的上下文,使得该线程看起来就像已经被 SwapContext 切换出去一样。当 KeSetPriorityThread(InitThread, 0) 触发调度时,KiSwapProcess 切换到 Phase1Initialization 线程,而空闲线程的上下文被保存在 P0BootStack 上的这些帧中。
7. 设计要点总结
7.1 函数简短但至关重要
虽然只有约 35 行代码,但它是引导加载程序世界和操作系统内核世界的最后一道关卡。在其之前,系统只有"引导加载程序设置的硬件状态";在其之后,系统拥有完整的 Windows NT 执行体(对象管理器、安全、进程/线程、配置管理器等)。
7.2 优先级设为 0 的调度技巧
空闲线程的优先级被设为 0(系统最低优先级),这样任何其他线程(最低优先级为 1)都优先于它。ExpInitializeExecutive 创建的 Phase1Initialization 线程优先级为 13,因此当调度发生时,Phase1 线程立即获得 CPU 控制权。当 Phase1 线程因等待 I/O 等操作阻塞时,调度器才将 CPU 交还给空闲线程。
7.3 从物理到虚拟的过渡
此函数运行在分页模式下,运行在内核自己的栈上,使用内核自己的 GDT/IDT。引导加载程序的所有遗留状态(FreeLDR 代码、rosload.exe)不再被使用,可以随时被覆盖。这标志着系统已经完全从引导加载程序过渡到独立的内核执行体。
7.4 KiIdleLoop 的设计
空闲循环不是简单的 while(1);,它在每次循环中执行:
- 启用中断 → 允许硬件中断到达 CPU
YieldProcessor()→ 暂停流水线(HLT/PAUSE),降低功耗- 禁用中断 → 安全检查待处理的 DPC/定时器/线程
- 检查 DPC 队列 → 如果有,调用
KiRetireDpcList处理延迟过程调用 - 检查就绪线程 → 如果有,执行上下文切换
这种设计使 CPU 在空闲时进入低功耗状态 ,但能快速响应中断和 DPC,在需要时立即切换到工作线程。