第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,但放弃了。原因有三:
- 9x 内核大量保留 16 位代码,与 32 位保护模式的边界模糊。
- 9x 依赖 DOS 引导,没有真正的特权级隔离;驱动 crash 整个系统崩溃。
- 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、驱动程序。这样做有三个好处:
- 隔离:应用程序无法直接访问系统空间的数据,避免错误的应用破坏内核。
- 保护:CPU 的 ring 3(用户态)与 ring 0(内核态)两级特权提供硬件级保护。
- 共享:所有进程共享同一份内核代码和数据结构,节省物理内存。
典型的 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 API → Native 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 / KeLowerIrql 在 ntoskrnl/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)间接访问对象,从而实现安全检查和生命周期管理。
-
入口函数示例:
ObReferenceObjectByHandle、ObOpenObjectByPointer、ObInsertObject -
深入理解"OBJECT_HEADER":在 Windows 内核中,所有"对象"在内存中都不是仅由一个结构体组成------对象的"体"(如
EPROCESS、ETHREAD、事件对象等)放在较低的地址,而在它的前面紧挨着放置一个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 调用。
- 入口函数示例:
PspCreateProcess(ntoskrnl/ps/process.c第 347 行)、PsCreateSystemThread、PsLookupProcessByProcessId
③ 内存管理器(Mm,ntoskrnl/mm/ 与 ntoskrnl/mm/ARM3/)
ReactOS 有两套内存管理实现:老的 mm/ 和新的 mm/ARM3/(ARM3 = "ARM revision 3",也用于 x86)。ARM3 是主要研究对象。功能包括:虚拟地址翻译、页故障(page fault)处理、换页池/非换页池、内存映射文件、section 对象。
- 入口函数示例:
MmAllocatePagesForMdl(mm/ARM3/mdlsup.c)、MiDispatchFault(mm/ARM3/pagfault.c)
④ I/O 管理器(Io,ntoskrnl/io/iomgr/、ntoskrnl/io/pnpmgr/)
驱动模型的核心:定义 IRP(I/O Request Packet)、设备栈、设备对象、驱动对象。应用发出的 I/O 请求被包装成 IRP 并沿设备栈一路下传。即插即用(PnP)管理器在 pnpmgr/ 中。
- 入口函数示例:
IoCreateDevice、IoCallDriver、IoAllocateIrp、IoCompleteRequest
⑤ 缓存管理器(Cc,ntoskrnl/cc/ 与 ntoskrnl/cache/)
为文件系统提供系统级文件缓存。cc/ 是老实现,cache/(newcc)是新实现。
⑥ 配置管理器(Cm,ntoskrnl/config/)
实现注册表:键、值、hive 结构。
⑦ 安全参考监视器(Se,ntoskrnl/se/)
SID(安全标识符)、ACL(访问控制列表)、Token(令牌)、权限检查。核心函数如 SeAccessCheck(ntoskrnl/se/accesschk.c)、SeAssignPrimaryToken(ntoskrnl/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/)
处理休眠/唤醒与电源状态。核心函数如 PoSetPowerState、PoCallDriver。
1.3.4 Win32k 子系统(win32k.sys)
图形与窗口子系统的内核态部分位于 win32ss/。它不属于执行体 ,而是通过回调机制挂接到 ntoskrnl:当线程第一次调用 GDI/User 函数时,KeUserModeCallback 切换到内核态并调用 win32k 的回调入口。这种设计使窗口绘制操作在内核态完成,避免频繁的用户态/内核态切换。
1.3.5 启动流程简述
以下是一次完整启动的简化流程,也是阅读源码时的路线图:
- bootloader (ReactOS 中为 freeldr.sys,位于
boot/)加载 ntoskrnl.exe、hal.dll、启动必需的驱动,并通过LOADER_PARAMETER_BLOCK传递启动参数。 - 内核入口
KiSystemStartup(架构相关的汇编文件中)设置好栈后调用KiInitializeKernel(ntoskrnl/ke/i386/kiinit.c),初始化处理器控制块与空闲线程。 ExpInitializeExecutive(ntoskrnl/ex/init.c)依次初始化各执行体子系统:对象、进程/线程、内存、I/O、配置、安全、缓存、LPC 等。- I/O 管理器加载其余驱动,完成设备初始化。
- 启动会话管理器
smss.exe,再由 smss 启动winlogon.exe与服务控制管理器。 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,跳转到内核的KiFastSystemCall或KiSystemService。内核从 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 作为主要许可协议(内核与大部分模块),部分组件使用 LGPL 或 BSD 许可。源码树根目录下可见 COPYING、COPYING.ARM、COPYING.LIB、COPYING3、COPYING3.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/*.c、ntoskrnl/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 halHALninja win32ssWin32k.sysninja mshtmlHTML 渲染ninja kernel32/ninja user32/ninja gdi32用户态基础 DLLninja 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 win32ss或ninja 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 / 系统调用(用户态可见的接口) | NtCreateFile、NtOpenProcess、NtReadFile |
ntdll.dll 的用户态 stub;真正的实现在 ntoskrnl 各子系统 |
Zw* |
Native API / 系统调用(内核态直接调用,强制 PreviousMode = KernelMode) | ZwCreateFile、ZwQueryInformationProcess |
ntoskrnl/ex/zw.S 派发表;转发到对应 Nt* |
Ke* |
Kernel 内核层(调度、IRQL、自旋锁、DPC/APC) | KeInitializeThread、KeRaiseIrql、KeAcquireSpinLock、KeBugCheckEx |
ntoskrnl/ke/ |
Ex* |
Executive 执行体辅助(池分配、lookaside、事件/互斥体、回调) | ExAllocatePoolWithTag、ExInitializeResourceLite、ExQueueWorkItem |
ntoskrnl/ex/ |
Mm* |
内存管理器(虚拟内存、页表、池、section) | MmAllocatePagesForMdl、MmMapLockedPagesSpecifyCache、MmCreateSection |
ntoskrnl/mm/、ntoskrnl/mm/ARM3/ |
Ob* |
对象管理器(句柄、引用计数、对象类型) | ObReferenceObjectByHandle、ObOpenObjectByPointer、ObInsertObject |
ntoskrnl/ob/ |
Ps* |
进程/线程管理器 | PsCreateSystemThread、PsLookupProcessByProcessId、PspCreateProcess |
ntoskrnl/ps/ |
Io* |
I/O 管理器(驱动、IRP、设备栈) | IoCreateDevice、IoCallDriver、IoAllocateIrp、IoCompleteRequest |
ntoskrnl/io/iomgr/、ntoskrnl/io/pnpmgr/ |
Se* |
安全参考监视器(SID、ACL、Token、权限检查) | SeAccessCheck、SeAssignPrimaryToken、SeSinglePrivilegeCheck |
ntoskrnl/se/ |
Cc* |
缓存管理器 | CcInitializeCacheMap、CcCopyRead、CcMdlRead |
ntoskrnl/cc/、ntoskrnl/cache/ |
Cm* |
配置管理器(注册表) | CmCreateKey、CmEnumerateKey、CmQueryValueKey |
ntoskrnl/config/ |
Rtl* |
运行时库(字符串、列表、异常、安全哈希、内存) | RtlCopyMemory、RtlInitUnicodeString、RtlCompareUnicodeString、RtlZeroMemory |
ntoskrnl/rtl/、sdk/lib/rtl/ |
Hal* |
硬件抽象层 | HalInitSystem、HalGetInterruptVector、HalAllocateCommonBuffer |
hal/ |
Lpc* / NtAlpc* |
本地过程调用(高级 LPC) | NtAlpcSendWaitReceivePort、LpcCreatePort |
ntoskrnl/lpc/ |
Po* |
电源管理 | PoSetPowerState、PoCallDriver |
ntoskrnl/po/ |
FsRtl* |
文件系统运行时库(Mcb、通配符匹配等) | FsRtlLookupMcbEntry、FsRtlIsNameInExpression |
ntoskrnl/fsrtl/ |
Kd* / Kdbg* |
内核调试器 | KdPrint、KdpPrompt、KdbpSymbolSearch |
ntoskrnl/kd/、ntoskrnl/kdbg/ |
Ki*、Mi*、Pi*、Oi* ... |
各子系统的内部未导出函数(第二个小写字母 i 暗示 internal) | KiDispatchInterrupt、MiDispatchFault、PspCreateProcess |
分布在各子系统目录 |
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 |
对象句柄;用户态与内核态之间的不透明引用 |
HKEY、HWND、HFILE |
更具体的句柄(注册表键、窗口、文件);注意 HWND 是用户态 Win32k 的句柄概念 |
PVOID、PCHAR、PWSTR、PUCHAR |
内存指针(void、char、宽字符、unsigned char) |
ULONG、ULONGLONG、SIZE_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_LEVEL、APC_LEVEL、DISPATCH_LEVEL、DIRQL |
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 的主要 MajorFunction (IRP_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 调用约定的具体含义
调用约定决定了:
- 参数传递顺序(左到右 / 右到左入栈)
- 谁清栈(调用方 / 被调用方)
- 前几个参数是否走寄存器
以 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 下,前两个参数会被放在 ECX 与 EDX 中。如果只有 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 源码时,建议按以下顺序建立"导航感":
- 找到入口 :
ntoskrnl/ke/i386/kiinit.c的KiInitializeKernel与ntoskrnl/ex/init.c的ExpInitializeExecutive是内核启动的两个主轴。 - 按前缀索引:遇到陌生函数,查 1.5.1 的前缀表,判断属于哪个子系统,然后到对应子目录找实现。
- "总地图"视角 :
ExpInitializeExecutive中按顺序调用的各初始化函数列表,就是执行体的"总地图"。 - 阅读头文件 :
ntoskrnl/include/internal/*.h(尤其是ke.h、mm.h、ob.h、ps.h、io.h、se.h)集中定义了各子系统的内部 API 与数据结构,经常比实现文件更容易理解。
1.6 本章小结
本章是为后续源码分析做铺垫的"总览章"。读者读完后应当能在脑中形成以下五个"心智模型":
- 历史模型:Windows 操作系统有两条路线------DOS 依附路线(9x 系列,已淘汰)与 NT 路线(现代 Windows 全部基于此)。ReactOS 兼容 Windows Server 2003 的 NT 内核。
- 地址空间模型 :x86 4 GB 虚拟地址被划分为用户空间(ring 3)和系统空间(ring 0)。CPU 通过
sysenter/INT 2Eh切到内核。ntdll.dll 的Nt*是用户态 stub;真正的实现位于 ntoskrnl。 - 内核分层模型:Windows 内核 = HAL + 内核层(Ke) + 执行体(Ob/Ps/Mm/Io/Se/Cc/Cm/Lpc/Rtl/Po 等) + Win32k。HAL 屏蔽硬件,内核层提供机制,执行体制定策略。
- 项目模型 :ReactOS 是一棵源码树,根目录的
ntoskrnl/、hal/、win32ss/、dll/win32/、sdk/等是研究重点;CMake + Ninja + RosBE 是构建工具链。 - 命名模型:函数前缀(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 内核的"对象"思想。