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,但放弃了。原因有三:
- 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 函数大致形式如下(概念性示意,真实代码为汇编):
; 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 风格字符串遍历计算长度:
/* 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,告诉对象管理器要做什么操作、对象名是什么、是否允许继承等:
/* 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,告诉调用者操作结果与附加信息:
/* 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 表示"哪个进程的哪个线程":
/* 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)完成硬件初始化。
/* 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 机制等。
/* 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 是阅读执行体源码的"总地图"——它依次调用各个子系统的初始化函数。
/* 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"的转换:/* 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* 函数大致结构如下(概念示意):
; 一个 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)是严重性位。判别一个状态码的语义不是看正负,而是看这个位:
/* 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 沿设备栈下传:
/* 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 代码如下(概念示意):
; 调用方压栈(从右向左)
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 调用:
/* 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'):
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 内核的"对象"思想。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)