KiSwitchToBootStack 函数分析

KiSwitchToBootStack 函数分析

1. 概述

KiSwitchToBootStack 是一个内联函数,负责将当前 CPU 的栈从引导加载程序 (FreeLDR) 使用的栈切换到内核自己的初始化栈,并在新栈上预留陷阱帧和 NPX 帧空间,然后跳转到 KiSystemStartupBootStack 执行最终的内核引导初始化。这个函数从不返回。

文件位置: ntoskrnl/include/internal/i386/ke.h#L909-L948(file:///d:/reactos/ntoskrnl/include/internal/i386/ke.h#L909)

c 复制代码
CODE_SEG("INIT")
FORCEINLINE
DECLSPEC_NORETURN
VOID
KiSwitchToBootStack(IN ULONG_PTR InitialStack)

函数属性

属性 含义
CODE_SEG("INIT") 代码位于 INIT 段,初始化完成后可被丢弃/覆盖
FORCEINLINE 强制内联(无论编译器的优化级别如何),因为此函数直接操作栈指针
DECLSPEC_NORETURN 告知编译器此函数不会返回(跳转到 KiSystemStartupBootStack)
IN ULONG_PTR InitialStack 新栈的栈顶指针(传入时已 & ~3 对齐清除低 2 位)

2. 调用上下文

2.1 调用位置

在 kiinit.c#L1005(file:///d:/reactos/ntoskrnl/ke/i386/kiinit.c#L1005) 由 KiSystemStartup 调用:

c 复制代码
/* 提升 IRQL 到 HIGH_LEVEL,确保后续栈切换操作不会被中断 */
KeRaiseIrql(HIGH_LEVEL, &DummyIrql);

KiSwitchToBootStack(InitialStack & ~3);

2.2 调用链

复制代码
KiSystemStartup(LoaderBlock)
  → KiSwitchToBootStack(InitialStack & ~3)   [ke.h:916]  ←★ 本文分析
      → jmp KiSystemStartupBootStack          [kiinit.c:685]
          → KiInitializeKernel()              [kiinit.c:433]
              → ExpInitializeExecutive()      [kiinit.c:593]
              → KeSetPriorityThread(..., 0)   → 触发调度器切换
          → KiIdleLoop()                      [永不返回]

2.3 参数 InitialStack 的来源

InitialStack 从 LoaderBlock->KernelStack 获取。在 BSP (Boot Strap Processor) 上,KiSystemStartup 的第 763 行将其设为 P0BootStack:

c 复制代码
LoaderBlock->KernelStack = (ULONG_PTR)P0BootStack;

而 P0BootStack 定义在 kiinit.c#L19-L22(file:///d:/reactos/ntoskrnl/ke/i386/kiinit.c#L19):

c 复制代码
UCHAR DECLSPEC_ALIGN(PAGE_SIZE) P0BootStackData[KERNEL_STACK_SIZE] = {0};
ULONG_PTR P0BootStack = (ULONG_PTR)&P0BootStackData[KERNEL_STACK_SIZE];
// P0BootStack 指向数组末尾 → 栈向下增长

3. 关键常量

常量 值 说明
NPX_FRAME_LENGTH 0x210 (528) NPX (Numerical Processor eXtension) 帧长度,等于 sizeof(FX_SAVE_AREA),用于保存 FPU/MMX/SSE 寄存器状态
KTRAP_FRAME_ALIGN 0x04 (4) 陷阱帧对齐填充
KTRAP_FRAME_LENGTH 0x8C (140) 陷阱帧 (KTRAP_FRAME) 结构体大小
总和 0x2A0 (672) 栈上预留的总空间
CR0_EM 0x04 CR0.Emulation --- x87 FPU 仿真标志
CR0_TS 0x08 CR0.Task Switched --- 任务切换标志
CR0_MP 0x02 CR0.Monitor Coprocessor --- 监控协处理器标志
OR 值 0x0E (14) 入栈的 CR0 标志组合值

4. 执行流程详解

4.1 函数原型和前置声明

c 复制代码
CODE_SEG("INIT") DECLSPEC_NORETURN VOID NTAPI KiSystemStartupBootStack(VOID);

函数体内首先声明了 KiSystemStartupBootStack 的原型,带有 CODE_SEG("INIT") 属性。此声明的作用是让内联汇编中的 jmp 指令可以正确引用该符号。

4.2 GCC 内联汇编版本

