Reactos 第1章 概述

第1章 概述

本章介绍 Windows 操作系统的发展简史、内存模型中用户空间与系统空间的划分、Windows 内核的整体架构、开源项目 ReactOS 的定位及其代码结构,最后以一张"函数命名词典"总结内核中常见的命名前缀、数据类型与调用约定,为后续章节深入源码分析打好基础。


1.1 Windows 操作系统发展简史

学习 Windows 内核时,读者经常会问一个问题:为何我们研究的是"NT 内核"而不是"9x 内核"?答案藏在 Windows 的两条发展脉络中。

第一条脉络:DOS 依附路线。 1981 年发布的 MS-DOS 是一个 16 位、单任务、运行在实模式下的操作系统。1985 年问世的 Windows 1.0、以及后来的 Windows 3.x(1990 年),本质上是运行在 DOS 之上的图形外壳------内核仍是 DOS,不支持保护模式,不支持抢占式多任务。1995 年的 Windows 95、1998 年的 Windows 98、以及 2000 年的 Windows ME(Millennium Edition)虽然引入了 32 位应用支持,但仍依赖 DOS 引导,内部保留了大量 16 位代码,稳定性差,被统称为"9x 系列"。

第二条脉络:NT 路线。 微软于 1993 年发布 Windows NT 3.1,这是一个完全从零开始设计的 32 位保护模式操作系统内核,与 DOS 毫无关系。NT 内核具有完整的进程/线程模型、虚拟内存管理、基于对象的执行体、分层驱动模型,以及严格的安全机制。随后的演进路径是:Windows NT 4.0(1996,将图形子系统移入内核)→ Windows 2000(NT 5.0)→ Windows XP(NT 5.1)→ Windows Server 2003(NT 5.2)→ Windows Vista/7/8/10/11(NT 6.x/10.x)。现代 Windows 全部基于 NT 内核。

为什么选择 Windows Server 2003 作为 ReactOS 的兼容目标? 在 NT 的演进中,Windows Server 2003 是一个功能完备、文档相对公开、同时源码逆向工程社区对其研究也最成熟的版本。它代表了"经典 NT 架构"------所有核心机制(对象管理器、进程/线程管理、虚拟内存管理器、I/O 管理器、配置管理器、LPC 等)都已定型。因此,理解了 Windows Server 2003 的 NT 内核,就抓住了理解现代 Windows 内核的钥匙。

1.1.1 名词与概念的补充说明

阅读后续章节前,先把本章与未来章节反复出现的重要术语一次性解释清楚。

  • 实模式(Real Mode):8086/8088 时代的工作方式。CPU 直接将段寄存器(segment)左移 4 位加上偏移形成 20 位物理地址,可寻址空间只有 1 MB,且没有内存保护,任何程序都能访问任意地址。MS-DOS 与早期的 Windows 1.0/3.x 都运行在实模式(或 Windows 3.x 的"标准模式"------一种 16 位保护模式)。
  • 保护模式(Protected Mode):80286 引入,80386 大幅完善。CPU 通过段描述符(GDT/LDT)和分页机制(PDE/PTE)提供两层地址翻译:虚拟地址 → 线性地址 → 物理地址;同时引入 ring 0~ring 3 四级特权。Windows NT 起所有现代操作系统内核都要求 CPU 运行在保护模式(64 位模式是保护模式的超集)。
  • 虚拟地址空间:进程看到的"地址"是虚拟地址,必须经过 CPU 的段式+页式翻译才得到物理地址。每个进程都有自己独立的虚拟地址空间映射。32 位 Windows 默认每个进程拥有 4 GB 虚拟地址空间;64 位 Windows 则提供 128 TB 的用户空间 + 128 TB 的系统空间。
  • 分页(Paging):以固定大小(x86 上为 4 KB,或 2 MB/1 GB 的大页)为单位的虚拟→物理映射机制。PDE(Page Directory Entry)和 PTE(Page Table Entry)记录映射关系。页故障(page fault)发生在映射缺失或权限不足时,由内存管理器处理。
  • 协作式多任务(Cooperative Multitasking) :早期 Windows 3.x 的调度模型。操作系统不会强制剥夺 CPU,应用程序必须主动调用 Yield() 或消息循环才释放 CPU。任何程序死循环都会冻结整个系统。
  • 抢占式多任务(Preemptive Multitasking):NT 内核采用的调度模型。操作系统可以在任意时刻强制收回 CPU(通过时钟中断触发调度器),应用程序无法独占 CPU。配合线程优先级,能保证实时性与公平性。
  • NT 架构(NT Architecture):指 1993 年 Windows NT 3.1 起奠定的整套设计:内核态与用户态的清晰分离、对象化的执行体、统一的驱动模型(WDM)、注册表、Win32 子系统等。本书与 ReactOS 所指的"Windows 内核"一律是 NT 内核。

1.1.2 "9x 系列为什么不稳定"------一个常见的误解

很多读者会问:Windows 9x 也支持 32 位应用,为什么不兼容它?技术上 ReactOS 早期确实考虑过兼容 9x,但放弃了。原因有三:

  1. 9x 内核大量保留 16 位代码,与 32 位保护模式的边界模糊。
  2. 9x 依赖 DOS 引导,没有真正的特权级隔离;驱动 crash 整个系统崩溃。
  3. 9x 的关键 API(VMM、IFS、CC)从未公开,且 9x 已于 2000 年随 Windows ME 一起被微软彻底放弃。

这三条原因正是为什么"研究 Windows 内核"必须聚焦 NT 架构------它是唯一既有公开文档、又有开源实现、又被现代 Windows 全部继承的版本。


1.2 用户空间和系统空间

1.2.1 为什么要划分

保护模式下的 x86 CPU 提供 4 GB 的虚拟地址空间(32 位)。操作系统将这 4 GB 划分为两部分:用户空间 (通常为低 2 GB)用于运行应用程序和用户态 DLL,系统空间(通常为高 2 GB)用于运行内核、HAL、驱动程序。这样做有三个好处:

  1. 隔离:应用程序无法直接访问系统空间的数据,避免错误的应用破坏内核。
  2. 保护:CPU 的 ring 3(用户态)与 ring 0(内核态)两级特权提供硬件级保护。
  3. 共享:所有进程共享同一份内核代码和数据结构,节省物理内存。

典型的 32 位 x86 布局如下(ASCII 示意图):

复制代码
  FFFFFFFF ┌──────────────────────────────────┐
           │      系统空间 (ring 0)           │
           │  ntoskrnl.exe · hal.dll · 驱动   │
           │  win32k.sys · 系统页表 · 非换页池 │
  80000000 ├──────────────────────────────────┤
           │      用户空间 (ring 3)            │
           │  应用 EXE · DLL · 用户堆栈        │
           │  每个进程都有独立的用户空间映射    │
  00000000 └──────────────────────────────────┘

默认划分是 2 GB 用户 / 2 GB 系统。使用 /3GB 启动参数可调整为 3 GB 用户 / 1 GB 系统,适合需要大量虚拟内存的应用(如大型数据库)。64 位系统的地址空间则更大(通常用户空间 128 TB,系统空间 128 TB),但"用户态 vs 内核态"的划分思想保持不变。

1.2.2 用户空间里有什么

在 ReactOS 中,用户态代码集中在 dll/win32/ 目录。典型的用户态模块包括:

