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 的来源

InitialStackLoaderBlock->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) 压入栈顶。这个值在后续 KiInitializeKernelKeInitSystem 中被读取和使用。

这三个标志位的含义:

标志 含义 用途
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_TSCR0_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 的衔接

jmpKiSystemStartupBootStack 时,栈的状态为:

复制代码
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。通过将初始标志压入栈,内核初始化代码可以正确地参与到这个机制中。

相关推荐
caimouse3 小时前
KiSystemStartupBootStack 函数分析
开发语言·reactos
caimouse3 天前
ReactOS Development Notes
reactos
爱学习的大牛1232 年前
通过VirtualBox虚拟机安装和调试编译好的 ReactOS
reactos·windows内核学习
zhyjhacker2 年前
ReactOS 4.2 OBJECT_TYPE_INITIALIZERj结构体的实现
c++·windows·reactos
zhyjhacker2 年前
3.1.1ReactOS系统中搜索给定长度的空间地址区间函数的实现
linux·c++·嵌入式硬件·reactos
zhyjhacker2 年前
memset()函数的实现
c++·windows·内核·reactos
zhyjhacker2 年前
3.1.1 ReactOS系统中二叉树创建一个MEMORY_AREA节点
java·前端·c++·windows·算法·内核·reactos
zhyjhacker2 年前
2.6.ReactOS系统中从内核中发起系统调用
c++·windows·算法·内核·reactos
zhyjhacker2 年前
2.4.ReactOS系统提升IRQL级别KfRaiseIrql 函数
c++·windows·reactos