c 复制代码
__asm__
(
    "movl %0, %%esp\n\t"                              // 步骤 1
    "subl %1, %%esp\n\t"                              // 步骤 2
    "pushl %2\n\t"                                    // 步骤 3
    "jmp _KiSystemStartupBootStack@0"                 // 步骤 4
    :
    : "c"(InitialStack),                               // %0 = InitialStack (ECX 寄存器)
      "i"(NPX_FRAME_LENGTH + KTRAP_FRAME_ALIGN + KTRAP_FRAME_LENGTH), // %1 = 立即数 0x2A0
      "i"(CR0_EM | CR0_TS | CR0_MP),                 // %2 = 立即数 0x0E
      "p"(KiSystemStartupBootStack)                   // %3 = 函数地址 (用于跳转)
    : "%esp"                                           // 告知编译器 ESP 被修改
);
步骤 1: movl %0, %%esp --- 切换栈指针
复制代码
ESP = InitialStack

将栈指针切换到内核的启动栈 P0BootStack。这是不可逆操作 --- 从此之后,局部变量、返回地址等都在新栈上。

为什么不在栈上留返回地址? 因为下一步不是 call 而是 jmp(没有 call 指令的隐式 push return_address),所以栈顶从 InitialStack 开始就是全新的空栈。

步骤 2: subl %1, %%esp --- 预留帧空间
复制代码
ESP = InitialStack - 0x2A0
ESP = InitialStack - (NPX_FRAME_LENGTH + KTRAP_FRAME_ALIGN + KTRAP_FRAME_LENGTH)
ESP = InitialStack - (528 + 4 + 140)
ESP = InitialStack - 672

在新栈上向下移动栈指针,预留出 672 字节的空间。这个空间将用于存放:

复制代码
新栈布局 (从高地址到低地址):
┌─────────────────────────────┐ ← InitialStack (原 ESP)
│                             │
│   NPX_FRAME (528 bytes)     │ ← NPX/FPU 保存区
│   FX_SAVE_AREA              │   (FNSAVE/FXSAVE 指令用)
│                             │
├─────────────────────────────┤ ← InitialStack - 0x210
│   KTRAP_FRAME_ALIGN (4B)    │ ← 对齐填充
├─────────────────────────────┤ ← InitialStack - 0x214
│                             │
│   KTRAP_FRAME (140 bytes)   │ ← 陷阱帧 (异常/中断上下文)
│   _KTRAP_FRAME              │
│                             │
├─────────────────────────────┤ ← InitialStack - 0x2A0 = ESP (预留后)
│   [已压入 CR0 标志] (4B)    │ ← pushl 之后
├─────────────────────────────┤
│                             │
│   空闲栈空间 (向下增长)      │
└─────────────────────────────┘

NPX_FRAME 用于在初始线程的上下文切换时保存 FPU 状态。KiInitializeKernel 初始化空闲线程时,会在栈上构造陷阱帧,使该线程看起来就像刚刚执行完一条 iret 指令并获得 CPU 控制权。

KTRAP_FRAME 是异常/中断发生时 CPU 自动或内核代码手动保存的寄存器上下文。在引导初始化阶段,内核先在栈上构造一个陷阱帧,然后通过 iret / sysexit 等方式切换到初始线程。

步骤 3: pushl %2 --- 保存 CR0 标志
复制代码
[ESP] = 0x0E  (CR0_EM | CR0_TS | CR0_MP)
ESP = ESP - 4

将 CR0_EM | CR0_TS | CR0_MP (= 0x0E) 压入栈顶。这个值在后续 KiInitializeKernel → KeInitSystem 中被读取和使用。

这三个标志位的含义:

标志 位 含义 用途
CR0_EM 位 1 (0x02) Monitor Coprocessor 控制 WAIT/FWAIT 指令在任务切换时的行为
CR0_TS 位 3 (0x08) Task Switched 每次任务切换时由硬件置位,表示 FPU 上下文可能已变更
CR0_MP 位 4 (0x10) Emulation 置位时表示没有 x87 FPU,所有 FPU 指令触发 #NM 异常

为什么入栈这三个标志位?

KiInitializeKernel 内部的 KeInitSystem() 会读取 CR0 的这些标志位来判断当前的 FPU 状态。但如果直接读 CR0 寄存器,得到的是已经设置好的最终值(FreeLDR 和 KiVerifyCpuFeatures 已经设置过 CR0)。压入栈的这个值实际上是一个初始标记值,表示"尚未初始化 FPU 状态",这样初始化代码就可以正确地检测到需要初始化 FPU 上下文。