DLL 作用 ReactOS 目录
ntdll.dll 系统调用的用户态包装、Native API、运行时辅助 dll/win32/ntdll/
kernel32.dll 基础 Win32 API(文件、进程、内存、同步等)的薄封装 dll/win32/kernel32/
user32.dll 窗口与消息 dll/win32/user32/
gdi32.dll 图形绘制 dll/win32/gdi32/
shell32.dll Shell 接口(文件夹、快捷方式) dll/win32/shell32/
mshtml.dll HTML 渲染引擎 dll/win32/mshtml/
ole32.dll / advapi32.dll COM / 注册表与安全 API dll/win32/ole32/dll/win32/advapi32/

1.2.3 系统空间里有什么

系统空间(ring 0)驻留了操作系统真正的核心:

  • ntoskrnl.exe :内核执行体(Executive)与内核层(Kernel)的集合,是内核的主体。在 ReactOS 中位于 ntoskrnl/
  • hal.dll :硬件抽象层,屏蔽主板、芯片组、中断控制器等硬件差异。位于 hal/
  • win32k.sys :窗口与图形子系统的内核态部分(为提升性能,由 ntoskrnl 通过回调挂接)。位于 win32ss/
  • 各类 .sys 驱动:文件系统驱动、磁盘/网络/USB 等设备驱动、过滤驱动等。

1.2.4 如何跨越边界------系统调用

应用程序通过"系统调用"(system call)请求内核提供的服务。ntdll.dll 中的 Nt* 系列函数就是系统调用的用户态包装。其内部通过以下机制从 ring 3 切换到 ring 0:

  • 早期 x86:INT 2Eh 软件中断
  • 现代 x86:sysenter 指令(32 位)
  • x64:syscall 指令

以打开文件为例,调用链路大致是:CreateFileA/W(kernel32.dll)→ NtCreateFile(ntdll.dll,用户态 stub)→ 通过 sysenter/INT 2Eh 进入内核 → NtCreateFile(ntoskrnl 中的真正实现)→ 经由 I/O 管理器派发 IRP 到文件系统驱动。

ntdll.dll 中的 stub 函数大致形式如下(概念性示意,真实代码为汇编):

asm 复制代码
; ntdll.dll 中的 NtCreateFile(概念示意)
NtCreateFile:
    mov  eax, 0x52        ; 系统调用号(NtCreateFile 的服务号)
    mov  edx, offset KiFastSystemCall
    call edx              ; 通过 sysenter 或 INT 2Eh 进入内核
    ret  2Ch              ; 返回用户态

内核中的派发函数根据 eax 中的系统调用号,在系统服务表(System Service Dispatch Table, SSDT)中查找对应的内核实现函数。

1.2.5 关键结构体的概念详解

后续小节中我们会反复使用以下内核结构体。在 1.2 中先把它们定义清楚,方便读者理解后续的系统调用过程。

(1) UNICODE_STRING(内核统一字符串)

内核中几乎所有路径名、对象名都使用 UNICODE_STRING。它的核心思想是"显式携带长度",避免 C 风格字符串遍历计算长度:

c 复制代码
/* sdk/include/psdk/winnt.h (via sdk/include/psdk/winternl.h:77) */
typedef struct _UNICODE_STRING
{
    USHORT Length;         /* 当前使用的字节数(不含结尾 \0) */
    USHORT MaximumLength;  /* 缓冲区总大小(字节),通常 = Buffer 实际字节数 */
    PWSTR  Buffer;         /* 指向宽字符(UTF-16 LE)缓冲区 */
} UNICODE_STRING, *PUNICODE_STRING;
  • Length 单位是字节 ,不是字符数。例如字符串 "ABC"Length = 6(3 字符 × 2 字节)。
  • Buffer 未必以 \0 结尾------长度才是权威,\0 只是"如果方便则加"的习惯。
  • 内核 API RtlInitUnicodeString(Destination, Source) 可用 C 字符串字面量快速初始化一个 UNICODE_STRING
  • 为什么不直接用 C 字符串?C 字符串每次都要遍历计算长度,且用户态传入的字符串可能不带 \0 终止符;使用带长度的结构体能保证内核在严格时间内完成拷贝与检查。

(2) OBJECT_ATTRIBUTES(对象属性)

几乎所有 Nt* 创建/打开对象的函数都要求传入一个 OBJECT_ATTRIBUTES,告诉对象管理器要做什么操作、对象名是什么、是否允许继承等:

c 复制代码
/* sdk/include/psdk/winternl.h:215 */
typedef struct _OBJECT_ATTRIBUTES
{
    ULONG Length;                  /* 本结构体大小(sizeof) */
    HANDLE RootDirectory;         /* 父目录句柄;NULL 表示对象管理器根 */
    PUNICODE_STRING ObjectName;   /* 对象相对名,例如 L"\\Device\\Harddisk0" */
    ULONG Attributes;             /* OBJ_INHERIT、OBJ_PERMANENT、OBJ_EXCLUSIVE 等 */
    PVOID SecurityDescriptor;     /* 安全描述符(SDDL),控制谁能访问 */
    PVOID SecurityQualityOfService; /* QoS(仅服务端使用) */
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;

注意有一个简洁的初始化宏 InitializeObjectAttributes(Macro, Name, Attribs, RootDir, SecDesc),可大幅减少模板代码。

(3) IO_STATUS_BLOCK(I/O 状态块)

每个 I/O 操作都会输出一个 IO_STATUS_BLOCK,告诉调用者操作结果与附加信息:

c 复制代码
/* sdk/include/psdk/winternl.h:247 */
typedef struct _IO_STATUS_BLOCK {
    union {
        NTSTATUS Status;   /* 错误码;0 = STATUS_SUCCESS */
        PVOID    Pointer;  /* 与 Status 共享同一存储,作其它用途 */
    } DUMMYUNIONNAME;
    ULONG_PTR Information;  /* 额外信息(如读写的字节数) */
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

(4) CLIENT_ID(进程/线程 ID)

内核中常用 CLIENT_ID 表示"哪个进程的哪个线程":

c 复制代码
/* sdk/include/xdk/ntkeapi.h 中定义 */
typedef struct _CLIENT_ID
{
    HANDLE UniqueProcess;   /* 进程 ID(PID),在所有进程内唯一 */
    HANDLE UniqueThread;    /* 线程 ID(TID),在同一进程内唯一 */
} CLIENT_ID, *PCLIENT_ID;
  • 注意 Windows 内部并不维护"全局线程 ID"------TID 只需要在同一进程内唯一就足够区分。UniqueThread 实际值是线程的 EThread 指针(或它的哈希),但应用层只能看到逻辑 TID。
  • PsGetCurrentProcessId() / PsGetCurrentThreadId() 是获取当前线程相关 ID 的最常用 API。

1.2.6 Native API 与 Win32 API 的关系

读者可能对 Nt*CreateFile* 这两种 API 感到困惑。两者的关系是:Win32 API 是 Native API 的"包装"

  • Win32 API (如 CreateFileW):由 kernel32.dll、user32.dll、gdi32.dll 等导出。历史悠久、文档完善、稳定不变。
  • Native API (如 NtCreateFile):由 ntdll.dll 导出,对应真正的系统调用。是 Windows 最底层的"操作系统"接口。微软文档中只公开其中一部分,另一部分虽然存在但属于"未文档化"。

调用路径是 Win32 APINative API系统调用内核实现。例如:

复制代码
CreateFileW (kernel32.dll)
   → CreateFileInternal (kernel32.dll 内部,做参数转换)
   → NtCreateFile (ntdll.dll,用户态 stub)
   → sysenter/INT 2Eh
   → NtCreateFile (ntoskrnl 内核实现)
   → I/O 管理器派发 IRP
   → 文件系统驱动

学习 ReactOS 源码时,我们通常直接看 Nt* 这一层------它跳过 Win32 包装的繁琐模板代码,最接近内核真相。


1.3 Windows 内核

本节以"内核 = 一层一层叠加的子系统"为线索,介绍 Windows 内核的分层架构,并在每一小节中给出 ReactOS 的源码目录和一个代表性函数签名,作为读者深入源码的入口。

1.3.1 硬件抽象层(HAL)

作用:屏蔽具体硬件差异。无论底层是 PIC 还是 APIC 中断控制器,无论是否支持 ACPI,内核主体都通过同一套 HAL API 访问硬件。

在 ReactOS 中位于hal/

子目录 功能
halx86/generic/ 通用 x86 实现(包含启动、时钟、DMA 等)
halx86/acpi/ ACPI 支持
halx86/apic/ APIC 中断控制器与 APIC 定时器
halx86/legacy/ 传统 PIC/ISA/PCI 支持
halx86/mp/halx86/smp/ 多处理器支持

情景入口 :内核启动时首先调用 HalInitSystem。该函数分阶段(BootPhase 0、1)完成硬件初始化。

c 复制代码
/* hal/halx86/generic/halinit.c, 第 84 行附近 */
BOOLEAN
NTAPI
HalInitSystem(
    _In_ ULONG BootPhase,
    _In_ PLOADER_PARAMETER_BLOCK LoaderBlock)
{
    PKPRCB Prcb = KeGetCurrentPrcb();
    NTSTATUS Status;

    if (BootPhase == 0)
    {
        /* 保存总线类型,解析命令行,识别固件类型 */
        HalpBusType = LoaderBlock->u.I386.MachineType & 0xFF;
        HalpGetParameters(LoaderBlock);
        ...
    }
    ...
}

可见 HAL 依赖于引导加载器传递的 LOADER_PARAMETER_BLOCK 结构,从中获取硬件信息。

1.3.2 内核层(Kernel,Ke* 前缀)

作用 :提供最底层的机制:线程调度、中断/异常处理、IRQL(中断请求级)管理、自旋锁、DPC(延迟过程调用)、APC(异步过程调用)。内核层不制定策略,只提供机制------策略留给执行体。

在 ReactOS 中位于ntoskrnl/ke/(i386、amd64、arm 等子目录对应不同架构的实现)。

情景入口 1 :内核真正的启动入口是 KiInitializeKernel。它初始化 PRCB(处理器控制块)、空闲线程、调度器、DPC/APC 机制等。

c 复制代码
/* ntoskrnl/ke/i386/kiinit.c, 第 433 行附近 */
VOID
NTAPI
KiInitializeKernel(IN PKPROCESS InitProcess,
                   IN PKTHREAD  InitThread,
                   IN PVOID     IdleStack,
                   IN PKPRCB    Prcb,
                   IN CCHAR     Number,
                   IN PLOADER_PARAMETER_BLOCK LoaderBlock)
{
    ...
    PoInitializePrcb(Prcb);        /* 电源管理初始化 */
    ...                            /* 初始化空闲线程、调度器、IRQL、自旋锁等 */
}

情景入口 2:IRQL(中断请求级)是 Windows 内核最核心的概念之一。x86 下常见的 IRQL 值有:

级别 常量 用途
0 PASSIVE_LEVEL 普通线程执行、可换页
1 APC_LEVEL 异步过程调用
2 DISPATCH_LEVEL 调度器/DPC、不可换页
3...26 设备 IRQL (DIRQL) 设备中断
27 PROFILE_LEVEL 性能剖析中断
28 CLOCK1_LEVEL / CLOCK2_LEVEL 时钟中断
29 IPI_LEVEL 处理器间中断
30 POWER_LEVEL 电源中断
31 HIGH_LEVEL 最高级,屏蔽所有中断

相关函数 KeRaiseIrql / KeLowerIrqlntoskrnl/ke/ 中实现。调度器代码必须提升至 DISPATCH_LEVEL,以防止被线程调度自身打断,同时也不允许访问换页池(因为缺页处理需要更低的 IRQL)。

1.3.3 执行体(Executive)

执行体是内核的"服务层",由多个管理器组成,每个管理器负责一类核心能力。它们全部集中在 ntoskrnl/ 下的子目录中。

情景入口ExpInitializeExecutive 是阅读执行体源码的"总地图"------它依次调用各个子系统的初始化函数。

c 复制代码
/* ntoskrnl/ex/init.c, 第 928 行附近 */
VOID
NTAPI
ExpInitializeExecutive(IN ULONG Cpu,
                       IN PLOADER_PARAMETER_BLOCK LoaderBlock)
{
    ...
    ExInitPoolLookasidePointers();     /* 初始化 lookaside 链表 */
    if (!HalInitSystem(ExpInitializationPhase, LoaderBlock))
        KeBugCheck(HAL_INITIALIZATION_FAILED);
    ...
    /* 后续调用:MiInitMachineDependent(内存管理)、ObInitSystem(对象)、
       PsInitSystem(进程/线程)、IoInitSystem(I/O)、CmInitSystem(配置)、
       SeInitSystem(安全)、CcInitializeCacheManager(缓存)等 */
}

下面列出各执行体子系统:

① 对象管理器(Ob,ntoskrnl/ob/

统一管理所有内核对象:进程、线程、事件、互斥体、文件、注册表键、设备等。每个对象都有一个 OBJECT_HEADER 头部,记录引用计数、名称、安全描述符、类型指针等。对象管理器提供句柄机制,使应用通过句柄(HANDLE)间接访问对象,从而实现安全检查和生命周期管理。

  • 入口函数示例:ObReferenceObjectByHandleObOpenObjectByPointerObInsertObject

  • 深入理解"OBJECT_HEADER":在 Windows 内核中,所有"对象"在内存中都不是仅由一个结构体组成------对象的"体"(如 EPROCESSETHREAD、事件对象等)放在较低的地址,而在它的前面紧挨着放置一个 OBJECT_HEADER 。这就是著名的"头部在低位、对象体在高位"布局。这样设计的好处是:

    • 给定任意对象的"体"指针 p,只需 p - 1(或 CONTAINING_RECORD 反推)即可得到头指针;

    • 类型检查只需要读头里的 TypeIndex 字段,无须遍历链表。

    • ReactOS 在 ntoskrnl/include/internal/ob.h 中通过宏 ObpGetHandleObject 实现"EXHANDLE → OBJECT_HEADER"的转换:

      c 复制代码
      /* ntoskrnl/include/internal/ob.h:91 */
      #define ObpGetHandleObject(x)                           \
          ((POBJECT_HEADER)((ULONG_PTR)x->Object & ~OBJ_HANDLE_ATTRIBUTES))
    • 这种"把元数据紧贴对象体"的布局在 ReactOS 与 Windows 中保持一致;阅读内核源码时一旦看到对 (p-1) 的操作,几乎都是在做头部转换。

  • 句柄表(Handle Table):每个进程都有一个内核句柄表,记录"本进程视角下的句柄 → 内核对象"的映射。HANDLE 在用户态与内核态是不透明值。内核句柄(KERNEL_HANDLE_FLAG)与用户态句柄在同一表中共存,但通过标志位区分。

  • 对象类型(OBJECT_TYPE):每种内核对象(进程、线程、文件、注册表键...)都有一个全局的 OBJECT_TYPE 结构体,记录对象的方法(打开、关闭、删除、解析路径、安全检查等)。ObCreateObjectType 在系统初始化时被各类子系统调用以注册新的对象类型。

② 进程/线程管理器(Ps,ntoskrnl/ps/

负责进程、线程的创建与终止,以及进程与 token 的关联。内部函数 PspCreateProcess 是进程创建的真正实现,被 NtCreateProcessEx 调用。

  • 入口函数示例:PspCreateProcessntoskrnl/ps/process.c 第 347 行)、PsCreateSystemThreadPsLookupProcessByProcessId

③ 内存管理器(Mm,ntoskrnl/mm/ntoskrnl/mm/ARM3/

ReactOS 有两套内存管理实现:老的 mm/ 和新的 mm/ARM3/(ARM3 = "ARM revision 3",也用于 x86)。ARM3 是主要研究对象。功能包括:虚拟地址翻译、页故障(page fault)处理、换页池/非换页池、内存映射文件、section 对象。

  • 入口函数示例:MmAllocatePagesForMdlmm/ARM3/mdlsup.c)、MiDispatchFaultmm/ARM3/pagfault.c

④ I/O 管理器(Io,ntoskrnl/io/iomgr/ntoskrnl/io/pnpmgr/

驱动模型的核心:定义 IRP(I/O Request Packet)、设备栈、设备对象、驱动对象。应用发出的 I/O 请求被包装成 IRP 并沿设备栈一路下传。即插即用(PnP)管理器在 pnpmgr/ 中。

  • 入口函数示例:IoCreateDeviceIoCallDriverIoAllocateIrpIoCompleteRequest

⑤ 缓存管理器(Cc,ntoskrnl/cc/ntoskrnl/cache/

为文件系统提供系统级文件缓存。cc/ 是老实现,cache/(newcc)是新实现。

⑥ 配置管理器(Cm,ntoskrnl/config/

实现注册表:键、值、hive 结构。

⑦ 安全参考监视器(Se,ntoskrnl/se/

SID(安全标识符)、ACL(访问控制列表)、Token(令牌)、权限检查。核心函数如 SeAccessCheckntoskrnl/se/accesschk.c)、SeAssignPrimaryTokenntoskrnl/se/token.c)、SeCreateTokenPrivilege(常量定义在 ntoskrnl/se/priv.c)。

⑧ 本地过程调用(Lpc,ntoskrnl/lpc/

Windows 原生的高性能 IPC 机制,用于进程与子系统之间的通信(例如 Win32 子系统 csrss.exe 与应用进程)。接口如 NtAlpcSendWaitReceivePort

⑨ 运行时库(Rtl,ntoskrnl/rtl/sdk/lib/rtl/

字符串操作、列表操作、异常处理、安全哈希、内存操作等工具函数。内核与用户态(ntdll.dll)都会用到 Rtl。

⑩ 电源管理(Po,ntoskrnl/po/

处理休眠/唤醒与电源状态。核心函数如 PoSetPowerStatePoCallDriver

1.3.4 Win32k 子系统(win32k.sys

图形与窗口子系统的内核态部分位于 win32ss/它不属于执行体 ,而是通过回调机制挂接到 ntoskrnl:当线程第一次调用 GDI/User 函数时,KeUserModeCallback 切换到内核态并调用 win32k 的回调入口。这种设计使窗口绘制操作在内核态完成,避免频繁的用户态/内核态切换。

1.3.5 启动流程简述

以下是一次完整启动的简化流程,也是阅读源码时的路线图:

  1. bootloader (ReactOS 中为 freeldr.sys,位于 boot/)加载 ntoskrnl.exe、hal.dll、启动必需的驱动,并通过 LOADER_PARAMETER_BLOCK 传递启动参数。
  2. 内核入口 KiSystemStartup(架构相关的汇编文件中)设置好栈后调用 KiInitializeKernelntoskrnl/ke/i386/kiinit.c),初始化处理器控制块与空闲线程。
  3. ExpInitializeExecutiventoskrnl/ex/init.c)依次初始化各执行体子系统:对象、进程/线程、内存、I/O、配置、安全、缓存、LPC 等。
  4. I/O 管理器加载其余驱动,完成设备初始化。
  5. 启动会话管理器 smss.exe,再由 smss 启动 winlogon.exe 与服务控制管理器。
  6. winlogon.exe 显示登录界面,用户登录后启动 Explorer(shell)。

1.3.6 中断、异常、系统调用------CPU 视角下的内核入口

读者需要理解,内核并不是被动地为应用"调用"的。在 CPU 层面,进入内核只有三种事件

(1) 中断(Interrupt)

  • 硬件产生的中断请求(IRQ),由中断控制器(PIC 或 APIC)送达 CPU。CPU 在执行完当前指令后,查 IDT(中断描述符表)调用对应中断处理程序。常见的中断有:时钟中断(IRQ 0)、键盘中断、网卡中断、磁盘中断。
  • 在 Windows 中,所有硬件中断都在 DIRQL(设备 IRQL)上处理。中断服务例程(ISR)非常短促,做最紧急的事情后返回;耗时的工作被延迟到 DPC(DISPATCH_LEVEL)处理。
  • 内核的"时钟中断"由 KiClockInterrupt 处理,每次都会触发线程调度检查。

(2) 异常(Exception)

  • CPU 在执行指令时检测到错误,例如除零、缺页、访问违例、断点(int 3)等。Windows 将这些"软件触发的、需要内核介入的事件"统一看作"陷阱(trap)"。
  • 缺页异常是异常中最重要的一种------它由内存管理器(MiDispatchFault)处理,可能把页面从磁盘调入。

(3) 系统调用(System Call)

  • 应用主动请求内核服务(用户态→内核态),CPU 通过 sysenter/INT 2Eh/syscall 切到 ring 0,跳转到内核的 KiFastSystemCallKiSystemService。内核从 eax 读出系统调用号,在 SSDT 中找到对应实现并执行。

下图展示了三种事件如何被 CPU 路由到内核:

复制代码
                ┌────────────────────────────────────────────┐
                │   CPU 在用户态执行应用代码(ring 3)        │
                └──────────────────────┬─────────────────────┘
                                       │
        ┌──────────────┬───────────────┼───────────────┐
        │              │               │               │
   INT n / IRQ     异常(异常码)   sysenter/INT 2Eh    │
        ▼              ▼               ▼               ▼
  KiInterruptDispatch  KiTrap/ExceptionDispatch  KiSystemService
        │              │               │
        ▼              ▼               ▼
    设备 ISR         MiDispatchFault     Nt* 实现
        │        (处理缺页/访问违例)        │
        ▼                              I/O 管理器派发 IRP
  KiDpcInterrupt
  (DPC 队列处理)

理解这张图后,读者就可以回答"我点击了文件菜单,发生了什么?":UI 消息 → 应用代码 → 打开文件 → Win32 API → Native API → sysenter → 内核 NtCreateFile → I/O 管理器派发 IRP → 文件系统驱动 → 磁盘中断(落盘)→ 时钟中断(统计)→ 调度器(运行下一个线程)。整个操作系统就是在这三种事件的循环中工作的


1.4 开源项目 ReactOS 及其代码

1.4.1 什么是 ReactOS

ReactOS("React Operating System")是一个开源项目,致力于实现与 Windows NT/2003 二进制兼容 的操作系统。也就是说,ReactOS 的目标是:在不安装 Microsoft Windows 的情况下,能够直接运行 Windows 应用程序与 Windows 驱动程序。ReactOS 不是 Linux + Wine------它拥有自己独立的内核(ntoskrnl)、自己的 HAL、自己的 Win32 子系统、自己的 bootloader,以及与 Windows 同名同功能的用户态 DLL。

1.4.2 项目简史与开发方式

  • 起源:1996 年以"FreeWin95"起步,目标是实现与 Windows 95 兼容的开源系统。后更名为 ReactOS,目标转向 Windows NT 架构。
  • 干净房间逆向工程 :ReactOS 采用"clean-room reverse engineering"方式开发------开发者通过公开文档(MSDN、Windows Driver Kit、微软公开协议规范)与行为观察,独立写出兼容实现,不复制 Windows 源码。项目定期进行代码审查以确保合规。
  • 活跃状态:目前仍处于活跃开发中,内核和关键用户态模块已具备相当的完整性和稳定性,可在真实硬件与虚拟机上运行。

1.4.3 授权协议

ReactOS 采用 GPLv2 作为主要许可协议(内核与大部分模块),部分组件使用 LGPLBSD 许可。源码树根目录下可见 COPYINGCOPYING.ARMCOPYING.LIBCOPYING3COPYING3.LIB 等文件。

1.4.4 构建系统

  • 构建工具:CMake(根目录 CMakeLists.txt(file:///d:/reactos/CMakeLists.txt))+ Ninja。
  • 交叉工具链:RosBE(ReactOS Build Environment)提供 gcc/g++/windres 等。
  • 构建输出目录output-MinGW-i386/

常用构建命令(在 output-MinGW-i386/ 目录执行):

命令 作用
ninja 完整构建所有模块
ninja mshtml 单独构建 mshtml.dll
ninja ntoskrnl 单独构建内核
ninja bootcd 生成可引导的安装 CD ISO
ninja livecd 生成 Live CD ISO

1.4.5 目录结构全景

下面列出 ReactOS 源码树的主要目录,帮助读者快速定位要读的代码:

目录 作用
boot/ 启动相关:freeldr bootloader、BCD 配置、boot.s 启动汇编
hal/ 硬件抽象层的多架构/多配置实现(x86 单处理器、SMP、ACPI、APIC 等)
ntoskrnl/ 核心内核:执行体各子系统(ex/mm/ob/ps/io/se/cc/cm/lpc/rtl/po)与内核层(ke)、调试器(kd、kdbg)、I/O(iomgr/pnpmgr)、vdm 等
win32ss/ Win32 子系统内核部分(win32k.sys)
dll/win32/ 用户态 Win32 DLL:ntdll、kernel32、user32、gdi32、shell32、mshtml、ole32、advapi32、comctl32、shlwapi 等
dll/cpl/ 控制面板 Applet(桌面、显示、区域设置等)
dll/np/ 网络提供者(如 NFS)
sdk/include/ 公共头文件(供所有模块使用)
sdk/lib/ 基础库:rtl、rossym、uuid 等
sdk/tools/ 构建工具:wpp 预处理器、hpp、bin2c、stubgen 等
base/ 基础命令行工具:cmd.exe 等
media/ 安装媒体资源:.inf 安装脚本、.nls 语言文件;media/doc/ 中还有若干内部技术文档
modules/rostests/ 测试套件(Win32 API 测试、内核模式测试等)
doc/ 项目文档(本文件也在此目录)

1.4.6 干净房间逆向工程(Clean-room Reverse Engineering)深入说明

"干净房间"是 ReactOS 在法律上安全兼容 Windows 行为的开发方式,必须把概念讲清楚,否则容易被误解为"复制 Windows 源码"。

  • 分组:项目分两组开发者------Group A 阅读 Windows 行为、查 MSDN/泄漏的内核调试输出/公开规范,写出"行为规格说明书";Group B 只看规格说明书,独立用 C 语言写出实现。
  • 关键点 :Group B 从未看过 Windows 源码,所以他们的代码是"原创"的,不侵犯版权。最终 ReactOS 的每一行代码都可以追溯到规格说明的某个条目。
  • 与逆向工程的区别:传统的逆向工程(disassemble + decompile)是"看二进制后翻译";干净房间只参考"行为"。这正是为什么 ReactOS 在开源社区是合法且受尊重的。
  • 实践中的权衡:对于未公开的行为,ReactOS 经常要"猜"------根据功能需求、网络协议观察、或者 Wireshark 抓包等。这种"猜"在 bug fix 时可能与 Windows 的实现细节不完全一致,但只要 ABI(应用二进制接口)一致就能让 Windows 应用正常运行。
  • 代码审计流程:ReactOS 维护者对每段新代码审查是否引入了非公开来源的痕迹,对与 Windows 二进制高度相似的代码会被标记"re-implement"。

1.4.7 构建系统的组成与原理

完整理解 ReactOS 的构建需要知道这三个组件:

(1) CMake :项目根目录的 CMakeLists.txt(file:///d:/reactos/CMakeLists.txt) 是入口。CMake 通过 add_subdirectory(...) 递归地处理每个子目录的 CMakeLists.txt(例如 ntoskrnl/CMakeLists.txt),生成 build.ninja 脚本。每个子目录声明自己的源文件、头文件、链接依赖与最终产物。

  • 例:ntoskrnl/CMakeLists.txt 收集 ntoskrnl/ke/*.cntoskrnl/mm/ARM3/*.c 等所有内核源文件,把它们编译成 ntoskrnl.exe
  • dll/win32/mshtml/CMakeLists.txt 把 HTML 渲染引擎的所有 .c.cpp 编译成 mshtml.dll
  • 头文件搜索路径通过 include_directories(${REACTOS_SOURCE_DIR}/sdk/include) 等指令集中设置。

(2) Ninja :执行 build.ninja 脚本的轻量级构建工具,速度比 Make 更快、并行度更好。所有 ninja <target> 命令的 <target> 名称都来自 CMakeLists.txt 中 add_executable / add_library 声明。

  • 常用 target 一览(来自根 CMakeLists.txt 与子 CMakeLists.txt):
    • ninja 完整构建
    • ninja ntoskrnl 内核
    • ninja hal HAL
    • ninja win32ss Win32k.sys
    • ninja mshtml HTML 渲染
    • ninja kernel32 / ninja user32 / ninja gdi32 用户态基础 DLL
    • ninja bootcd / ninja livecd 安装 CD / Live CD
  • 增量构建:Ninja 维护 .ninja_log.ninja_deps 记录上次构建状态;只重编受影响的文件。

(3) RosBE(ReactOS Build Environment):基于 MinGW 的交叉工具链,提供 ReactOS 内核与 DLL 编译所需的编译器、链接器、资源编译器:

  • C:\RosBE\i386\bin\gcc.exe 编译 C 代码
  • C:\RosBE\i386\bin\g++.exe 编译 C++ 代码(如 mshtml 中的 C++ 模块)
  • C:\RosBE\i386\bin\windres.exe 编译 .rc 资源文件
  • C:\RosBE\bin\ninja.exe 构建工具本身

(4) 构建输出结构

复制代码
d:\reactos\output-MinGW-i386\
├── ntoskrnl\        # 内核可执行文件 ntoskrnl.exe
├── hal\             # hal.dll
├── dll\win32\mshtml\ # mshtml.dll
└── ...

每个模块的 .dll / .exe 直接落在与源码相似的相对路径下,调试时一眼可见。

1.4.8 如何开始动手阅读/调试 ReactOS

  • IDE 集成 :使用 Visual Studio Code 或 CLion 配合 CMake 插件打开 d:\reactos 根目录即可被识别为 CMake 项目,可跳转定义、断点调试(需要双机调试环境)。
  • 单步编译 :推荐先编译 ninja win32ssninja mshtml 这种用户态 DLL,速度快,便于熟悉工具链。
  • 内核调试 :使用 ninja livecd 生成 Live CD 镜像后,配合 VirtualBox/VMware 的串口 + Kd 调试器双机调试(Kdbg 默认开启)。也可以使用 ReactOS 官方提供的"kdbg"内置内核调试器。
  • 测试套件modules/rostests/ 包含大量自动化测试。运行 ninja winetests 可以执行 Wine 兼容测试套件,验证 ReactOS 实现是否符合 Windows 行为。

1.5 Windows 内核函数的命名

阅读 ReactOS 源码时,最显著的特征之一是函数和类型的命名极有规律。本节把这些命名规律整理成"词典",读者在遇到陌生函数时可以通过前缀快速判断其归属。

1.5.1 函数前缀对照表

前缀 含义 / 所属子系统 典型函数 源码位置
Nt* Native API / 系统调用(用户态可见的接口) NtCreateFileNtOpenProcessNtReadFile ntdll.dll 的用户态 stub;真正的实现在 ntoskrnl 各子系统
Zw* Native API / 系统调用(内核态直接调用,强制 PreviousMode = KernelMode) ZwCreateFileZwQueryInformationProcess ntoskrnl/ex/zw.S 派发表;转发到对应 Nt*
Ke* Kernel 内核层(调度、IRQL、自旋锁、DPC/APC) KeInitializeThreadKeRaiseIrqlKeAcquireSpinLockKeBugCheckEx ntoskrnl/ke/
Ex* Executive 执行体辅助(池分配、lookaside、事件/互斥体、回调) ExAllocatePoolWithTagExInitializeResourceLiteExQueueWorkItem ntoskrnl/ex/
Mm* 内存管理器(虚拟内存、页表、池、section) MmAllocatePagesForMdlMmMapLockedPagesSpecifyCacheMmCreateSection ntoskrnl/mm/ntoskrnl/mm/ARM3/
Ob* 对象管理器(句柄、引用计数、对象类型) ObReferenceObjectByHandleObOpenObjectByPointerObInsertObject ntoskrnl/ob/
Ps* 进程/线程管理器 PsCreateSystemThreadPsLookupProcessByProcessIdPspCreateProcess ntoskrnl/ps/
Io* I/O 管理器(驱动、IRP、设备栈) IoCreateDeviceIoCallDriverIoAllocateIrpIoCompleteRequest ntoskrnl/io/iomgr/ntoskrnl/io/pnpmgr/
Se* 安全参考监视器(SID、ACL、Token、权限检查) SeAccessCheckSeAssignPrimaryTokenSeSinglePrivilegeCheck ntoskrnl/se/
Cc* 缓存管理器 CcInitializeCacheMapCcCopyReadCcMdlRead ntoskrnl/cc/ntoskrnl/cache/
Cm* 配置管理器(注册表) CmCreateKeyCmEnumerateKeyCmQueryValueKey ntoskrnl/config/
Rtl* 运行时库(字符串、列表、异常、安全哈希、内存) RtlCopyMemoryRtlInitUnicodeStringRtlCompareUnicodeStringRtlZeroMemory ntoskrnl/rtl/sdk/lib/rtl/
Hal* 硬件抽象层 HalInitSystemHalGetInterruptVectorHalAllocateCommonBuffer hal/
Lpc* / NtAlpc* 本地过程调用(高级 LPC) NtAlpcSendWaitReceivePortLpcCreatePort ntoskrnl/lpc/
Po* 电源管理 PoSetPowerStatePoCallDriver ntoskrnl/po/
FsRtl* 文件系统运行时库(Mcb、通配符匹配等) FsRtlLookupMcbEntryFsRtlIsNameInExpression ntoskrnl/fsrtl/
Kd* / Kdbg* 内核调试器 KdPrintKdpPromptKdbpSymbolSearch ntoskrnl/kd/ntoskrnl/kdbg/
Ki*Mi*Pi*Oi* ... 各子系统的内部未导出函数(第二个小写字母 i 暗示 internal) KiDispatchInterruptMiDispatchFaultPspCreateProcess 分布在各子系统目录

1.5.2 Nt*Zw* 的区别

这是内核初学者最常困惑的问题,值得单独说明。

  • 在用户态调用Nt*Zw* 指向同一段用户态 stub(在 ntdll.dll 中),最终都通过系统调用进入内核执行真正的 Nt* 实现。此时二者完全等价。
  • 在内核态调用 :存在重要差别:
    • 直接调用 Nt*:内核内部直接调用 NtCreateFile 等函数时,它会检查当前线程的 PreviousMode 。如果线程的 PreviousMode 是 UserMode(即这个内核执行路径是因为处理了用户态请求而进入内核的),Nt* 将对指针、句柄、字符串等做严格的用户态验证(probe & capture)。如果 PreviousMode 是 KernelMode,则跳过这些验证。
    • 调用 Zw*Zw* 在执行真正的 Nt* 之前,会把当前线程的 PreviousMode 临时设置为 KernelMode,从而跳过所有用户态验证。

因此,驱动程序在内核态主动发起系统调用操作时应当使用 Zw* ,以避免"如果调用者恰好来自用户态请求上下文就被拒绝"的问题。而处理用户态请求时,内核自身的 Nt* 才是真正的实现入口。

ReactOS 的 ntoskrnl/ex/zw.S 中,每个 Zw* 函数大致结构如下(概念示意):

asm 复制代码
; 一个 Zw 函数的典型派发模板
ZwCreateFile:
    mov  eax, [PsGetCurrentThread()]  ; 取当前线程
    mov  ecx, [eax + Tcb.PreviousMode]
    push ecx                          ; 保存旧的 PreviousMode
    mov  byte ptr [eax + Tcb.PreviousMode], KernelMode  ; 强制设为内核态
    call NtCreateFile                 ; 调用真正的实现
    pop  ecx
    mov  [eax + Tcb.PreviousMode], cl ; 恢复 PreviousMode
    ret                               ; 返回

1.5.3 数据类型命名

Windows 内核使用大量 typedef 定义的"匈牙利风格"类型名。常见类型如下:

类型/前缀 含义
HANDLE 对象句柄;用户态与内核态之间的不透明引用
HKEYHWNDHFILE 更具体的句柄(注册表键、窗口、文件);注意 HWND 是用户态 Win32k 的句柄概念
PVOIDPCHARPWSTRPUCHAR 内存指针(void、char、宽字符、unsigned char)
ULONGULONGLONGSIZE_T 无符号整数与大小类型
BOOLEAN 布尔(TRUE/FALSE)
NTSTATUS 状态码;0 = STATUS_SUCCESS,负数为错误,正数为警告/信息
UNICODE_STRING 内核标准字符串(带长度计数的宽字符串);内核中几乎不用 C 风格 NUL 终止字符串
ANSI_STRING ANSI 字符串(同样带长度计数);在内核中较少见
OBJECT_ATTRIBUTES 对象属性结构体;几乎所有 Nt* 创建对象的函数都需要它
IO_STATUS_BLOCK I/O 状态块;存放 I/O 请求的返回值与信息
IRP I/O Request Packet,内核 I/O 请求的基本单位
IO_STACK_LOCATION IRP 的栈位置单元(沿设备栈逐层下传时每一层设置自己的参数)
KIRQL 中断请求级;常见值:PASSIVE_LEVELAPC_LEVELDISPATCH_LEVELDIRQL
KPROCESSOR_MODE 处理器模式:KernelMode / UserMode
1.5.3.1 NTSTATUS 严重性位(Severity Bits)------状态码的"颜色"

NTSTATUS 是一个 32 位有符号整数。最高位(bit 31)是严重性位。判别一个状态码的语义不是看正负,而是看这个位:

c 复制代码
/* sdk/include/xdk/ntdef.template.h:129 */
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;

/* sdk/include/psdk/ntstatus.h:118-121 */
#define STATUS_SEVERITY_SUCCESS       0x0   /* 0b0 */
#define STATUS_SEVERITY_INFORMATIONAL 0x1   /* 0b1 */
#define STATUS_SEVERITY_WARNING       0x2   /* 0b10 */
#define STATUS_SEVERITY_ERROR         0x3   /* 0b11 */

位 31 = 0 表示成功或信息(成功 0x0、信息 0x1),位 31 = 1 表示警告或错误(警告 0x2、错误 0x3)。具体编码:

类型 高 2 位 例子 含义
成功 00 STATUS_SUCCESS = 0x00000000 一切正常
信息 01 STATUS_BUFFER_OVERFLOW = 0x80000005 已完成部分工作,结果需扩展
警告 10 STATUS_BUFFER_TOO_SMALL = 0xC0000023 警告调用者缓冲区不足
错误 11 STATUS_INVALID_PARAMETER = 0xC000000D 失败,参数无效

测试一个 NTSTATUS 应当用 NT_SUCCESS(Status) 宏(等价于 Status >= 0,即检查严重性位);不应直接与 STATUS_SUCCESS 比较。

1.5.3.2 IRQL(中断请求级)详解

KIRQL 是 Windows 内核最重要的"自旋锁隐式计数器"。在任何时刻,每个 CPU 都处于一个 IRQL,它决定:

  • 哪些中断可以打断我(IRQL 高于当前的中断可以打断;等于或低于的将被屏蔽)。
  • 能否被线程调度切换走DISPATCH_LEVEL 及以上禁止线程调度)。
  • 能否访问换页池DISPATCH_LEVEL 及以上不能访问换页池,因为缺页处理需要更低的 IRQL)。

更细致的 IRQL 取值(x86):

级别 名称 触发场景 关键约束
0 PASSIVE_LEVEL 普通线程、可分页代码 可访问分页/非分页池;可被任何中断打断
1 APC_LEVEL APC 派发中 普通线程被屏蔽,但 DPC 仍可发生
2 DISPATCH_LEVEL DPC、线程调度、自旋锁持有 不可访问分页池;线程调度被禁止
3...26 设备 DIRQL 设备 ISR 完全独占 CPU;不可调用任何可能阻塞的 API
27 PROFILE_LEVEL 性能剖析中断 极短暂
28 CLOCK1_LEVEL 时钟中断 1 时钟 ISR;触发线程调度
29 CLOCK2_LEVEL 时钟中断 2(备用) 仅在使用 APIC 时使用
30 IPI_LEVEL 处理器间中断 多 CPU 通信
31 POWER_LEVEL 电源故障中断 极高级别
31 HIGH_LEVEL 完全屏蔽 调试器使用

为什么"调度器要提升到 DISPATCH_LEVEL"? 设想调度器正在修改就绪队列但没做完,此时时钟中断到达,又想触发一次调度,就会破坏链表一致性。提升到 DISPATCH_LEVEL 后,时钟中断(处于 CLOCK1_LEVEL)可以进入,但后续的"DPC/调度"被屏蔽------保证链表修改的原子性。

1.5.3.3 IRP(I/O Request Packet)------内核 I/O 的"总控"

IRP 是 I/O 管理器派发到驱动程序的"任务包"。任何文件、网络、磁盘操作最终都被包装为 IRP 沿设备栈下传:

c 复制代码
/* IRP 的关键字段(简化) */
typedef struct _IRP {
    CSHORT Type;
    USHORT Size;
    PMDL MdlAddress;            /* 描述缓冲区的 MDL(内存描述符列表) */
    ULONG Flags;
    union {
        struct _IRP *MasterIrp; /* 用于链式异步 I/O */
        ...
    } AssociatedIrp;
    LIST_ENTRY ThreadListEntry;/* 关联到发起线程 */
    IO_STATUS_BLOCK IoStatus;   /* I/O 状态(Status、Information) */
    KPROCESSOR_MODE RequestorMode; /* 请求来自用户态还是内核态 */
    BOOLEAN PendingReturned;
    BOOLEAN Cancel;
    KIRQL CancelIrql;
    PDRIVER_CANCEL CancelRoutine;
    PVOID UserBuffer;
    ...
    IO_STACK_LOCATION Tail.Overlay.CurrentStackLocation; /* 当前的栈位置 */
    ...
} IRP, *PIRP;

每个设备栈中的设备对象(DEVICE_OBJECT)都会读写一个 IO_STACK_LOCATION------这是设备栈的"调用栈" ,每一层驱动从自己的栈位置读取参数,处理完后通过 IoCallDriver 把 IRP 传到下一层。

IRP 的主要 MajorFunctionIRP_MJ_*):

MajorFunction 含义 典型调用
IRP_MJ_CREATE 打开文件/设备 CreateFile
IRP_MJ_CLOSE 关闭 CloseHandle
IRP_MJ_READ ReadFile
IRP_MJ_WRITE WriteFile
IRP_MJ_DEVICE_CONTROL 设备控制 DeviceIoControl
IRP_MJ_PNP 即插即用 内部使用
IRP_MJ_POWER 电源管理 内部使用

1.5.4 调用约定

真实约定 用途
NTAPI __stdcall 大多数 Native API;被调用方清栈;参数从右向左入栈
CALLBACK __stdcall 回调函数
FASTCALL __fastcall 将前两个参数放在寄存器(ECX/EDX)以加速调用;ReactOS 内部部分性能敏感代码使用
CDECL __cdecl C 默认约定,调用方清栈;主要用于可变参数函数,如 DbgPrint
1.5.4.1 调用约定的具体含义

调用约定决定了:

  1. 参数传递顺序(左到右 / 右到左入栈)
  2. 谁清栈(调用方 / 被调用方)
  3. 前几个参数是否走寄存器

NTAPI__stdcall)为例,调用 NTSTATUS NTAPI NtCreateFile(PHANDLE a, ACCESS_MASK b, ...) 时,编译后的 32 位 x86 代码如下(概念示意):

asm 复制代码
; 调用方压栈(从右向左)
push arg10
push arg9
push arg8
...
push arg2  ; ACCESS_MASK b
push arg1  ; PHANDLE a
call NtCreateFile  ; 调用,ret 16(被调用方清理 40 字节 = 10 参数 × 4 字节)

被调用方在反汇编后第一行是 mov edi, edi(32 位热补丁占位)或者直接进入函数体,最后用 ret N 形式返回,由被调用方调整栈 。这与 cdecl 的"调用方自己 add esp, N"形成对比。

__fastcall 在 x86 下,前两个参数会被放在 ECXEDX 中。如果只有 2 个以下的参数,可避免全部压栈,从而让"短小高频"的函数(如 KeAcquireSpinLock)更快。

1.5.4.2 函数调用时 IRQL 的隐含规则

调用约定之外,IRQL 是内核态函数的另一个"调用约束" 。Sal 注释 _IRQL_requires_max_(PASSIVE_LEVEL) 表示函数只能在 PASSIVE_LEVEL 调用:

c 复制代码
/* sdk/include/xdk/zwfuncs.h:15 */
_IRQL_requires_max_(PASSIVE_LEVEL)
NTSYSCALLAPI
NTSTATUS
NtCreateFile(
    ...
);

这表示:调用方必须处于 PASSIVE_LEVEL;如果调用方处于 DISPATCH_LEVEL(如 DPC),会破坏可分页内存访问与线程调度。SAL(Source-Code Annotation Language)注释被静态分析工具(如 PREfast)用于检查。常见约束:

  • _IRQL_requires_max_(PASSIVE_LEVEL):只能在 PASSIVE_LEVEL 调用
  • _IRQL_requires_min_(DISPATCH_LEVEL):只能在 DISPATCH_LEVEL 及以上调用(如 KeAcquireSpinLock
  • _IRQL_raises_(DISPATCH_LEVEL):调用后 IRQL 提升到 DISPATCH_LEVEL
  • _IRQL_saves_global_(OldIrql, Param):保存并修改 IRQL

这些注释在内核代码中随处可见(ReactOS 同样使用),是阅读源码时判断"这个函数能不能在这里调用"的关键线索。

1.5.5 标签与命名空间思想

内核函数两字母前缀的本质是命名空间 :看到 Mm* 就知道是内存管理,看到 Ps* 就知道是进程管理------无需额外文档就能快速定位。在大型代码库中,这种约定极大降低了理解成本。

1.5.5.1 Pool Tag(池标签)------ 4 字节的所有权标记

类似地,池分配函数 ExAllocatePoolWithTag(PoolType, NumberOfBytes, Tag) 的第三个参数 Tag 是一个 4 字节的 ASCII 标签(注意它在内存中以小端序 存储,所以 'Proc' 写成 'corP'):

c 复制代码
PVOID Buffer = ExAllocatePoolWithTag(NonPagedPool, 256, ' oiM');  /* 'Mio ' = 内存 I/O */

崩溃时 Windbg 看到某块内存的 Tag = 'Mio ',就能立刻判断它由内存管理器的 I/O 子模块分配。ReactOS 内部使用 4 个 ASCII 字符标签,常见例子:

标签(代码中写) 实际内容 所属子系统
' tSbO' OB S (小端) 对象管理器(Ob
' leT' Tel (小端) Telnet/IPC 客户端
'galF' Flag 标志位对象
'rPCT' TCPr TCP 模块
'diM' Mid 内存中间层
' pme' emp 池实现

小贴士 :在 C 源文件中写 'Proc'(4 字节字符常量),编译器会按当前平台的字节序组织。在 x86/x64(都是 little-endian)上,内存中实际字节序是 'c', 'o', 'r', 'P',Windbg 显示时会还原为 "Proc"。这个反直觉的细节是初学者最常踩的坑。

1.5.5.2 内部函数 "Ki/Mi/Pi/Oi/Ci" 前缀的含义

读者在内核代码中还会频繁看到"小写 i"开头的内部函数:

前缀 含义
Ki*(Kernel Internal) 内核层内部,未导出
Mi*(Memory Internal) 内存管理器内部
Pi*(PnP Internal) PnP 管理器内部
Ci*(Config Internal) 配置管理器(注册表)内部
Obp*(Object private) 对象管理器私有
Exp*(Executive private) 执行体私有
Psp*(Process private) 进程/线程管理器私有
Io* / Iop* I/O 管理器,公开/私有两套

这些函数往往不在 .h 中声明,只在 .c 文件内部使用,承担具体子系统的"非策略"细节。

1.5.6 快速上手建议

阅读 ReactOS 源码时,建议按以下顺序建立"导航感":

  1. 找到入口ntoskrnl/ke/i386/kiinit.cKiInitializeKernelntoskrnl/ex/init.cExpInitializeExecutive 是内核启动的两个主轴。
  2. 按前缀索引:遇到陌生函数,查 1.5.1 的前缀表,判断属于哪个子系统,然后到对应子目录找实现。
  3. "总地图"视角ExpInitializeExecutive 中按顺序调用的各初始化函数列表,就是执行体的"总地图"。
  4. 阅读头文件ntoskrnl/include/internal/*.h(尤其是 ke.hmm.hob.hps.hio.hse.h)集中定义了各子系统的内部 API 与数据结构,经常比实现文件更容易理解。

1.6 本章小结

本章是为后续源码分析做铺垫的"总览章"。读者读完后应当能在脑中形成以下五个"心智模型":

  1. 历史模型:Windows 操作系统有两条路线------DOS 依附路线(9x 系列,已淘汰)与 NT 路线(现代 Windows 全部基于此)。ReactOS 兼容 Windows Server 2003 的 NT 内核。
  2. 地址空间模型 :x86 4 GB 虚拟地址被划分为用户空间(ring 3)和系统空间(ring 0)。CPU 通过 sysenter/INT 2Eh 切到内核。ntdll.dll 的 Nt* 是用户态 stub;真正的实现位于 ntoskrnl。
  3. 内核分层模型:Windows 内核 = HAL + 内核层(Ke) + 执行体(Ob/Ps/Mm/Io/Se/Cc/Cm/Lpc/Rtl/Po 等) + Win32k。HAL 屏蔽硬件,内核层提供机制,执行体制定策略。
  4. 项目模型 :ReactOS 是一棵源码树,根目录的 ntoskrnl/hal/win32ss/dll/win32/sdk/ 等是研究重点;CMake + Ninja + RosBE 是构建工具链。
  5. 命名模型:函数前缀(Nt/Zw/Ke/Ex/Mm/Ob/Ps/Io/Se/Cc/Cm/Rtl/Hal/...)= 命名空间;数据类型 NTSTATUS/HANDLE/UNICODE_STRING/IRP/IRQL = 内核"词汇";NTAPI/NTSTATUS/Pool Tag = 命名习惯。

下一章将正式进入"对象管理器"的源码情景分析------这是 NT 内核中一切资源的"管理中枢",理解了它就理解了 Windows 内核的"对象"思想。

相关推荐
.千余1 小时前
【C++】C++继承入门(下):友元、静态成员与菱形继承的底层逻辑
开发语言·c++·笔记·学习·其他
namexingyun1 小时前
拆解Fable 5三重安全护栏:模型路由、蒸馏防护与生物安全分类器的技术原理 - 微元算力(weytoken)
java·人工智能·python·安全·架构·ai编程
小短腿的代码世界1 小时前
行情快照与增量更新引擎:Qt在高频交易数据分发中的核心架构——你的行情推送为什么延迟了500ms?
开发语言·qt·架构
上海云盾第一敬业销售1 小时前
高效阻止网站攻击的WAF防护架构解析
web安全·架构·ddos
初中就开始混世的大魔王1 小时前
6 Fast DDS-传输层
开发语言·c++·中间件·信息与通信
啊森要自信2 小时前
【GUI自动化测试】控件、鼠标键盘操作与多场景自动化
c语言·开发语言·python·adb·ipython
花北城2 小时前
【C#】ABP框架服务端开发
开发语言·c#·abp
意图共鸣2 小时前
意图共鸣科技《AI记忆链商业化白皮书3.0》假设场景解析:从母亲到消防员,专属AI如何重塑记忆与传承
人工智能·科技·架构
FPGA小徐2 小时前
Xilinx zynq-7000系列FPGA移植Linux操作系统详细教程
fpga开发·架构