具体来说,当初始线程第一次切换到用户模式时,内核的 KiInitSystem() 看到 CR0_TS 和 CR0_EM 被置位,就知道需要设置 FPU 状态并清除这些位,然后再继续执行。

步骤 4: jmp _KiSystemStartupBootStack@0 --- 跳转到引导栈
复制代码
EIP = KiSystemStartupBootStack

这是一个尾调用优化 --- 用 jmp 代替 call:

  • 不会压入返回地址
  • 不会改变栈的内容
  • KiSystemStartupBootStack 会直接在当前准备好的栈上执行
  • 永不返回

4.3 MSVC 内联汇编版本

c 复制代码
__asm
{
    mov esp, InitialStack                       // 步骤 1: 切换栈
    sub esp, (NPX_FRAME_LENGTH +               // 步骤 2: 预留帧空间
              KTRAP_FRAME_ALIGN +
              KTRAP_FRAME_LENGTH)
    push (CR0_EM | CR0_TS | CR0_MP)            // 步骤 3: 保存 CR0 标志
    jmp KiSystemStartupBootStack                // 步骤 4: 跳转
}

MSVC 版本的功能与 GCC 版本完全相同,只是语法不同:

  • MSVC 使用 __asm { ... } 块而非 __asm__("...")
  • MSVC 可以直接引用局部变量 InitialStack
  • 不需要像 GCC 一样使用寄存器约束 ("c" 约束)

4.4 UNREACHABLE

c 复制代码
UNREACHABLE;

在两种汇编版本之后放置 UNREACHABLE 宏:

  • 告知编译器控制流永远不会到达这里(不会生成 ret 指令的返回代码)
  • 如果因为某些原因(如目标函数损坏)执行到达此处,会触发 CPU 异常或编译器特定的 UB 陷阱

5. 栈布局详细图解

切换前 (在 KiSystemStartup 的栈上)

复制代码
KiSystemStartup 的栈帧:
┌─────────────────────────────┐
│  KiSystemStartup 局部变量    │
│  (DummyIrql, Pcr, Tss, ...) │
├─────────────────────────────┤ ← ESP (FreeLDR 的栈)
│    返回地址到 FreeLDR        │
├─────────────────────────────┤
│  FreeLDR 的栈帧              │
└─────────────────────────────┘

切换后 (在 P0BootStack 上)

复制代码
P0BootStack 内存布局 (高 → 低):
┌─────────────────────────────┐ ← P0BootStack = InitialStack
│                             │   (P0BootStackData 数组末尾)
│                             │
│   ← 空闲空间 (可被使用)      │
│                             │
├─────────────────────────────┤ ← InitialStack - 0x210
│   NPX_FRAME                 │
│   (FX_SAVE_AREA, 528 字节)  │
│   --- 用于保存 FPU/MMX/SSE    │
│     寄存器状态的区域          │
├─────────────────────────────┤ ← InitialStack - 0x214
│   KTRAP_FRAME_ALIGN (4 字节)│
│   --- 对齐填充到 8 字节边界    │
├─────────────────────────────┤ ← InitialStack - 0x218
│   KTRAP_FRAME               │
│   (140 字节)                │
│   --- 陷阱帧                   │
│   --- 包含: EBP, EBX, ESI,    │
│     EDI, EAX, ECX, EDX,    │
│     DS, ES, FS, GS, SegCs, │
│     SegSs, EFlags, ESP,    │
│     ErrCode, TrapFrame      │
├─────────────────────────────┤ ← InitialStack - 0x2A0
│   CR0 Flags (0x0E)          │ ← ESP 在此 (pushl 之后)
│   --- CR0_MP | CR0_EM |      │
│     CR0_TS 的组合位          │
├─────────────────────────────┤ ← 新 ESP (jmp 之后)
│   ← 空闲栈空间               │
│      (向下增长)              │
└─────────────────────────────┘

6. 与 KiSystemStartupBootStack 的衔接

当 jmp 到 KiSystemStartupBootStack 时,栈的状态为:

复制代码
ESP → [CR0_Flags = 0x0E]     (4 字节)
       [NPX_Frame 空间]       (528 字节)
       [对齐填充]             (4 字节)
       [KTRAP_Frame 空间]     (140 字节)
       ← InitialStack 顶部

然后 kiinit.c#L685-L710(file:///d:/reactos/ntoskrnl/ke/i386/kiinit.c#L685) 的 KiSystemStartupBootStack 执行:

c 复制代码
KiSystemStartupBootStack(VOID)
{
    // 调用 KiInitializeKernel,它使用当前栈帧
    KiInitializeKernel(&KiInitialProcess.Pcb,
                       (PKTHREAD)KeLoaderBlock->Thread,
                       (PVOID)(KeLoaderBlock->KernelStack & ~3),
                       (PKPRCB)__readfsdword(KPCR_PRCB),
                       KeNumberProcessors - 1,
                       KeLoaderBlock);

    // ...

    // 进入空闲循环
    KiIdleLoop();
}

KiInitializeKernel 内部 (特别是 KeInitSystem) 会使用栈上的 NPX 帧和陷阱帧来初始化空闲线程的初始上下文。当设置空闲线程优先级为 0 时,调度器执行上下文切换,从栈上的陷阱帧恢复寄存器并用 iret 切换到初始线程。


7. 设计要点总结

7.1 为什么用 jmp 而不是 call

用 jmp 而不是 call 是因为这是一个栈切换操作 。如果用 call,CPU 会将返回地址压入当前栈 (也就是旧的 FreeLDR 栈),然后代码才切换 ESP 到新栈,这样返回地址就留在了旧栈上。而 KiSystemStartupBootStack 是永远不会返回的,所以不需要保存返回地址。jmp 直接跳转,不在栈上产生任何额外数据。

7.2 FORCEINLINE 的必要性

此函数被标记为 FORCEINLINE,意味着它在 KiSystemStartup 中被内联展开而不是调用。这是因为:

  • 该函数直接修改 ESP 寄存器,如果作为被调函数执行,编译器会为函数入口生成 push ebp; mov ebp, esp,函数返回时会 leave; ret,这些操作都在旧栈上进行,干扰栈切换
  • 强制内联后,汇编代码直接嵌入到 KiSystemStartup 的函数体中,栈切换发生时直接跳转到新的执行路径

7.3 完整的栈过渡流程

复制代码
引导加载程序 (FreeLDR) 的栈
    ↓  KiSystemStartup 被调用
KiSystemStartup 使用 FreeLDR 的栈执行初始化
    ↓  InitialStack = P0BootStack
KiSwitchToBootStack 切换 ESP 到 P0BootStack
    ↓  jmp KiSystemStartupBootStack
在新栈上继续执行
    ↓  KiInitializeKernel
    ↓  KeSetPriorityThread(..., 0) → 调度器切换
P0BootStack 被保存为内核栈
    ↓  初始线程获得 CPU 控制权
系统继续初始化...

7.4 栈上的 CR0 标志位的作用

栈上保存的 CR0_EM | CR0_TS | CR0_MP 组合标志位是 x86 传统上用于延迟 FPU 上下文切换的机制。CR0_TS 在每次任务切换时被 CPU 自动置位,当下一次 FPU 指令被执行时触发 #NM (Device Not Available, 中断 7) 异常,内核在异常处理程序中保存前一个任务的 FPU 状态、恢复当前任务的 FPU 状态,然后清除 CR0_TS。通过将初始标志压入栈,内核初始化代码可以正确地参与到这个机制中。

相关推荐
caimouse1 个月前
ReactOS 窗口系统分析(22):鼠标 — mouse.c
c语言·开发语言·reactos
caimouse1 个月前
ReactOS 窗口系统分析(14):计时器/属性/加速键/热键 — timer.c + prop.c + accelerator.c + hotkey.c
c语言·开发语言·reactos
caimouse1 个月前
ReactOS 窗口系统分析(24):输入法 — ime.c
c语言·reactos
caimouse1 个月前
ReactOS 窗口系统分析(13):滚动条 — scrollbar.c + scrollex.c
c语言·开发语言·reactos
caimouse2 个月前
ReactOS 图形系统分析(36):位块传输 — bitblt.c
c语言·reactos
caimouse2 个月前
ReactOS 图形系统分析(37):路径子系统 — path.c
c语言·reactos
caimouse2 个月前
ReactOS 图形系统分析(41):画刷子系统 — brush.cpp
c++·reactos
caimouse2 个月前
ReactOS 图形系统分析(31):内核 GDI — ntgdi
性能优化·reactos
caimouse2 个月前
ReactOS 图形系统分析(33):字体子系统 — font.c
c语言·开发语言·reactos
caimouse2 个月前
ReactOS 图形系统分析(42):形状填充 — fillshap.c
c语言·reactos